diff --git a/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.controller.js b/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.controller.js index 3c4ddf1be..c3d58176f 100644 --- a/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.controller.js +++ b/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.controller.js @@ -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(); } diff --git a/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.html b/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.html index 5d3d3e52e..cd057f73c 100644 --- a/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.html +++ b/app/kubernetes/components/kube-services/kube-services-item/kube-services-item.html @@ -1,11 +1,11 @@ -
+

No Load balancer is available in this cluster, click here to configure load balancer.

-
+

No Load balancer is available in this cluster, contact your administrator.

@@ -13,9 +13,9 @@
@@ -24,7 +24,7 @@ publish a new port
-
+
Container port @@ -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 @@
-
+
Nodeport Nodeport number must be inside the range 30000-32767 or blank for system allocated.

+
+ This node port is already used. +
-
+
Loadbalancer port
@@ -177,7 +180,7 @@ >
diff --git a/app/kubernetes/components/kube-services/kube-services.js b/app/kubernetes/components/kube-services/kube-services.js index 22c1bef7c..1d3610f7d 100644 --- a/app/kubernetes/components/kube-services/kube-services.js +++ b/app/kubernetes/components/kube-services/kube-services.js @@ -7,6 +7,7 @@ angular.module('portainer.kubernetes').component('kubeServicesView', { bindings: { formValues: '=', isEdit: '<', + namespaces: '<', loadbalancerEnabled: '<', }, }); diff --git a/app/kubernetes/helpers/formValidationHelper.js b/app/kubernetes/helpers/formValidationHelper.js index 5b3cc8989..a48f38aab 100644 --- a/app/kubernetes/helpers/formValidationHelper.js +++ b/app/kubernetes/helpers/formValidationHelper.js @@ -13,14 +13,24 @@ class KubernetesFormValidationHelper { } static getDuplicates(names) { - const groupped = _.groupBy(names); + const grouped = _.groupBy(names); const res = {}; _.forEach(names, (name, index) => { - if (name && groupped[name].length > 1) { + if (name && grouped[name].length > 1) { res[index] = name; } }); return res; } + + static getDuplicateNodePorts(serviceNodePorts, allOtherNodePorts) { + const res = {}; + serviceNodePorts.forEach((sNodePort, index) => { + if (allOtherNodePorts.includes(sNodePort) || serviceNodePorts.filter((snp) => snp === sNodePort).length > 1) { + res[index] = sNodePort; + } + }); + return res; + } } export default KubernetesFormValidationHelper; diff --git a/app/kubernetes/models/service/models.js b/app/kubernetes/models/service/models.js index 19423eca8..5d0bea48d 100644 --- a/app/kubernetes/models/service/models.js +++ b/app/kubernetes/models/service/models.js @@ -23,6 +23,8 @@ const _KubernetesService = Object.freeze({ Note: '', Ingress: false, Selector: {}, + nodePortError: false, + servicePortError: false, }); export class KubernetesService { diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index ea90dc985..1b4055b7b 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -13,6 +13,7 @@ import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper'; import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper'; import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter'; import KubernetesPodConverter from 'Kubernetes/pod/converter'; +import { notifyError } from '@/portainer/services/notifications'; class KubernetesApplicationService { /* #region CONSTRUCTOR */ @@ -213,7 +214,11 @@ class KubernetesApplicationService { if (services) { services.forEach(async (service) => { - await this.KubernetesServiceService.create(service); + try { + await this.KubernetesServiceService.create(service); + } catch (error) { + notifyError('Unable to create service', error); + } }); } @@ -221,7 +226,11 @@ class KubernetesApplicationService { if (app instanceof KubernetesStatefulSet) { app.VolumeClaims = claims; - headlessService = await this.KubernetesServiceService.create(headlessService); + try { + headlessService = await this.KubernetesServiceService.create(headlessService); + } catch (error) { + notifyError('Unable to create service', error); + } app.ServiceName = headlessService.metadata.name; } else { const claimPromises = _.map(claims, (item) => { @@ -276,7 +285,11 @@ class KubernetesApplicationService { } if (newApp instanceof KubernetesStatefulSet) { - await this.KubernetesServiceService.patch(oldHeadlessService, newHeadlessService); + try { + await this.KubernetesServiceService.patch(oldHeadlessService, newHeadlessService); + } catch (error) { + notifyError('Unable to update service', error); + } } else { const claimPromises = _.map(newClaims, (newClaim) => { if (!newClaim.PreviousName && !newClaim.Id) { @@ -294,7 +307,11 @@ class KubernetesApplicationService { // Create services if (oldServices.length === 0 && newServices.length !== 0) { newServices.forEach(async (service) => { - await this.KubernetesServiceService.create(service); + try { + await this.KubernetesServiceService.create(service); + } catch (error) { + notifyError('Unable to create service', error); + } }); } @@ -315,9 +332,17 @@ class KubernetesApplicationService { newServices.forEach(async (newService) => { const oldServiceMatched = _.find(oldServices, { Name: newService.Name }); if (oldServiceMatched) { - await this.KubernetesServiceService.patch(oldServiceMatched, newService); + try { + await this.KubernetesServiceService.patch(oldServiceMatched, newService); + } catch (error) { + notifyError('Unable to update service', error); + } } else { - await this.KubernetesServiceService.create(newService); + try { + await this.KubernetesServiceService.create(newService); + } catch (error) { + notifyError('Unable to create service', error); + } } }); } diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 9a42dabfa..5702e7d51 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -1291,7 +1291,12 @@
- + @@ -1349,7 +1354,7 @@ ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM" type="button" class="btn btn-primary btn-sm !ml-0" - ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.imageValidityIsValid()" + ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.imageValidityIsValid() || ctrl.hasPortErrors()" ng-click="ctrl.deployApplication()" button-spinner="ctrl.state.actionInProgress" data-cy="k8sAppCreate-deployButton" diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index bd95ca5b3..90d5e4e5f 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -680,6 +680,11 @@ class KubernetesCreateApplicationController { return this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL ? this.nodeNumber : this.formValues.ReplicaCount; } + hasPortErrors() { + const portError = this.formValues.Services.some((service) => service.nodePortError || service.servicePortError); + return portError; + } + resourceReservationsOverflow() { const instances = this.effectiveInstances(); const cpu = this.formValues.CpuLimit; @@ -1187,6 +1192,7 @@ class KubernetesCreateApplicationController { const nonSystemNamespaces = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name)); + this.allNamespaces = resourcePools.map(({ Namespace }) => Namespace.Name); this.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1)); this.formValues.ResourcePool = this.resourcePools[0]; diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index e039e9ad2..ba23754dd 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -284,6 +284,7 @@ diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js index 3adfe7398..49c8384fa 100644 --- a/app/kubernetes/views/applications/edit/applicationController.js +++ b/app/kubernetes/views/applications/edit/applicationController.js @@ -108,6 +108,7 @@ class KubernetesApplicationController { Notifications, LocalStorage, ModalService, + KubernetesResourcePoolService, KubernetesApplicationService, KubernetesEventService, KubernetesStackService, @@ -121,6 +122,7 @@ class KubernetesApplicationController { this.Notifications = Notifications; this.LocalStorage = LocalStorage; this.ModalService = ModalService; + this.KubernetesResourcePoolService = KubernetesResourcePoolService; this.StackService = StackService; this.KubernetesApplicationService = KubernetesApplicationService; @@ -376,6 +378,9 @@ class KubernetesApplicationController { SelectedRevision: undefined, }; + const resourcePools = await this.KubernetesResourcePoolService.get(); + this.allNamespaces = resourcePools.map(({ Namespace }) => Namespace.Name); + await this.getApplication(); await this.getEvents(); this.updateApplicationKindText();