mirror of https://github.com/portainer/portainer
fix(ing): nodeport validate and show errors [EE-4373] (#7801)
parent
fd91de3571
commit
7a6ff10268
|
@ -1,5 +1,5 @@
|
||||||
import _ from 'lodash-es';
|
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 { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
|
||||||
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
|
||||||
import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models/constants';
|
import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models/constants';
|
||||||
|
@ -18,34 +18,17 @@ export default class KubeServicesItemViewController {
|
||||||
port.port = '';
|
port.port = '';
|
||||||
port.targetPort = '';
|
port.targetPort = '';
|
||||||
port.protocol = 'TCP';
|
port.protocol = 'TCP';
|
||||||
|
this.service.Ports.push(port);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removePort(index) {
|
removePort(index) {
|
||||||
this.servicePorts.splice(index, 1);
|
this.service.Ports.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
servicePort(index) {
|
servicePort(index) {
|
||||||
const targetPort = this.servicePorts[index].targetPort;
|
const targetPort = this.service.Ports[index].targetPort;
|
||||||
this.servicePorts[index].port = targetPort;
|
this.service.Ports[index].port = targetPort;
|
||||||
|
this.onChangeServicePort();
|
||||||
}
|
}
|
||||||
|
|
||||||
isAdmin() {
|
isAdmin() {
|
||||||
|
@ -54,7 +37,7 @@ export default class KubeServicesItemViewController {
|
||||||
|
|
||||||
onChangeContainerPort() {
|
onChangeContainerPort() {
|
||||||
const state = this.state.duplicates.targetPort;
|
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);
|
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||||
state.refs = duplicates;
|
state.refs = duplicates;
|
||||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||||
|
@ -62,22 +45,41 @@ export default class KubeServicesItemViewController {
|
||||||
|
|
||||||
onChangeServicePort() {
|
onChangeServicePort() {
|
||||||
const state = this.state.duplicates.servicePort;
|
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);
|
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
|
||||||
state.refs = duplicates;
|
state.refs = duplicates;
|
||||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||||
|
|
||||||
|
this.service.servicePortError = state.hasRefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeNodePort() {
|
onChangeNodePort() {
|
||||||
const state = this.state.duplicates.nodePort;
|
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.refs = duplicates;
|
||||||
state.hasRefs = Object.keys(duplicates).length > 0;
|
state.hasRefs = Object.keys(duplicates).length > 0;
|
||||||
|
|
||||||
|
this.service.nodePortError = state.hasRefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
$onInit() {
|
$onInit() {
|
||||||
if (this.servicePorts.length === 0) {
|
if (this.service.Ports.length === 0) {
|
||||||
this.addPort();
|
this.addPort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<ng-form name="serviceForm">
|
<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">
|
<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
|
<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.
|
<a class="hyperlink" ui-sref="kubernetes.cluster.setup">here</a> to configure load balancer.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<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.
|
<pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> No Load balancer is available in this cluster, contact your administrator.
|
||||||
</p>
|
</p>
|
||||||
|
@ -13,9 +13,9 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ng-if="
|
ng-if="
|
||||||
($ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && $ctrl.loadbalancerEnabled) ||
|
($ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && $ctrl.loadbalancerEnabled) ||
|
||||||
$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP ||
|
$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP ||
|
||||||
$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.NODE_PORT
|
$ctrl.service.Type === $ctrl.KubernetesApplicationPublishingTypes.NODE_PORT
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div ng-show="!$ctrl.multiItemDisable" class="mt-5 mb-5 vertical-center">
|
<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
|
<pr-icon icon="'plus'" mode="'alt'" size="'sm'" feather="true"></pr-icon> publish a new port
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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="form-group !mx-0 !pl-0 col-sm-3">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<span class="input-group-addon required">Container port</span>
|
<span class="input-group-addon required">Container port</span>
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
max="65535"
|
max="65535"
|
||||||
ng-change="$ctrl.servicePort($index)"
|
ng-change="$ctrl.servicePort($index)"
|
||||||
required
|
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()"
|
ng-change="$ctrl.onChangeContainerPort()"
|
||||||
data-cy="k8sAppCreate-containerPort_{{ $index }}"
|
data-cy="k8sAppCreate-containerPort_{{ $index }}"
|
||||||
/>
|
/>
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
min="1"
|
min="1"
|
||||||
max="65535"
|
max="65535"
|
||||||
required
|
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()"
|
ng-change="$ctrl.onChangeServicePort()"
|
||||||
data-cy="k8sAppCreate-servicePort_{{ $index }}"
|
data-cy="k8sAppCreate-servicePort_{{ $index }}"
|
||||||
/>
|
/>
|
||||||
|
@ -98,7 +98,7 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<div class="input-group input-group-sm">
|
||||||
<span class="input-group-addon required">Nodeport</span>
|
<span class="input-group-addon required">Nodeport</span>
|
||||||
<input
|
<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
|
><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
|
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>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="input-group input-group-sm">
|
||||||
<span class="input-group-addon">Loadbalancer port</span>
|
<span class="input-group-addon">Loadbalancer port</span>
|
||||||
<input
|
<input
|
||||||
|
@ -148,7 +151,7 @@
|
||||||
min="1"
|
min="1"
|
||||||
max="65535"
|
max="65535"
|
||||||
required
|
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 }}"
|
data-cy="k8sAppCreate-loadbalancerPort_{{ $index }}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -177,7 +180,7 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
ng-disabled="$ctrl.servicePorts.length === 1"
|
ng-disabled="$ctrl.service.Ports.length === 1"
|
||||||
ng-show="!$ctrl.multiItemDisable"
|
ng-show="!$ctrl.multiItemDisable"
|
||||||
class="btn btn-sm btn-dangerlight btn-only-icon"
|
class="btn btn-sm btn-dangerlight btn-only-icon"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -5,15 +5,10 @@ angular.module('portainer.kubernetes').component('kubeServicesItemView', {
|
||||||
templateUrl: './kube-services-item.html',
|
templateUrl: './kube-services-item.html',
|
||||||
controller,
|
controller,
|
||||||
bindings: {
|
bindings: {
|
||||||
serviceType: '<',
|
nodePortServices: '<',
|
||||||
servicePorts: '=',
|
formServices: '<',
|
||||||
serviceRoutes: '=',
|
service: '=',
|
||||||
ingressType: '<',
|
|
||||||
originalIngresses: '<',
|
|
||||||
isEdit: '<',
|
isEdit: '<',
|
||||||
serviceName: '<',
|
|
||||||
multiItemDisable: '<',
|
|
||||||
serviceIndex: '<',
|
|
||||||
loadbalancerEnabled: '<',
|
loadbalancerEnabled: '<',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
import { KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||||
import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models/constants';
|
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 {
|
export default class KubeServicesViewController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
@ -7,6 +9,7 @@ export default class KubeServicesViewController {
|
||||||
this.$async = $async;
|
this.$async = $async;
|
||||||
this.EndpointProvider = EndpointProvider;
|
this.EndpointProvider = EndpointProvider;
|
||||||
this.Authentication = Authentication;
|
this.Authentication = Authentication;
|
||||||
|
this.asyncOnInit = this.asyncOnInit.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
addEntry(service) {
|
addEntry(service) {
|
||||||
|
@ -74,6 +77,21 @@ export default class KubeServicesViewController {
|
||||||
return 'fa fa-project-diagram';
|
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() {
|
$onInit() {
|
||||||
this.state = {
|
this.state = {
|
||||||
serviceType: [
|
serviceType: [
|
||||||
|
@ -93,5 +111,6 @@ export default class KubeServicesViewController {
|
||||||
selected: KubernetesApplicationPublishingTypes.CLUSTER_IP,
|
selected: KubernetesApplicationPublishingTypes.CLUSTER_IP,
|
||||||
endpointId: this.EndpointProvider.endpointID(),
|
endpointId: this.EndpointProvider.endpointID(),
|
||||||
};
|
};
|
||||||
|
return this.$async(this.asyncOnInit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,10 +44,9 @@
|
||||||
{{ $ctrl.serviceType(service.Type) }}
|
{{ $ctrl.serviceType(service.Type) }}
|
||||||
</div>
|
</div>
|
||||||
<kube-services-item-view
|
<kube-services-item-view
|
||||||
service-routes="$ctrl.formValues.Services[$index].IngressRoute"
|
node-port-services="$ctrl.nodePortServices"
|
||||||
ingress-type="$ctrl.formValues.Services[$index].Ingress"
|
form-services="$ctrl.formValues.Services"
|
||||||
service-type="$ctrl.formValues.Services[$index].Type"
|
service="$ctrl.formValues.Services[$index]"
|
||||||
service-ports="$ctrl.formValues.Services[$index].Ports"
|
|
||||||
is-edit="$ctrl.isEdit"
|
is-edit="$ctrl.isEdit"
|
||||||
loadbalancer-enabled="$ctrl.loadbalancerEnabled"
|
loadbalancer-enabled="$ctrl.loadbalancerEnabled"
|
||||||
></kube-services-item-view>
|
></kube-services-item-view>
|
||||||
|
|
|
@ -7,6 +7,7 @@ angular.module('portainer.kubernetes').component('kubeServicesView', {
|
||||||
bindings: {
|
bindings: {
|
||||||
formValues: '=',
|
formValues: '=',
|
||||||
isEdit: '<',
|
isEdit: '<',
|
||||||
|
namespaces: '<',
|
||||||
loadbalancerEnabled: '<',
|
loadbalancerEnabled: '<',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,14 +13,24 @@ class KubernetesFormValidationHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDuplicates(names) {
|
static getDuplicates(names) {
|
||||||
const groupped = _.groupBy(names);
|
const grouped = _.groupBy(names);
|
||||||
const res = {};
|
const res = {};
|
||||||
_.forEach(names, (name, index) => {
|
_.forEach(names, (name, index) => {
|
||||||
if (name && groupped[name].length > 1) {
|
if (name && grouped[name].length > 1) {
|
||||||
res[index] = name;
|
res[index] = name;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return res;
|
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;
|
export default KubernetesFormValidationHelper;
|
||||||
|
|
|
@ -23,6 +23,8 @@ const _KubernetesService = Object.freeze({
|
||||||
Note: '',
|
Note: '',
|
||||||
Ingress: false,
|
Ingress: false,
|
||||||
Selector: {},
|
Selector: {},
|
||||||
|
nodePortError: false,
|
||||||
|
servicePortError: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class KubernetesService {
|
export class KubernetesService {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import KubernetesServiceHelper from 'Kubernetes/helpers/serviceHelper';
|
||||||
import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper';
|
import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper';
|
||||||
import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter';
|
import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter';
|
||||||
import KubernetesPodConverter from 'Kubernetes/pod/converter';
|
import KubernetesPodConverter from 'Kubernetes/pod/converter';
|
||||||
|
import { notifyError } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
class KubernetesApplicationService {
|
class KubernetesApplicationService {
|
||||||
/* #region CONSTRUCTOR */
|
/* #region CONSTRUCTOR */
|
||||||
|
@ -213,7 +214,11 @@ class KubernetesApplicationService {
|
||||||
|
|
||||||
if (services) {
|
if (services) {
|
||||||
services.forEach(async (service) => {
|
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) {
|
if (app instanceof KubernetesStatefulSet) {
|
||||||
app.VolumeClaims = claims;
|
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;
|
app.ServiceName = headlessService.metadata.name;
|
||||||
} else {
|
} else {
|
||||||
const claimPromises = _.map(claims, (item) => {
|
const claimPromises = _.map(claims, (item) => {
|
||||||
|
@ -276,7 +285,11 @@ class KubernetesApplicationService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newApp instanceof KubernetesStatefulSet) {
|
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 {
|
} else {
|
||||||
const claimPromises = _.map(newClaims, (newClaim) => {
|
const claimPromises = _.map(newClaims, (newClaim) => {
|
||||||
if (!newClaim.PreviousName && !newClaim.Id) {
|
if (!newClaim.PreviousName && !newClaim.Id) {
|
||||||
|
@ -294,7 +307,11 @@ class KubernetesApplicationService {
|
||||||
// Create services
|
// Create services
|
||||||
if (oldServices.length === 0 && newServices.length !== 0) {
|
if (oldServices.length === 0 && newServices.length !== 0) {
|
||||||
newServices.forEach(async (service) => {
|
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) => {
|
newServices.forEach(async (newService) => {
|
||||||
const oldServiceMatched = _.find(oldServices, { Name: newService.Name });
|
const oldServiceMatched = _.find(oldServices, { Name: newService.Name });
|
||||||
if (oldServiceMatched) {
|
if (oldServiceMatched) {
|
||||||
await this.KubernetesServiceService.patch(oldServiceMatched, newService);
|
try {
|
||||||
|
await this.KubernetesServiceService.patch(oldServiceMatched, newService);
|
||||||
|
} catch (error) {
|
||||||
|
notifyError('Unable to update service', error);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.KubernetesServiceService.create(newService);
|
try {
|
||||||
|
await this.KubernetesServiceService.create(newService);
|
||||||
|
} catch (error) {
|
||||||
|
notifyError('Unable to create service', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1291,7 +1291,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- kubernetes services options -->
|
<!-- kubernetes services options -->
|
||||||
<kube-services-view form-values="ctrl.formValues" is-edit="ctrl.state.isEdit" loadbalancer-enabled="ctrl.publishViaLoadBalancerEnabled()"></kube-services-view>
|
<kube-services-view
|
||||||
|
form-values="ctrl.formValues"
|
||||||
|
is-edit="ctrl.state.isEdit"
|
||||||
|
namespaces="ctrl.allNamespaces"
|
||||||
|
loadbalancer-enabled="ctrl.publishViaLoadBalancerEnabled()"
|
||||||
|
></kube-services-view>
|
||||||
<!-- kubernetes services options -->
|
<!-- kubernetes services options -->
|
||||||
|
|
||||||
<!-- summary -->
|
<!-- summary -->
|
||||||
|
@ -1349,7 +1354,7 @@
|
||||||
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
|
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm !ml-0"
|
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()"
|
ng-click="ctrl.deployApplication()"
|
||||||
button-spinner="ctrl.state.actionInProgress"
|
button-spinner="ctrl.state.actionInProgress"
|
||||||
data-cy="k8sAppCreate-deployButton"
|
data-cy="k8sAppCreate-deployButton"
|
||||||
|
|
|
@ -680,6 +680,11 @@ class KubernetesCreateApplicationController {
|
||||||
return this.formValues.DeploymentType === this.ApplicationDeploymentTypes.GLOBAL ? this.nodeNumber : this.formValues.ReplicaCount;
|
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() {
|
resourceReservationsOverflow() {
|
||||||
const instances = this.effectiveInstances();
|
const instances = this.effectiveInstances();
|
||||||
const cpu = this.formValues.CpuLimit;
|
const cpu = this.formValues.CpuLimit;
|
||||||
|
@ -1187,6 +1192,7 @@ class KubernetesCreateApplicationController {
|
||||||
|
|
||||||
const nonSystemNamespaces = _.filter(resourcePools, (resourcePool) => !KubernetesNamespaceHelper.isSystemNamespace(resourcePool.Namespace.Name));
|
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.resourcePools = _.sortBy(nonSystemNamespaces, ({ Namespace }) => (Namespace.Name === 'default' ? 0 : 1));
|
||||||
|
|
||||||
this.formValues.ResourcePool = this.resourcePools[0];
|
this.formValues.ResourcePool = this.resourcePools[0];
|
||||||
|
|
|
@ -284,6 +284,7 @@
|
||||||
<!-- table -->
|
<!-- table -->
|
||||||
<kubernetes-application-services-table
|
<kubernetes-application-services-table
|
||||||
services="ctrl.application.Services"
|
services="ctrl.application.Services"
|
||||||
|
namespaces="ctrl.allNamespaces"
|
||||||
application="ctrl.application"
|
application="ctrl.application"
|
||||||
public-url="ctrl.state.publicUrl"
|
public-url="ctrl.state.publicUrl"
|
||||||
></kubernetes-application-services-table>
|
></kubernetes-application-services-table>
|
||||||
|
|
|
@ -108,6 +108,7 @@ class KubernetesApplicationController {
|
||||||
Notifications,
|
Notifications,
|
||||||
LocalStorage,
|
LocalStorage,
|
||||||
ModalService,
|
ModalService,
|
||||||
|
KubernetesResourcePoolService,
|
||||||
KubernetesApplicationService,
|
KubernetesApplicationService,
|
||||||
KubernetesEventService,
|
KubernetesEventService,
|
||||||
KubernetesStackService,
|
KubernetesStackService,
|
||||||
|
@ -121,6 +122,7 @@ class KubernetesApplicationController {
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
this.LocalStorage = LocalStorage;
|
this.LocalStorage = LocalStorage;
|
||||||
this.ModalService = ModalService;
|
this.ModalService = ModalService;
|
||||||
|
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||||
this.StackService = StackService;
|
this.StackService = StackService;
|
||||||
|
|
||||||
this.KubernetesApplicationService = KubernetesApplicationService;
|
this.KubernetesApplicationService = KubernetesApplicationService;
|
||||||
|
@ -376,6 +378,9 @@ class KubernetesApplicationController {
|
||||||
SelectedRevision: undefined,
|
SelectedRevision: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resourcePools = await this.KubernetesResourcePoolService.get();
|
||||||
|
this.allNamespaces = resourcePools.map(({ Namespace }) => Namespace.Name);
|
||||||
|
|
||||||
await this.getApplication();
|
await this.getApplication();
|
||||||
await this.getEvents();
|
await this.getEvents();
|
||||||
this.updateApplicationKindText();
|
this.updateApplicationKindText();
|
||||||
|
|
Loading…
Reference in New Issue