mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
fix(ing): nodeport validate and show errors [EE-4373] (#7801)
This commit is contained in:
parent
fd91de3571
commit
7a6ff10268
13 changed files with 135 additions and 62 deletions
|
@ -1,5 +1,5 @@
|
|||
import _ from 'lodash-es';
|
||||
import { KubernetesServicePort, KubernetesIngressServiceRoute } from 'Kubernetes/models/service/models';
|
||||
import { KubernetesServicePort } from 'Kubernetes/models/service/models';
|
||||
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||
import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models/constants';
|
||||
|
@ -18,34 +18,17 @@ export default class KubeServicesItemViewController {
|
|||
port.port = '';
|
||||
port.targetPort = '';
|
||||
port.protocol = 'TCP';
|
||||
|
||||
if (this.ingressType) {
|
||||
const route = new KubernetesIngressServiceRoute();
|
||||
route.ServiceName = this.serviceName;
|
||||
|
||||
if (this.serviceType === KubernetesApplicationPublishingTypes.CLUSTER_IP && this.originalIngresses && this.originalIngresses.length > 0) {
|
||||
if (!route.IngressName) {
|
||||
route.IngressName = this.originalIngresses[0].Name;
|
||||
}
|
||||
|
||||
if (!route.Host) {
|
||||
route.Host = this.originalIngresses[0].Hosts[0];
|
||||
}
|
||||
}
|
||||
|
||||
port.ingress = route;
|
||||
port.Ingress = true;
|
||||
}
|
||||
this.servicePorts.push(port);
|
||||
this.service.Ports.push(port);
|
||||
}
|
||||
|
||||
removePort(index) {
|
||||
this.servicePorts.splice(index, 1);
|
||||
this.service.Ports.splice(index, 1);
|
||||
}
|
||||
|
||||
servicePort(index) {
|
||||
const targetPort = this.servicePorts[index].targetPort;
|
||||
this.servicePorts[index].port = targetPort;
|
||||
const targetPort = this.service.Ports[index].targetPort;
|
||||
this.service.Ports[index].port = targetPort;
|
||||
this.onChangeServicePort();
|
||||
}
|
||||
|
||||
isAdmin() {
|
||||
|
@ -54,7 +37,7 @@ export default class KubeServicesItemViewController {
|
|||
|
||||
onChangeContainerPort() {
|
||||
const state = this.state.duplicates.targetPort;
|
||||
const source = _.map(this.servicePorts, (sp) => sp.targetPort);
|
||||
const source = _.map(this.service.Ports, (sp) => sp.targetPort);
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||
state.refs = duplicates;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
|
@ -62,22 +45,41 @@ export default class KubeServicesItemViewController {
|
|||
|
||||
onChangeServicePort() {
|
||||
const state = this.state.duplicates.servicePort;
|
||||
const source = _.map(this.servicePorts, (sp) => sp.port);
|
||||
const source = _.map(this.service.Ports, (sp) => sp.port);
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||
state.refs = duplicates;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
|
||||
this.service.servicePortError = state.hasRefs;
|
||||
}
|
||||
|
||||
onChangeNodePort() {
|
||||
const state = this.state.duplicates.nodePort;
|
||||
const source = _.map(this.servicePorts, (sp) => sp.nodePort);
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||
|
||||
// create a list of all the node ports (number[]) in the cluster and current form
|
||||
const clusterNodePortsWithoutCurrentService = this.nodePortServices
|
||||
.filter((npService) => npService.Name !== this.service.Name)
|
||||
.map((npService) => npService.Ports)
|
||||
.flat()
|
||||
.map((npServicePorts) => npServicePorts.NodePort);
|
||||
const formNodePortsWithoutCurrentService = this.formServices
|
||||
.filter((formService) => formService.Type === KubernetesApplicationPublishingTypes.NODE_PORT && formService.Name !== this.service.Name)
|
||||
.map((formService) => formService.Ports)
|
||||
.flat()
|
||||
.map((formServicePorts) => formServicePorts.nodePort);
|
||||
const serviceNodePorts = this.service.Ports.map((sp) => sp.nodePort);
|
||||
// getDuplicates cares about the index, so put the serviceNodePorts at the start
|
||||
const allNodePortsWithoutCurrentService = [...clusterNodePortsWithoutCurrentService, ...formNodePortsWithoutCurrentService];
|
||||
|
||||
const duplicates = KubernetesFormValidationHelper.getDuplicateNodePorts(serviceNodePorts, allNodePortsWithoutCurrentService);
|
||||
state.refs = duplicates;
|
||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||
|
||||
this.service.nodePortError = state.hasRefs;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
if (this.servicePorts.length === 0) {
|
||||
if (this.service.Ports.length === 0) {
|
||||
this.addPort();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<ng-form name="serviceForm">
|
||||
<div ng-if="$ctrl.isAdmin()" class="small" ng-show="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled">
|
||||
<div ng-if="$ctrl.isAdmin()" class="small" ng-show="$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled">
|
||||
<p class="text-warning pt-2 vertical-center">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> No Load balancer is available in this cluster, click
|
||||
<a class="hyperlink" ui-sref="kubernetes.cluster.setup">here</a> to configure load balancer.
|
||||
</p>
|
||||
</div>
|
||||
<div ng-if="!$ctrl.isAdmin()" class="small" ng-show="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled">
|
||||
<div ng-if="!$ctrl.isAdmin()" class="small" ng-show="$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled">
|
||||
<p class="text-warning pt-2 vertical-center">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> No Load balancer is available in this cluster, contact your administrator.
|
||||
</p>
|
||||
|
@ -13,9 +13,9 @@
|
|||
|
||||
<div
|
||||
ng-if="
|
||||
($ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && $ctrl.loadbalancerEnabled) ||
|
||||
$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP ||
|
||||
$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.NODE_PORT
|
||||
($ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && $ctrl.loadbalancerEnabled) ||
|
||||
$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP ||
|
||||
$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.NODE_PORT
|
||||
"
|
||||
>
|
||||
<div ng-show="!$ctrl.multiItemDisable" class="mt-5 mb-5 vertical-center">
|
||||
|
@ -24,7 +24,7 @@
|
|||
<pr-icon icon="'plus'" mode="'alt'" size="'sm'" feather="true"></pr-icon> publish a new port
|
||||
</span>
|
||||
</div>
|
||||
<div ng-repeat="servicePort in $ctrl.servicePorts" class="mt-5 service-form row">
|
||||
<div ng-repeat="servicePort in $ctrl.service.Ports" class="mt-5 service-form row">
|
||||
<div class="form-group !mx-0 !pl-0 col-sm-3">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon required">Container port</span>
|
||||
|
@ -40,7 +40,7 @@
|
|||
max="65535"
|
||||
ng-change="$ctrl.servicePort($index)"
|
||||
required
|
||||
ng-disabled="$ctrl.originalIngresses.length === 0 || ($ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled)"
|
||||
ng-disabled="$ctrl.originalIngresses.length === 0 || ($ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled)"
|
||||
ng-change="$ctrl.onChangeContainerPort()"
|
||||
data-cy="k8sAppCreate-containerPort_{{ $index }}"
|
||||
/>
|
||||
|
@ -75,7 +75,7 @@
|
|||
min="1"
|
||||
max="65535"
|
||||
required
|
||||
ng-disabled="$ctrl.originalIngresses.length === 0 || ($ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled)"
|
||||
ng-disabled="$ctrl.originalIngresses.length === 0 || ($ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled)"
|
||||
ng-change="$ctrl.onChangeServicePort()"
|
||||
data-cy="k8sAppCreate-servicePort_{{ $index }}"
|
||||
/>
|
||||
|
@ -98,7 +98,7 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group !mx-0 !pl-0 col-sm-3" ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.NODE_PORT">
|
||||
<div class="form-group !mx-0 !pl-0 col-sm-3" ng-if="$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.NODE_PORT">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon required">Nodeport</span>
|
||||
<input
|
||||
|
@ -129,12 +129,15 @@
|
|||
><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Nodeport number must be inside the range 30000-32767 or blank for system
|
||||
allocated.</p
|
||||
>
|
||||
<div class="mt-1 text-warning" ng-if="$ctrl.state.duplicates.nodePort.refs[$index] !== undefined">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> This node port is already used.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group !mx-0 !pl-0 col-sm-3" ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER">
|
||||
<div class="form-group !mx-0 !pl-0 col-sm-3" ng-if="$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-addon">Loadbalancer port</span>
|
||||
<input
|
||||
|
@ -148,7 +151,7 @@
|
|||
min="1"
|
||||
max="65535"
|
||||
required
|
||||
ng-disabled="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled"
|
||||
ng-disabled="$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled"
|
||||
data-cy="k8sAppCreate-loadbalancerPort_{{ $index }}"
|
||||
/>
|
||||
</div>
|
||||
|
@ -177,7 +180,7 @@
|
|||
>
|
||||
</div>
|
||||
<button
|
||||
ng-disabled="$ctrl.servicePorts.length === 1"
|
||||
ng-disabled="$ctrl.service.Ports.length === 1"
|
||||
ng-show="!$ctrl.multiItemDisable"
|
||||
class="btn btn-sm btn-dangerlight btn-only-icon"
|
||||
type="button"
|
||||
|
|
|
@ -5,15 +5,10 @@ angular.module('portainer.kubernetes').component('kubeServicesItemView', {
|
|||
templateUrl: './kube-services-item.html',
|
||||
controller,
|
||||
bindings: {
|
||||
serviceType: '<',
|
||||
servicePorts: '=',
|
||||
serviceRoutes: '=',
|
||||
ingressType: '<',
|
||||
originalIngresses: '<',
|
||||
nodePortServices: '<',
|
||||
formServices: '<',
|
||||
service: '=',
|
||||
isEdit: '<',
|
||||
serviceName: '<',
|
||||
multiItemDisable: '<',
|
||||
serviceIndex: '<',
|
||||
loadbalancerEnabled: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||
import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models/constants';
|
||||
import { getServices } from 'Kubernetes/react/views/networks/services/service';
|
||||
import { notifyError } from '@/portainer/services/notifications';
|
||||
|
||||
export default class KubeServicesViewController {
|
||||
/* @ngInject */
|
||||
|
@ -7,6 +9,7 @@ export default class KubeServicesViewController {
|
|||
this.$async = $async;
|
||||
this.EndpointProvider = EndpointProvider;
|
||||
this.Authentication = Authentication;
|
||||
this.asyncOnInit = this.asyncOnInit.bind(this);
|
||||
}
|
||||
|
||||
addEntry(service) {
|
||||
|
@ -74,6 +77,21 @@ export default class KubeServicesViewController {
|
|||
return 'fa fa-project-diagram';
|
||||
}
|
||||
}
|
||||
|
||||
async asyncOnInit() {
|
||||
try {
|
||||
// get all nodeport services in the cluster, to validate unique nodeports in the form
|
||||
const allSettledServices = await Promise.allSettled(this.namespaces.map((namespace) => getServices(this.state.endpointId, namespace)));
|
||||
const allServices = allSettledServices
|
||||
.filter((settledService) => settledService.status === 'fulfilled' && settledService.value)
|
||||
.map((fulfilledService) => fulfilledService.value)
|
||||
.flat();
|
||||
this.nodePortServices = allServices.filter((service) => service.Type === 'NodePort');
|
||||
} catch (error) {
|
||||
notifyError('Failure', error, 'Failed getting services');
|
||||
}
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.state = {
|
||||
serviceType: [
|
||||
|
@ -93,5 +111,6 @@ export default class KubeServicesViewController {
|
|||
selected: KubernetesApplicationPublishingTypes.CLUSTER_IP,
|
||||
endpointId: this.EndpointProvider.endpointID(),
|
||||
};
|
||||
return this.$async(this.asyncOnInit);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,10 +44,9 @@
|
|||
{{ $ctrl.serviceType(service.Type) }}
|
||||
</div>
|
||||
<kube-services-item-view
|
||||
service-routes="$ctrl.formValues.Services[$index].IngressRoute"
|
||||
ingress-type="$ctrl.formValues.Services[$index].Ingress"
|
||||
service-type="$ctrl.formValues.Services[$index].Type"
|
||||
service-ports="$ctrl.formValues.Services[$index].Ports"
|
||||
node-port-services="$ctrl.nodePortServices"
|
||||
form-services="$ctrl.formValues.Services"
|
||||
service="$ctrl.formValues.Services[$index]"
|
||||
is-edit="$ctrl.isEdit"
|
||||
loadbalancer-enabled="$ctrl.loadbalancerEnabled"
|
||||
></kube-services-item-view>
|
||||
|
|
|
@ -7,6 +7,7 @@ angular.module('portainer.kubernetes').component('kubeServicesView', {
|
|||
bindings: {
|
||||
formValues: '=',
|
||||
isEdit: '<',
|
||||
namespaces: '<',
|
||||
loadbalancerEnabled: '<',
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue