1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-19 13:29:41 +02:00

feat(k8s): Allow mix services for k8s app EE-1791 (#6198)

allow a mix of services for k8s in ui
This commit is contained in:
Richard Wei 2022-01-17 08:37:46 +13:00 committed by GitHub
parent edf048570b
commit c47e840b37
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 2336 additions and 1863 deletions

View file

@ -166,7 +166,7 @@
Status Status
</th> </th>
<th> <th>
Publishing mode Published
</th> </th>
<th> <th>
<a ng-click="$ctrl.changeOrderBy('CreationDate')"> <a ng-click="$ctrl.changeOrderBy('CreationDate')">
@ -233,15 +233,9 @@
{{ item.Pods[0].Status }} {{ item.Pods[0].Status }}
</td> </td>
<td> <td>
<span ng-if="item.PublishedPorts.length"> <span>
<span> {{ item.Services.length === 0 ? 'No' : 'Yes' }}
<a ng-click="$ctrl.onPublishingModeClick(item); $event.stopPropagation()">
<i class="fa {{ item.ServiceType | kubernetesApplicationServiceTypeIcon }}" aria-hidden="true" style="margin-right: 2px;"> </i>
{{ item.ServiceType | kubernetesApplicationServiceTypeText }}
</a>
</span>
</span> </span>
<span ng-if="item.PublishedPorts.length === 0">-</span>
</td> </td>
<td>{{ item.CreationDate | getisodate }} {{ item.ApplicationOwner ? 'by ' + item.ApplicationOwner : '' }}</td> <td>{{ item.CreationDate | getisodate }} {{ item.ApplicationOwner ? 'by ' + item.ApplicationOwner : '' }}</td>
</tr> </tr>

View file

@ -0,0 +1,83 @@
import _ from 'lodash-es';
import { KubernetesServicePort, KubernetesIngressServiceRoute } 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';
export default class KubeServicesItemViewController {
/* @ngInject */
constructor(EndpointProvider, Authentication) {
this.EndpointProvider = EndpointProvider;
this.Authentication = Authentication;
}
addPort() {
const p = new KubernetesServicePort();
p.nodePort = '';
p.port = '';
p.targetPort = '';
p.protocol = 'TCP';
if (this.ingressType) {
const r = new KubernetesIngressServiceRoute();
r.ServiceName = this.serviceName;
p.ingress = r;
p.Ingress = true;
}
this.servicePorts.push(p);
}
removePort(index) {
this.servicePorts.splice(index, 1);
}
servicePort(index) {
const targetPort = this.servicePorts[index].targetPort;
this.servicePorts[index].port = targetPort;
}
isAdmin() {
return this.Authentication.isAdmin();
}
onChangeContainerPort() {
const state = this.state.duplicates.targetPort;
const source = _.map(this.servicePorts, (sp) => sp.targetPort);
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
state.refs = duplicates;
state.hasRefs = Object.keys(duplicates).length > 0;
}
onChangeServicePort() {
const state = this.state.duplicates.servicePort;
const source = _.map(this.servicePorts, (sp) => sp.port);
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
state.refs = duplicates;
state.hasRefs = Object.keys(duplicates).length > 0;
}
onChangeNodePort() {
const state = this.state.duplicates.nodePort;
const source = _.map(this.servicePorts, (sp) => sp.nodePort);
const duplicates = KubernetesFormValidationHelper.getDuplicates(source);
state.refs = duplicates;
state.hasRefs = Object.keys(duplicates).length > 0;
}
$onInit() {
if (this.servicePorts.length === 0) {
this.addPort();
}
this.KubernetesApplicationPublishingTypes = KubernetesApplicationPublishingTypes;
this.state = {
duplicates: {
targetPort: new KubernetesFormValidationReferences(),
servicePort: new KubernetesFormValidationReferences(),
nodePort: new KubernetesFormValidationReferences(),
},
endpointId: this.EndpointProvider.endpointID(),
};
}
}

View file

@ -0,0 +1,242 @@
<form name="serviceForm">
<div ng-if="$ctrl.isAdmin()" class="small text-warning" ng-show="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled">
<p style="margin-top: 10px">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> No Load balancer is available in this cluster, click
<a ui-sref="portainer.k8sendpoint.kubernetesConfig({id: $ctrl.state.endpointId})">here</a> to configure load balancer.
</p>
</div>
<div ng-if="!$ctrl.isAdmin()" class="small text-warning" ng-show="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled">
<p style="margin-top: 10px"> <i class="fa fa-exclamation-circle" aria-hidden="true"></i> No Load balancer is available in this cluster, contract your administrator. </p>
</div>
<div
ng-if="
($ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && $ctrl.loadbalancerEnabled) ||
$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP ||
$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.NODE_PORT
"
>
<div ng-show="!$ctrl.multiItemDisable" style="margin-top: 5px; margin-bottom: 5px">
<label class="control-label text-left">Published ports</label>
<span class="label label-default interactive" style="margin-left: 10px" ng-click="$ctrl.addPort()" data-cy="k8sAppCreate-addNewPortButton">
<i class="fa fa-plus-circle" aria-hidden="true"></i> publish a new port
</span>
</div>
<div ng-repeat="servicePort in $ctrl.servicePorts" style="margin-top: 10px">
<div class="input-group input-group-sm">
<span class="input-group-addon">container port</span>
<input
type="number"
class="form-control"
name="container_port_{{ $index }}"
ng-model="servicePort.targetPort"
placeholder="80"
ng-min="1"
ng-max="65535"
ng-change="$ctrl.servicePort($index)"
required
ng-disabled="$ctrl.originalIngresses.length === 0 || ($ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled)"
ng-change="$ctrl.onChangeContainerPort()"
data-cy="k8sAppCreate-containerPort_{{ $index }}"
/>
</div>
<div class="input-group input-group-sm">
<span class="input-group-addon">service port</span>
<input
type="number"
class="form-control"
name="service_port_{{ $index }}"
ng-model="servicePort.port"
placeholder="80"
ng-min="1"
ng-max="65535"
required
ng-disabled="$ctrl.originalIngresses.length === 0 || ($ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled)"
ng-change="$ctrl.onChangeServicePort()"
data-cy="k8sAppCreate-servicePort_{{ $index }}"
/>
</div>
<div ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.NODE_PORT" class="input-group input-group-sm">
<span class="input-group-addon">nodeport</span>
<input
type="number"
class="form-control"
name="node_port_{{ $index }}"
ng-model="servicePort.nodePort"
placeholder="30080"
ng-min="30000"
ng-max="32767"
ng-change="$ctrl.onChangeNodePort()"
data-cy="k8sAppCreate-nodeportPort_{{ $index }}"
/>
</div>
<div ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER" class="input-group input-group-sm">
<span class="input-group-addon">loadbalancer port</span>
<input
type="number"
class="form-control"
name="loadbalancer_port_{{ $index }}"
ng-model="servicePort.port"
placeholder="80"
ng-min="1"
ng-max="65535"
required
ng-disabled="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.LOAD_BALANCER && !$ctrl.loadbalancerEnabled"
data-cy="k8sAppCreate-loadbalancerPort_{{ $index }}"
/>
</div>
<div ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP && $ctrl.ingressType" class="input-group input-group-sm">
<span class="input-group-addon">ingress</span>
<select
ng-init="servicePort.ingress.IngressName = $ctrl.originalIngresses[0].Name"
class="form-control"
name="ingress_port_{{ $index }}"
ng-model="servicePort.ingress.IngressName"
required
ng-disabled="$ctrl.originalIngresses.length === 0"
ng-options="ingress.Name as ingress.Name for ingress in $ctrl.originalIngresses"
data-cy="k8sAppCreate-ingressPort_{{ $index }}"
>
<option selected disabled hidden value="">Select an ingress</option>
</select>
</div>
<div ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP && $ctrl.ingressType" class="input-group input-group-sm">
<span class="input-group-addon">hostname</span>
<select
ng-init="servicePort.ingress.Host = $ctrl.originalIngresses[0].Hosts"
class="form-control"
name="hostname_port_{{ $index }}"
ng-model="servicePort.ingress.Host"
required
ng-disabled="$ctrl.originalIngresses.length === 0"
ng-options="ingress.Hosts as ingress.Hosts for ingress in $ctrl.originalIngresses"
data-cy="k8sAppCreate-hostnamePort_{{ $index }}"
>
<option selected disabled hidden value="">Select a hostname</option>
</select>
</div>
<div ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP && $ctrl.ingressType" class="input-group input-group-sm">
<span class="input-group-addon">route</span>
<input
class="form-control"
name="ingress_route_{{ $index }}"
ng-model="servicePort.ingress.Path"
placeholder="route"
required
ng-disabled="$ctrl.originalIngresses.length === 0"
ng-pattern="/^(\/?[a-zA-Z0-9]+([a-zA-Z0-9-/_]*[a-zA-Z0-9])?|[a-zA-Z0-9]+)|(\/){1}$/"
data-cy="k8sAppCreate-route_{{ $index }}"
/>
</div>
<div class="input-group col-sm-2 input-group-sm">
<div class="btn-group btn-group-sm">
<label
class="btn btn-primary"
ng-model="servicePort.protocol"
uib-btn-radio="'TCP'"
ng-change="ctrl.onChangePortProtocol($index)"
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'TCP')"
data-cy="k8sAppCreate-TCPButton_{{ $index }}"
>TCP</label
>
<label
class="btn btn-primary"
ng-model="servicePort.protocol"
uib-btn-radio="'UDP'"
ng-change="ctrl.onChangePortProtocol($index)"
ng-disabled="ctrl.isProtocolOptionDisabled($index, 'UDP')"
data-cy="k8sAppCreate-UDPButton_{{ $index }}"
>UDP</label
>
</div>
<button
ng-disabled="$ctrl.servicePorts.length === 1"
ng-show="!$ctrl.multiItemDisable"
class="btn btn-sm btn-danger"
type="button"
ng-click="$ctrl.removePort($index)"
data-cy="k8sAppCreate-rmPortButton_{{ $index }}"
>
<i class="fa fa-trash-alt" aria-hidden="true"></i>
</button>
</div>
<div class="col-sm-12 input-group input-group-sm">
<div class="col-sm-2">
<div class="small text-warning" style="margin-top: 5px">
<p ng-if="$ctrl.state.duplicates.targetPort.refs[$index] !== undefined">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This container port is already used.
</p>
</div>
<div class="small text-warning" ng-messages="serviceForm['container_port_'+$index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number is required.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
</div>
</div>
<div class="col-sm-2">
<div class="small text-warning" style="margin-top: 5px">
<p ng-if="$ctrl.state.duplicates.servicePort.refs[$index] !== undefined">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This service port is already used.
</p>
</div>
<div class="small text-warning" style="margin-top: 5px">
<div ng-messages="serviceForm['service_port_'+$index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Service port number is required.</p>
<p ng-message="min"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
<p ng-message="max"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Container port number must be inside the range 1-65535.</p>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="small text-warning" style="margin-top: 5px">
<div ng-messages="serviceForm['node_port_'+$index].$error">
<p ng-message="min"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Node port number must be inside the range 30000-32767 or blank for system allocated.</p
>
<p ng-message="max"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Node port number must be inside the range 30000-32767 or blank for system allocated.</p
>
</div>
</div>
</div>
<div class="col-sm-2">
<div class="small text-warning" style="margin-top: 5px">
<div ng-messages="serviceForm['ingress_port_'+$index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Ingress selection is required.</p>
</div>
</div>
</div>
<div class="col-sm-2">
<div class="small text-warning" style="margin-top: 5px">
<div ng-messages="serviceForm['hostname_port_'+$index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Host is required.</p>
</div>
</div>
</div>
<div class="col-sm-8" ng-show="">
<div class="small text-warning" style="margin-top: 5px">
<div ng-messages="serviceForm['ingress_route_'+$index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Route is required.</p>
<p ng-message="pattern"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of alphanumeric characters or the special characters: '-', '_' or '/'. It
must start and end with an alphanumeric character (e.g. 'my-route', or 'route-123').</p
>
</div>
</div>
</div>
</div>
</div>
</div>
</form>

View file

@ -0,0 +1,19 @@
import angular from 'angular';
import controller from './kube-services-item.controller';
angular.module('portainer.kubernetes').component('kubeServicesItemView', {
templateUrl: './kube-services-item.html',
controller,
bindings: {
serviceType: '<',
servicePorts: '=',
serviceRoutes: '=',
ingressType: '<',
originalIngresses: '<',
isEdit: '<',
serviceName: '<',
multiItemDisable: '<',
serviceIndex: '<',
loadbalancerEnabled: '<',
},
});

View file

@ -0,0 +1,105 @@
import { KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import { KubernetesApplicationPublishingTypes } from 'Kubernetes/models/application/models/constants';
export default class KubeServicesViewController {
/* @ngInject */
constructor($async, EndpointProvider, Authentication) {
this.$async = $async;
this.EndpointProvider = EndpointProvider;
this.Authentication = Authentication;
}
addEntry(service) {
const p = new KubernetesService();
if (service === KubernetesApplicationPublishingTypes.INGRESS) {
p.Type = KubernetesApplicationPublishingTypes.CLUSTER_IP;
p.Ingress = true;
} else {
p.Type = service;
}
p.Selector = this.formValues.Selector;
p.Name = this.getUniqName();
this.state.nameIndex += 1;
this.formValues.Services.push(p);
}
getUniqName() {
let name = this.formValues.Name + '-' + this.state.nameIndex;
const services = this.formValues.Services;
services.forEach((service) => {
if (service.Name === name) {
this.state.nameIndex += 1;
name = this.formValues.Name + '-' + this.state.nameIndex;
}
});
const UniqName = this.formValues.Name + '-' + this.state.nameIndex;
return UniqName;
}
deleteService(index) {
this.formValues.Services.splice(index, 1);
this.state.nameIndex -= 1;
}
addPort(index) {
const p = new KubernetesServicePort();
this.formValues.Services[index].Ports.push(p);
}
serviceType(type) {
switch (type) {
case KubernetesApplicationPublishingTypes.CLUSTER_IP:
return KubernetesServiceTypes.CLUSTER_IP;
case KubernetesApplicationPublishingTypes.NODE_PORT:
return KubernetesServiceTypes.NODE_PORT;
case KubernetesApplicationPublishingTypes.LOAD_BALANCER:
return KubernetesServiceTypes.LOAD_BALANCER;
case KubernetesApplicationPublishingTypes.INGRESS:
return KubernetesServiceTypes.INGRESS;
}
}
isAdmin() {
return this.Authentication.isAdmin();
}
iconStyle(type) {
switch (type) {
case KubernetesApplicationPublishingTypes.CLUSTER_IP:
return 'fa fa-list-alt';
case KubernetesApplicationPublishingTypes.NODE_PORT:
return 'fa fa-list';
case KubernetesApplicationPublishingTypes.LOAD_BALANCER:
return 'fa fa-project-diagram';
case KubernetesApplicationPublishingTypes.INGRESS:
return 'fa fa-route';
}
}
$onInit() {
this.state = {
serviceType: [
{
typeName: KubernetesServiceTypes.CLUSTER_IP,
typeValue: KubernetesApplicationPublishingTypes.CLUSTER_IP,
},
{
typeName: KubernetesServiceTypes.NODE_PORT,
typeValue: KubernetesApplicationPublishingTypes.NODE_PORT,
},
{
typeName: KubernetesServiceTypes.LOAD_BALANCER,
typeValue: KubernetesApplicationPublishingTypes.LOAD_BALANCER,
},
{
typeName: KubernetesServiceTypes.INGRESS,
typeValue: KubernetesApplicationPublishingTypes.INGRESS,
},
],
selected: KubernetesApplicationPublishingTypes.CLUSTER_IP,
nameIndex: this.formValues.Services.length,
endpointId: this.EndpointProvider.endpointID(),
};
}
}

View file

@ -0,0 +1,94 @@
<div class="col-sm-12 form-section-title">
Publishing the application
</div>
<div class="form-group">
<div class="col-sm-12 form-inline">
<div class="col-sm-5" style="padding-left: 0px;">
<select class="form-control" ng-model="$ctrl.state.selected" ng-options="item.typeValue as item.typeName for item in $ctrl.state.serviceType"></select>
<button type="button" class="btn btn-sm btn-default" style="margin-left: 0;" ng-click="$ctrl.addEntry( $ctrl.state.selected )" data-cy="k8sConfigCreate-createEntryButton">
<i class="fa fa-plus-circle" aria-hidden="true"></i> Create service
</button>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-12 form-inline" style="margin-top: 20px;" ng-repeat="service in $ctrl.formValues.Services">
<div ng-if="!$ctrl.formValues.Services[$index].Ingress">
<div class="text-muted">
<i class="{{ $ctrl.iconStyle(service.Type) }}" aria-hidden="true" style="margin-right: 2px;"></i>
{{ $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"
is-edit="$ctrl.isEdit"
loadbalancer-enabled="$ctrl.loadbalancerEnabled"
></kube-services-item-view>
<button
type="button"
class="btn btn-sm btn-danger space-right"
style="margin-left: 0; margin-top: 10px;"
ng-click="$ctrl.deleteService( $index )"
data-cy="k8sConfigCreate-removeButton"
>
<i class="fa fa-trash-alt" aria-hidden="true"></i> Remove
</button>
</div>
<div ng-if="$ctrl.formValues.Services[$index].Ingress && $ctrl.formValues.OriginalIngresses.length === 0">
<div class="text-muted">
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
Ingress
</div>
<div ng-if="$ctrl.isAdmin()" class="small text-warning">
<p style="margin-top: 10px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Ingress is not configured in this namespace, select another namespace or click
<a ui-sref="portainer.k8sendpoint.kubernetesConfig({id: $ctrl.state.endpointId})">here</a> to configure ingress.
</p>
</div>
<div ng-if="!$ctrl.isAdmin()" class="small text-warning">
<p style="margin-top: 10px;">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Ingress is not configured in this namespace, select another namespace or contact your administrator.
</p>
</div>
<button
type="button"
class="btn btn-sm btn-danger space-right"
style="margin-left: 0; margin-top: 10px;"
ng-click="$ctrl.deleteService( $index )"
data-cy="k8sConfigCreate-removeButton"
>
<i class="fa fa-trash-alt" aria-hidden="true"></i> Remove
</button>
</div>
<div ng-if="$ctrl.formValues.Services[$index].Ingress && $ctrl.formValues.OriginalIngresses.length !== 0">
<div class="text-muted">
<i class="fa fa-route" aria-hidden="true" style="margin-right: 2px;"></i>
Ingress
</div>
<kube-services-item-view
original-ingresses="$ctrl.formValues.OriginalIngresses"
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"
service-name="$ctrl.formValues.Services[$index].Name"
multi-item-disable="true"
></kube-services-item-view>
<button
type="button"
class="btn btn-sm btn-danger space-right"
style="margin-left: 0; margin-top: 10px;"
ng-click="$ctrl.deleteService( $index )"
data-cy="k8sConfigCreate-removeButton"
>
<i class="fa fa-trash-alt" aria-hidden="true"></i> Remove
</button>
</div>
</div>
</div>

View file

@ -0,0 +1,12 @@
import angular from 'angular';
import controller from './kube-services.controller';
angular.module('portainer.kubernetes').component('kubeServicesView', {
templateUrl: './kube-services.html',
controller,
bindings: {
formValues: '=',
isEdit: '<',
loadbalancerEnabled: '<',
},
});

View file

@ -120,6 +120,8 @@ class KubernetesApplicationConverter {
res.ServiceType = serviceType; res.ServiceType = serviceType;
res.ServiceId = service.metadata.uid; res.ServiceId = service.metadata.uid;
res.ServiceName = service.metadata.name; res.ServiceName = service.metadata.name;
res.ClusterIp = service.spec.clusterIP;
res.ExternalIp = service.spec.externalIP;
if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) { if (serviceType === KubernetesServiceTypes.LOAD_BALANCER) {
if (service.status.loadBalancer.ingress && service.status.loadBalancer.ingress.length > 0) { if (service.status.loadBalancer.ingress && service.status.loadBalancer.ingress.length > 0) {
@ -279,6 +281,8 @@ class KubernetesApplicationConverter {
res.ApplicationType = app.ApplicationType; res.ApplicationType = app.ApplicationType;
res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]); res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]);
res.Name = app.Name; res.Name = app.Name;
res.Services = KubernetesApplicationHelper.generateServicesFormValuesFromServices(app);
res.Selector = KubernetesApplicationHelper.generateSelectorFromService(app);
res.StackName = app.StackName; res.StackName = app.StackName;
res.ApplicationOwner = app.ApplicationOwner; res.ApplicationOwner = app.ApplicationOwner;
res.ImageModel.Image = app.Image; res.ImageModel.Image = app.Image;
@ -356,7 +360,9 @@ class KubernetesApplicationConverter {
service = undefined; service = undefined;
} }
return [app, headlessService, service, claims]; let services = KubernetesServiceConverter.applicationFormValuesToServices(formValues);
return [app, headlessService, services, service, claims];
} }
} }

View file

@ -52,10 +52,59 @@ class KubernetesServiceConverter {
return res; return res;
} }
static applicationFormValuesToServices(formValues) {
let services = [];
formValues.Services.forEach(function (service) {
const res = new KubernetesService();
res.Namespace = formValues.ResourcePool.Namespace.Name;
res.Name = service.Name;
res.StackName = formValues.StackName ? formValues.StackName : formValues.Name;
res.ApplicationOwner = formValues.ApplicationOwner;
res.ApplicationName = formValues.Name;
if (service.Type === KubernetesApplicationPublishingTypes.NODE_PORT) {
res.Type = KubernetesServiceTypes.NODE_PORT;
} else if (service.Type === KubernetesApplicationPublishingTypes.LOAD_BALANCER) {
res.Type = KubernetesServiceTypes.LOAD_BALANCER;
} else if (service.Type === KubernetesApplicationPublishingTypes.CLUSTER_IP) {
res.Type = KubernetesServiceTypes.CLUSTER_IP;
}
res.Ingress = service.Ingress;
if (service.Selector !== undefined) {
res.Selector = service.Selector;
} else {
res.Selector = {
app: formValues.Name,
};
}
let ports = [];
service.Ports.forEach(function (port, index) {
const res = new KubernetesServicePort();
res.name = 'port-' + index;
res.port = port.port;
if (port.nodePort) {
res.nodePort = port.nodePort;
}
res.protocol = port.protocol;
res.targetPort = port.targetPort;
res.ingress = port.ingress;
ports.push(res);
});
res.Ports = ports;
services.push(res);
});
return services;
}
static applicationFormValuesToHeadlessService(formValues) { static applicationFormValuesToHeadlessService(formValues) {
const res = KubernetesServiceConverter.applicationFormValuesToService(formValues); const res = KubernetesServiceConverter.applicationFormValuesToService(formValues);
res.Name = KubernetesServiceHelper.generateHeadlessServiceName(formValues.Name); res.Name = KubernetesServiceHelper.generateHeadlessServiceName(formValues.Name);
res.Headless = true; res.Headless = true;
res.Selector = {
app: formValues.Name,
};
return res; return res;
} }
@ -70,8 +119,20 @@ class KubernetesServiceConverter {
payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = service.StackName; payload.metadata.labels[KubernetesPortainerApplicationStackNameLabel] = service.StackName;
payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = service.ApplicationName; payload.metadata.labels[KubernetesPortainerApplicationNameLabel] = service.ApplicationName;
payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.ApplicationOwner; payload.metadata.labels[KubernetesPortainerApplicationOwnerLabel] = service.ApplicationOwner;
payload.spec.ports = service.Ports;
payload.spec.selector.app = service.ApplicationName; const ports = [];
service.Ports.forEach((port) => {
const p = {};
p.name = port.name;
p.port = port.port;
p.nodePort = port.nodePort;
p.protocol = port.protocol;
p.targetPort = port.targetPort;
ports.push(p);
});
payload.spec.ports = ports;
payload.spec.selector = service.Selector;
if (service.Headless) { if (service.Headless) {
payload.spec.clusterIP = KubernetesServiceHeadlessClusterIP; payload.spec.clusterIP = KubernetesServiceHeadlessClusterIP;
delete payload.spec.ports; delete payload.spec.ports;

View file

@ -1,6 +1,6 @@
import _ from 'lodash-es'; import _ from 'lodash-es';
import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/models/port/models'; import { KubernetesPortMapping, KubernetesPortMappingPort } from 'Kubernetes/models/port/models';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; import { KubernetesService, KubernetesServicePort, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models'; import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
import { import {
KubernetesApplicationAutoScalerFormValue, KubernetesApplicationAutoScalerFormValue,
@ -276,6 +276,61 @@ class KubernetesApplicationHelper {
} }
/* #endregion */ /* #endregion */
/* #region SERVICES -> SERVICES FORM VALUES */
static generateServicesFormValuesFromServices(app) {
let services = [];
app.Services.forEach(function (service) {
const svc = new KubernetesService();
svc.Namespace = service.metadata.namespace;
svc.Name = service.metadata.name;
svc.StackName = service.StackName;
svc.ApplicationOwner = app.ApplicationOwner;
svc.ApplicationName = app.ApplicationName;
svc.Type = service.spec.type;
if (service.spec.type === KubernetesServiceTypes.CLUSTER_IP) {
svc.Type = 1;
} else if (service.spec.type === KubernetesServiceTypes.NODE_PORT) {
svc.Type = 2;
} else if (service.spec.type === KubernetesServiceTypes.LOAD_BALANCER) {
svc.Type = 3;
}
let ports = [];
service.spec.ports.forEach(function (port) {
const svcport = new KubernetesServicePort();
svcport.name = port.name;
svcport.port = port.port;
svcport.nodePort = port.nodePort;
svcport.protocol = port.protocol;
svcport.targetPort = port.targetPort;
app.Ingresses.value.forEach((ingress) => {
const ingressMatched = _.find(ingress.Paths, { ServiceName: service.metadata.name });
if (ingressMatched) {
svcport.ingress = {
IngressName: ingressMatched.IngressName,
Host: ingressMatched.Host,
Path: ingressMatched.Path,
};
svc.Ingress = true;
}
});
ports.push(svcport);
});
svc.Ports = ports;
svc.Selector = app.Raw.spec.selector.matchLabels;
services.push(svc);
});
return services;
}
/* #endregion */
static generateSelectorFromService(app) {
const selector = app.Raw.spec.selector.matchLabels;
return selector;
}
/* #region PUBLISHED PORTS FV <> PUBLISHED PORTS */ /* #region PUBLISHED PORTS FV <> PUBLISHED PORTS */
static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts, ingress) { static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts, ingress) {
const generatePort = (port, rule) => { const generatePort = (port, rule) => {

View file

@ -12,5 +12,12 @@ class KubernetesServiceHelper {
} }
return _.find(services, (item) => item.spec.selector && _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector)); return _.find(services, (item) => item.spec.selector && _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector));
} }
static findApplicationBoundServices(services, rawApp) {
if (!rawApp.spec.template) {
return undefined;
}
return _.filter(services, (item) => item.spec.selector && _.isMatch(rawApp.spec.template.metadata.labels, item.spec.selector));
}
} }
export default KubernetesServiceHelper; export default KubernetesServiceHelper;

View file

@ -76,6 +76,69 @@ export class KubernetesIngressConverter {
return ingresses; return ingresses;
} }
static applicationFormValuesToDeleteIngresses(formValues, application) {
const ingresses = angular.copy(formValues.OriginalIngresses);
application.Services.forEach((service) => {
ingresses.forEach((ingress) => {
const path = _.find(ingress.Paths, { ServiceName: service.metadata.name });
if (path) {
_.remove(ingress.Paths, path);
}
});
});
return ingresses;
}
static deleteIngressByServiceName(formValues, service) {
const ingresses = angular.copy(formValues.OriginalIngresses);
ingresses.forEach((ingress) => {
const path = _.find(ingress.Paths, { ServiceName: service.Name });
if (path) {
_.remove(ingress.Paths, path);
}
});
return ingresses;
}
static newApplicationFormValuesToIngresses(formValues, serviceName, servicePorts) {
const ingresses = angular.copy(formValues.OriginalIngresses);
servicePorts.forEach((port) => {
const ingress = _.find(ingresses, { Name: port.ingress.IngressName });
if (ingress) {
const rule = new KubernetesIngressRule();
rule.ServiceName = serviceName;
rule.IngressName = port.ingress.IngressName;
rule.Host = port.ingress.Host;
rule.Path = _.startsWith(port.ingress.Path, '/') ? port.ingress.Path : '/' + port.ingress.Path;
rule.Port = port.port;
ingress.Paths.push(rule);
}
});
return ingresses;
}
static editingFormValuesToIngresses(formValues, serviceName, servicePorts) {
const ingresses = angular.copy(formValues.OriginalIngresses);
servicePorts.forEach((port) => {
const ingressMatched = _.find(ingresses, { Name: port.ingress.IngressName });
if (ingressMatched) {
const pathMatched = _.find(ingressMatched.Paths, { ServiceName: serviceName });
_.remove(ingressMatched.Paths, pathMatched);
const rule = new KubernetesIngressRule();
rule.ServiceName = serviceName;
rule.IngressName = port.ingress.IngressName;
rule.Host = port.ingress.Host;
rule.Path = _.startsWith(port.ingress.Path, '/') ? port.ingress.Path : '/' + port.ingress.Path;
rule.Port = port.port;
ingressMatched.Paths.push(rule);
}
});
return ingresses;
}
/** /**
* *
* @param {KubernetesResourcePoolIngressClassFormValue[]} formValues * @param {KubernetesResourcePoolIngressClassFormValue[]} formValues

View file

@ -18,6 +18,7 @@ export function KubernetesApplicationFormValues() {
this.ReplicaCount = 1; this.ReplicaCount = 1;
this.AutoScaler = {}; this.AutoScaler = {};
this.Containers = []; this.Containers = [];
this.Services = [];
this.EnvironmentVariables = []; // KubernetesApplicationEnvironmentVariableFormValue lis; this.EnvironmentVariables = []; // KubernetesApplicationEnvironmentVariableFormValue lis;
this.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED; this.DataAccessPolicy = KubernetesApplicationDataAccessPolicies.ISOLATED;
this.PersistedFolders = []; // KubernetesApplicationPersistedFolderFormValue lis; this.PersistedFolders = []; // KubernetesApplicationPersistedFolderFormValue lis;

View file

@ -4,6 +4,7 @@ export const KubernetesServiceTypes = Object.freeze({
LOAD_BALANCER: 'LoadBalancer', LOAD_BALANCER: 'LoadBalancer',
NODE_PORT: 'NodePort', NODE_PORT: 'NodePort',
CLUSTER_IP: 'ClusterIP', CLUSTER_IP: 'ClusterIP',
INGRESS: 'Ingress',
}); });
/** /**
@ -20,6 +21,8 @@ const _KubernetesService = Object.freeze({
ApplicationName: '', ApplicationName: '',
ApplicationOwner: '', ApplicationOwner: '',
Note: '', Note: '',
Ingress: false,
Selector: {},
}); });
export class KubernetesService { export class KubernetesService {
@ -28,6 +31,40 @@ export class KubernetesService {
} }
} }
const _KubernetesIngressService = Object.freeze({
Headless: false,
Namespace: '',
Name: '',
StackName: '',
Ports: [],
Type: '',
ClusterIP: '',
ApplicationName: '',
ApplicationOwner: '',
Note: '',
Ingress: true,
IngressRoute: [],
});
export class KubernetesIngressService {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesIngressService)));
}
}
const _KubernetesIngressServiceRoute = Object.freeze({
Host: '',
IngressName: '',
Path: '',
ServiceName: '',
});
export class KubernetesIngressServiceRoute {
constructor() {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesIngressServiceRoute)));
}
}
/** /**
* KubernetesServicePort Model * KubernetesServicePort Model
*/ */
@ -37,6 +74,7 @@ const _KubernetesServicePort = Object.freeze({
targetPort: 0, targetPort: 0,
protocol: '', protocol: '',
nodePort: 0, nodePort: 0,
ingress: '',
}); });
export class KubernetesServicePort { export class KubernetesServicePort {

View file

@ -120,16 +120,19 @@ class KubernetesApplicationService {
const services = await this.KubernetesServiceService.get(namespace); const services = await this.KubernetesServiceService.get(namespace);
const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw); const boundService = KubernetesServiceHelper.findApplicationBoundService(services, rootItem.value.Raw);
const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {}; const service = boundService ? await this.KubernetesServiceService.get(namespace, boundService.metadata.name) : {};
const boundServices = KubernetesServiceHelper.findApplicationBoundServices(services, rootItem.value.Raw);
const application = converterFunc(rootItem.value.Raw, pods.value, service.Raw, ingresses.value); const application = converterFunc(rootItem.value.Raw, pods.value, service.Raw, ingresses.value);
application.Yaml = rootItem.value.Yaml; application.Yaml = rootItem.value.Yaml;
application.Raw = rootItem.value.Raw; application.Raw = rootItem.value.Raw;
application.Pods = _.map(application.Pods, (item) => KubernetesPodConverter.apiToModel(item)); application.Pods = _.map(application.Pods, (item) => KubernetesPodConverter.apiToModel(item));
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application); application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
application.Services = boundServices;
const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application); const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers.value, application);
const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(namespace, boundScaler.Name) : undefined; const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(namespace, boundScaler.Name) : undefined;
application.AutoScaler = scaler; application.AutoScaler = scaler;
application.Ingresses = ingresses;
await this.KubernetesHistoryService.get(application); await this.KubernetesHistoryService.get(application);
@ -149,8 +152,10 @@ class KubernetesApplicationService {
const convertToApplication = (item, converterFunc, services, pods, ingresses) => { const convertToApplication = (item, converterFunc, services, pods, ingresses) => {
const service = KubernetesServiceHelper.findApplicationBoundService(services, item); const service = KubernetesServiceHelper.findApplicationBoundService(services, item);
const servicesFound = KubernetesServiceHelper.findApplicationBoundServices(services, item);
const application = converterFunc(item, pods, service, ingresses); const application = converterFunc(item, pods, service, ingresses);
application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application); application.Containers = KubernetesApplicationHelper.associateContainersAndApplication(application);
application.Services = servicesFound;
return application; return application;
}; };
@ -187,6 +192,7 @@ class KubernetesApplicationService {
const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers, application); const boundScaler = KubernetesHorizontalPodAutoScalerHelper.findApplicationBoundScaler(autoScalers, application);
const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(ns, boundScaler.Name) : undefined; const scaler = boundScaler ? await this.KubernetesHorizontalPodAutoScalerService.get(ns, boundScaler.Name) : undefined;
application.AutoScaler = scaler; application.AutoScaler = scaler;
application.Ingresses = await this.KubernetesIngressService.get(ns);
}) })
); );
return applications; return applications;
@ -214,7 +220,18 @@ class KubernetesApplicationService {
* also be displayed in the summary output (getCreatedApplicationResources) * also be displayed in the summary output (getCreatedApplicationResources)
*/ */
async createAsync(formValues) { async createAsync(formValues) {
let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues); // formValues -> Application
let [app, headlessService, services, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
if (services) {
services.forEach(async (service) => {
this.KubernetesServiceService.create(service);
if (service.Ingress) {
const ingresses = KubernetesIngressConverter.newApplicationFormValuesToIngresses(formValues, service.Name, service.Ports);
await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses));
}
});
}
if (service) { if (service) {
await this.KubernetesServiceService.create(service); await this.KubernetesServiceService.create(service);
@ -261,8 +278,8 @@ class KubernetesApplicationService {
* in this method should also be displayed in the summary output (getUpdatedApplicationResources) * in this method should also be displayed in the summary output (getUpdatedApplicationResources)
*/ */
async patchAsync(oldFormValues, newFormValues) { async patchAsync(oldFormValues, newFormValues) {
const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues); const [oldApp, oldHeadlessService, oldServices, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
const [newApp, newHeadlessService, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues); const [newApp, newHeadlessService, newServices, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues);
const oldApiService = this._getApplicationApiService(oldApp); const oldApiService = this._getApplicationApiService(oldApp);
const newApiService = this._getApplicationApiService(newApp); const newApiService = this._getApplicationApiService(newApp);
@ -271,6 +288,9 @@ class KubernetesApplicationService {
if (oldService) { if (oldService) {
await this.KubernetesServiceService.delete(oldService); await this.KubernetesServiceService.delete(oldService);
} }
if (newService) {
return '';
}
return await this.create(newFormValues); return await this.create(newFormValues);
} }
@ -290,25 +310,54 @@ class KubernetesApplicationService {
await newApiService.patch(oldApp, newApp); await newApiService.patch(oldApp, newApp);
if (oldService && newService) { if (oldServices.length === 0 && newServices.length !== 0) {
await this.KubernetesServiceService.patch(oldService, newService); newServices.forEach(async (service) => {
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS || oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { await this.KubernetesServiceService.create(service);
const oldIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(oldFormValues, oldService.Name); if (service.Ingress) {
const newIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name); const ingresses = KubernetesIngressConverter.newApplicationFormValuesToIngresses(oldFormValues, service.Name, service.Ports);
await Promise.all(this._generateIngressPatchPromises(oldIngresses, newIngresses)); await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
} }
} else if (!oldService && newService) { });
await this.KubernetesServiceService.create(newService); }
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name); if (oldServices.length !== 0 && newServices.length === 0) {
await Promise.all(this._generateIngressPatchPromises(newFormValues.OriginalIngresses, ingresses)); oldServices.forEach(async (oldService) => {
} if (oldService.Ingress) {
} else if (oldService && !newService) { const ingresses = KubernetesIngressConverter.deleteIngressByServiceName(oldFormValues, oldService);
await this.KubernetesServiceService.delete(oldService); await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
if (oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { }
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, oldService.Name); });
await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses)); await this.KubernetesServiceService.deleteAll(oldServices);
} }
if (oldServices.length !== 0 && newServices.length !== 0) {
newServices.forEach(async (newService) => {
const oldServiceMatched = _.find(oldServices, { Name: newService.Name });
if (oldServiceMatched) {
await this.KubernetesServiceService.patch(oldServiceMatched, newService);
if (newService.Ingress) {
const ingresses = KubernetesIngressConverter.editingFormValuesToIngresses(oldFormValues, newService.Name, newService.Ports);
await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
}
} else {
await this.KubernetesServiceService.create(newService);
if (newService.Ingress) {
const ingresses = KubernetesIngressConverter.newApplicationFormValuesToIngresses(oldFormValues, newService.Name, newService.Ports);
await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
}
}
});
oldServices.forEach(async (oldService) => {
const newServiceMatched = _.find(newServices, { Name: oldService.Name });
if (!newServiceMatched) {
await this.KubernetesServiceService.deleteSingle(oldService);
if (oldService.Ingress) {
const ingresses = KubernetesIngressConverter.deleteIngressByServiceName(oldFormValues, oldService);
await Promise.all(this._generateIngressPatchPromises(oldFormValues.OriginalIngresses, ingresses));
}
}
});
} }
const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp); const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp);
@ -381,16 +430,16 @@ class KubernetesApplicationService {
} }
if (application.ServiceType) { if (application.ServiceType) {
await this.KubernetesServiceService.delete(servicePayload); await this.KubernetesServiceService.delete(application.Services);
const isIngress = _.filter(application.PublishedPorts, (p) => p.IngressRules.length).length;
if (isIngress) { if (application.Ingresses.length) {
const originalIngresses = await this.KubernetesIngressService.get(payload.Namespace); const originalIngresses = await this.KubernetesIngressService.get(payload.Namespace);
const formValues = { const formValues = {
OriginalIngresses: originalIngresses, OriginalIngresses: originalIngresses,
PublishedPorts: KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(application.ServiceType, application.PublishedPorts), PublishedPorts: KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(application.ServiceType, application.PublishedPorts),
}; };
_.forEach(formValues.PublishedPorts, (p) => (p.NeedsDeletion = true)); const ingresses = KubernetesIngressConverter.applicationFormValuesToDeleteIngresses(formValues, application);
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, servicePayload.Name);
await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses)); await Promise.all(this._generateIngressPatchPromises(formValues.OriginalIngresses, ingresses));
} }
} }

View file

@ -14,6 +14,8 @@ class KubernetesServiceService {
this.createAsync = this.createAsync.bind(this); this.createAsync = this.createAsync.bind(this);
this.patchAsync = this.patchAsync.bind(this); this.patchAsync = this.patchAsync.bind(this);
this.deleteAsync = this.deleteAsync.bind(this); this.deleteAsync = this.deleteAsync.bind(this);
this.deleteSingleAsync = this.deleteSingleAsync.bind(this);
this.deleteAllAsync = this.deleteAllAsync.bind(this);
} }
/** /**
@ -95,7 +97,43 @@ class KubernetesServiceService {
/** /**
* DELETE * DELETE
*/ */
async deleteAsync(service) { async deleteAsync(services) {
services.forEach(async (service) => {
try {
const params = new KubernetesCommonParams();
params.id = service.metadata.name;
const namespace = service.metadata.namespace;
await this.KubernetesServices(namespace).delete(params).$promise;
} catch (err) {
// eslint-disable-next-line no-console
console.error('unable to remove service', err);
}
});
}
delete(services) {
return this.$async(this.deleteAsync, services);
}
async deleteAllAsync(formValuesServices) {
formValuesServices.forEach(async (service) => {
try {
const params = new KubernetesCommonParams();
params.id = service.Name;
const namespace = service.Namespace;
await this.KubernetesServices(namespace).delete(params).$promise;
} catch (err) {
// eslint-disable-next-line no-console
console.error('unable to remove service', err);
}
});
}
deleteAll(formValuesServices) {
return this.$async(this.deleteAllAsync, formValuesServices);
}
async deleteSingleAsync(service) {
try { try {
const params = new KubernetesCommonParams(); const params = new KubernetesCommonParams();
params.id = service.Name; params.id = service.Name;
@ -107,8 +145,8 @@ class KubernetesServiceService {
} }
} }
delete(service) { deleteSingle(service) {
return this.$async(this.deleteAsync, service); return this.$async(this.deleteSingleAsync, service);
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -720,7 +720,7 @@ class KubernetesCreateApplicationController {
} }
publishViaLoadBalancerEnabled() { publishViaLoadBalancerEnabled() {
return this.state.useLoadBalancer; return this.state.useLoadBalancer && this.state.maxLoadBalancersQuota !== 0;
} }
publishViaIngressEnabled() { publishViaIngressEnabled() {
@ -799,6 +799,14 @@ class KubernetesCreateApplicationController {
return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable || isPublishingWithoutPorts; return overflow || autoScalerOverflow || inProgress || invalid || hasNoChanges || nonScalable || isPublishingWithoutPorts;
} }
isExternalApplication() {
if (this.application) {
return KubernetesApplicationHelper.isExternalApplication(this.application);
} else {
return false;
}
}
disableLoadBalancerEdit() { disableLoadBalancerEdit() {
return ( return (
this.state.isEdit && this.state.isEdit &&

View file

@ -208,6 +208,17 @@
> >
<i class="fa fa-file-code space-right" aria-hidden="true"></i>Edit this application <i class="fa fa-file-code space-right" aria-hidden="true"></i>Edit this application
</button> </button>
<button
authorization="K8sApplicationDetailsW"
ng-if="ctrl.isExternalApplication()"
type="button"
class="btn btn-sm btn-primary"
ui-sref="kubernetes.applications.application.edit"
style="margin-left: 0;"
data-cy="k8sAppDetail-editAppButton"
>
<i class="fa fa-file-code space-right" aria-hidden="true"></i>Edit External application
</button>
<button <button
ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD" ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD"
type="button" type="button"
@ -249,147 +260,27 @@
</div> </div>
<div ng-if="ctrl.application.PublishedPorts.length > 0"> <div ng-if="ctrl.application.PublishedPorts.length > 0">
<!-- LB notice --> <!-- Services notice -->
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.LOAD_BALANCER"> <div>
<div class="small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This application is exposed through a service of type <span class="bold">{{ ctrl.application.ServiceType }}</span
>. Refer to the port configuration below to access it.
</div>
<div style="margin-top: 10px;" class="small text-muted">
<span ng-if="!ctrl.application.LoadBalancerIPAddress">
<p> Load balancer status: <i class="fa fa-cog fa-spin" style="margin-left: 2px;"></i> pending </p>
<p>
<u>what does the "pending" status means?</u>
<portainer-tooltip
position="bottom"
message="A pending status means that Portainer delegated the request to the provider responsible for creating the external load balancer. If it stays in pending state for long, this means that this capability might not be supported or you might have an issue with your cluster provider. Contact your cluster administrator for more information."
>
</portainer-tooltip>
</p>
</span>
<span ng-if="ctrl.application.LoadBalancerIPAddress">
<p> Load balancer status: <i class="fa fa-check green-icon" style="margin-left: 2px;"></i> available </p>
<p>
Load balancer IP address: {{ ctrl.application.LoadBalancerIPAddress }}
<span class="btn btn-primary btn-xs" ng-click="ctrl.copyLoadBalancerIP()" style="margin-left: 5px;">
<i class="fa fa-copy space-right" aria-hidden="true"></i>Copy
</span>
<span id="copyNotificationLB" style="margin-left: 7px; display: none; color: #23ae89;" class="small">
<i class="fa fa-check" aria-hidden="true"></i> copied
</span>
</p>
</span>
</div>
</div>
<!-- NodePort notice -->
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.NODE_PORT">
<div class="small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This application is exposed through a service of type <span class="bold">{{ ctrl.application.ServiceType }}</span
>. It can be reached using the IP address of any node in your cluster using the port configuration below.
</div>
</div>
<!-- ClusterIP notice -->
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.CLUSTER_IP && !ctrl.state.useIngress">
<div class="small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This application is exposed through a service of type <span class="bold">{{ ctrl.application.ServiceType }}</span
>. It can be reached via the application name <code>{{ ctrl.application.ServiceName }}</code> and the port configuration below.
<span class="btn btn-primary btn-xs" ng-click="ctrl.copyApplicationName()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</span>
<span id="copyNotificationApplicationName" style="margin-left: 7px; display: none; color: #23ae89;" class="small"
><i class="fa fa-check" aria-hidden="true"></i> copied</span
>
</div>
</div>
<!-- Ingress notice -->
<div ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.CLUSTER_IP && ctrl.state.useIngress">
<div class="small text-muted"> <div class="small text-muted">
<p> <p>
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i> <i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This application is exposed through a service of type <span class="bold">{{ ctrl.application.ServiceType }}</span This application is exposed through service(s) as below:
>. It can be reached via the application name <code>{{ ctrl.application.ServiceName }}</code> and the port configuration below.
<span class="btn btn-primary btn-xs" ng-click="ctrl.copyApplicationName()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</span>
<span id="copyNotificationApplicationName" style="margin-left: 7px; display: none; color: #23ae89;" class="small"
><i class="fa fa-check" aria-hidden="true"></i> copied</span
>
</p> </p>
<p>It is also associated to an <span class="bold">Ingress</span> and can be accessed via specific HTTP route(s).</p>
</div> </div>
</div> </div>
<!-- table --> <!-- table -->
<div style="margin-top: 15px;"> <kubernetes-application-services-table
<table class="table"> services="ctrl.application.Services"
<tbody> application="ctrl.application"
<tr class="text-muted"> public-url="ctrl.state.publicUrl"
<td style="width: 25%;">Container port</td> ></kubernetes-application-services-table>
<td style="width: 25%;">Service port</td> <!-- table -->
<td style="width: 50%;">HTTP route</td>
</tr> <!-- table -->
<tr ng-repeat-start="port in ctrl.application.PublishedPorts"> <kubernetes-application-ingress-table application="ctrl.application" public-url="ctrl.state.publicUrl"></kubernetes-application-ingress-table>
<td ng-if="!ctrl.portHasIngressRules(port)" data-cy="k8sAppDetail-containerPort">{{ port.TargetPort }}/{{ port.Protocol }}</td> <!-- table -->
<td ng-if="!ctrl.portHasIngressRules(port)">
<span ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.NODE_PORT" data-cy="k8sAppDetail-nodePort">
{{ port.NodePort }}
</span>
<span ng-if="ctrl.application.ServiceType !== ctrl.KubernetesServiceTypes.NODE_PORT" data-cy="k8sAppDetail-containerPort">
{{ port.Port }}
</span>
<a
ng-if="ctrl.application.LoadBalancerIPAddress"
ng-href="http://{{ ctrl.application.LoadBalancerIPAddress }}:{{ port.Port }}"
target="_blank"
style="margin-left: 5px;"
data-cy="k8sAppDetail-accessLink"
>
<i class="fa fa-external-link-alt" aria-hidden="true"></i> access
</a>
</td>
<td ng-if="!ctrl.portHasIngressRules(port)">-</td>
</tr>
<tr ng-repeat-end ng-repeat="rule in port.IngressRules">
<td data-cy="k8sAppDetail-httpRoute">{{ port.TargetPort }}/{{ port.Protocol }}</td>
<td>
<span ng-if="ctrl.application.ServiceType === ctrl.KubernetesServiceTypes.NODE_PORT" data-cy="k8sAppDetail-nodePort">
{{ port.NodePort }}
</span>
<span ng-if="ctrl.application.ServiceType !== ctrl.KubernetesServiceTypes.NODE_PORT" data-cy="k8sAppDetail-port">
{{ port.Port }}
</span>
<a
ng-if="ctrl.application.LoadBalancerIPAddress"
ng-href="http://{{ ctrl.application.LoadBalancerIPAddress }}:{{ port.Port }}"
target="_blank"
style="margin-left: 5px;"
>
<i class="fa fa-external-link-alt" aria-hidden="true"></i> access
</a>
</td>
<td>
<span
ng-if="!ctrl.ruleCanBeDisplayed(rule)"
class="text-muted"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
uib-tooltip="Ingress controller IP address not available yet"
style="cursor: pointer;"
>pending
</span>
<span ng-if="ctrl.ruleCanBeDisplayed(rule)">
<a ng-href="{{ ctrl.buildIngressRuleURL(rule) }}" target="_blank" data-cy="k8sAppDetail-httpRouteLink">
{{ ctrl.buildIngressRuleURL(rule) | stripprotocol }}
</a>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
<!-- !ACCESSING APPLICATION --> <!-- !ACCESSING APPLICATION -->
<!-- AUTO SCALING --> <!-- AUTO SCALING -->

View file

@ -1,6 +1,7 @@
import angular from 'angular'; import angular from 'angular';
import _ from 'lodash-es'; import _ from 'lodash-es';
import * as JsonPatch from 'fast-json-patch'; import * as JsonPatch from 'fast-json-patch';
import { import {
KubernetesApplicationDataAccessPolicies, KubernetesApplicationDataAccessPolicies,
KubernetesApplicationDeploymentTypes, KubernetesApplicationDeploymentTypes,
@ -112,7 +113,6 @@ class KubernetesApplicationController {
KubernetesStackService, KubernetesStackService,
KubernetesPodService, KubernetesPodService,
KubernetesNodeService, KubernetesNodeService,
StackService StackService
) { ) {
this.$async = $async; this.$async = $async;
@ -317,6 +317,7 @@ class KubernetesApplicationController {
this.application = application; this.application = application;
this.allContainers = KubernetesApplicationHelper.associateAllContainersAndApplication(application); this.allContainers = KubernetesApplicationHelper.associateAllContainersAndApplication(application);
this.formValues.Note = this.application.Note; this.formValues.Note = this.application.Note;
this.formValues.Services = this.application.Services;
if (this.application.Note) { if (this.application.Note) {
this.state.expandedNote = true; this.state.expandedNote = true;
} }
@ -347,6 +348,12 @@ class KubernetesApplicationController {
} }
async onInit() { async onInit() {
const endpointId = this.LocalStorage.getEndpointID();
const endpoints = this.LocalStorage.getEndpoints();
const endpoint = _.find(endpoints, function (item) {
return item.Id === endpointId;
});
this.state = { this.state = {
activeTab: 0, activeTab: 0,
currentName: this.$state.$current.name, currentName: this.$state.$current.name,
@ -365,6 +372,7 @@ class KubernetesApplicationController {
expandedNote: false, expandedNote: false,
useIngress: false, useIngress: false,
useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics, useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics,
publicUrl: endpoint.PublicURL,
}; };
this.state.activeTab = this.LocalStorage.getActiveTab('application'); this.state.activeTab = this.LocalStorage.getActiveTab('application');

View file

@ -0,0 +1,29 @@
import _ from 'lodash-es';
export default class KubernetesApplicationIngressController {
/* @ngInject */
constructor($async, KubernetesIngressService) {
this.$async = $async;
this.KubernetesIngressService = KubernetesIngressService;
}
$onInit() {
return this.$async(async () => {
this.hasIngress;
this.applicationIngress = [];
const ingresses = await this.KubernetesIngressService.get(this.application.ResourcePool);
const services = this.application.Services;
_.forEach(services, (service) => {
_.forEach(ingresses, (ingress) => {
_.forEach(ingress.Paths, (path) => {
if (path.ServiceName === service.metadata.name) {
this.applicationIngress.push(path);
this.hasIngress = true;
}
});
});
});
});
}
}

View file

@ -0,0 +1,24 @@
<div style="margin-top: 15px" ng-if="$ctrl.hasIngress">
<table class="table">
<tbody>
<tr class="text-muted">
<td style="width: 15%">Ingress name</td>
<td style="width: 10%">Service name</td>
<td style="width: 10%">Host</td>
<td style="width: 10%">Port</td>
<td style="width: 10%">Path</td>
<td style="width: 15%">HTTP Route</td>
</tr>
<tr ng-repeat="ingress in $ctrl.applicationIngress">
<td>{{ ingress.IngressName }}</td>
<td>{{ ingress.ServiceName }}</td>
<td>{{ ingress.Host }}</td>
<td>{{ ingress.Port }}</td>
<td>{{ ingress.Path }}</td>
<td
><a target="_blank" href="http://{{ ingress.Host }}{{ ingress.Path }}">{{ ingress.Host }}{{ ingress.Path }}</a></td
>
</tr>
</tbody>
</table>
</div>

View file

@ -0,0 +1,11 @@
import angular from 'angular';
import controller from './ingress-table.controller';
angular.module('portainer.kubernetes').component('kubernetesApplicationIngressTable', {
templateUrl: './ingress-table.html',
controller,
bindings: {
application: '<',
publicUrl: '<',
},
});

View file

@ -0,0 +1,56 @@
<!-- table -->
<div style="margin-top: 15px">
<table class="table">
<tbody>
<tr class="text-muted">
<td style="width: 15%">Service name</td>
<td style="width: 10%">Type</td>
<td style="width: 10%">Cluster IP</td>
<td style="width: 10%">External IP</td>
<td style="width: 10%">Container port</td>
<td style="width: 15%">Service port(s)</td>
</tr>
<tr ng-repeat="service in $ctrl.services">
<td>{{ service.metadata.name }}</td>
<td>{{ service.spec.type }}</td>
<td>{{ service.spec.clusterIP }}</td>
<td ng-show="service.spec.type === 'LoadBalancer'">
<div ng-show="service.status.loadBalancer.ingress">
<a target="_blank" ng-href="http://{{ service.status.loadBalancer.ingress[0].ip }}:{{ service.spec.ports[0].port }}">
<i class="fa fa-external-link-alt" aria-hidden="true"></i>
<span data-cy="k8sAppDetail-containerPort"> Access </span>
</a>
</div>
<div ng-show="!service.status.loadBalancer.ingress">
{{ service.spec.externalIP ? service.spec.externalIP : 'pending...' }}
</div>
</td>
<td ng-show="service.spec.type !== 'LoadBalancer'">{{ service.spec.externalIP ? service.spec.externalIP : '-' }}</td>
<td data-cy="k8sAppDetail-containerPort">
<div ng-repeat="port in service.spec.ports">{{ port.targetPort }}</div>
</td>
<td ng-if="!ctrl.portHasIngressRules(port)">
<div ng-repeat="port in service.spec.ports">
<a ng-if="$ctrl.publicUrl && port.nodePort" ng-href="http://{{ $ctrl.publicUrl }}:{{ port.nodePort }}" target="_blank" style="margin-left: 5px">
<i class="fa fa-external-link-alt" aria-hidden="true"></i>
<span data-cy="k8sAppDetail-containerPort">
{{ port.port }}
</span>
<span>{{ port.nodePort ? ':' : '' }}</span>
<span data-cy="k8sAppDetail-nodePort"> {{ port.nodePort }}/{{ port.protocol }} </span>
</a>
<div ng-if="!$ctrl.publicUrl">
<span data-cy="k8sAppDetail-servicePort">
{{ port.port }}
</span>
<span>{{ port.nodePort ? ':' : '' }}</span>
<span data-cy="k8sAppDetail-nodePort"> {{ port.nodePort }}/{{ port.protocol }} </span>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>

View file

@ -0,0 +1,10 @@
import angular from 'angular';
angular.module('portainer.kubernetes').component('kubernetesApplicationServicesTable', {
templateUrl: './services-table.html',
bindings: {
services: '<',
application: '<',
publicUrl: '<',
},
});

View file

@ -4,7 +4,7 @@ import { KubernetesApplicationFormValues } from 'Kubernetes/models/application/f
import { KubernetesDeployment } from 'Kubernetes/models/deployment/models'; import { KubernetesDeployment } from 'Kubernetes/models/deployment/models';
import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models'; import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models';
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models'; import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; import { KubernetesService, KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import { import {
KubernetesApplication, KubernetesApplication,
KubernetesApplicationDeploymentTypes, KubernetesApplicationDeploymentTypes,
@ -40,7 +40,17 @@ export default function (formValues, oldFormValues = {}) {
function getCreatedApplicationResources(formValues) { function getCreatedApplicationResources(formValues) {
const resources = []; const resources = [];
let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues); let [app, headlessService, services, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
if (services) {
services.forEach((service) => {
resources.push({ action: CREATE, kind: KubernetesResourceTypes.SERVICE, name: service.Name, type: service.Type || KubernetesServiceTypes.CLUSTER_IP });
if (formValues.OriginalIngresses.length !== 0) {
const ingresses = KubernetesIngressConverter.newApplicationFormValuesToIngresses(formValues, service.Name, service.Ports);
resources.push(...getIngressUpdateSummary(formValues.OriginalIngresses, ingresses));
}
});
}
if (service) { if (service) {
// Service // Service
@ -87,16 +97,15 @@ function getCreatedApplicationResources(formValues) {
function getUpdatedApplicationResources(oldFormValues, newFormValues) { function getUpdatedApplicationResources(oldFormValues, newFormValues) {
const resources = []; const resources = [];
const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues); const [oldApp, oldHeadlessService, oldServices, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
const [newApp, newHeadlessService, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues); const [newApp, newHeadlessService, newServices, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues);
const oldAppResourceType = getApplicationResourceType(oldApp); const oldAppResourceType = getApplicationResourceType(oldApp);
const newAppResourceType = getApplicationResourceType(newApp); const newAppResourceType = getApplicationResourceType(newApp);
if (oldAppResourceType !== newAppResourceType) { if (oldAppResourceType !== newAppResourceType) {
// Deployment // Deployment
resources.push({ action: DELETE, kind: oldAppResourceType, name: oldApp.Name }); resources.push({ action: DELETE, kind: oldAppResourceType, name: oldApp.Name });
if (oldService) { if (oldService && oldServices) {
// Service // Service
resources.push({ action: DELETE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP }); resources.push({ action: DELETE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP });
} }
@ -129,11 +138,13 @@ function getUpdatedApplicationResources(oldFormValues, newFormValues) {
// Deployment // Deployment
resources.push({ action: UPDATE, kind: oldAppResourceType, name: oldApp.Name }); resources.push({ action: UPDATE, kind: oldAppResourceType, name: oldApp.Name });
if (oldService && newService) { if (oldServices && newServices) {
// Service // Service
const serviceUpdateResourceSummary = getServiceUpdateResourceSummary(oldService, newService); const serviceUpdateResourceSummary = getServiceUpdateResourceSummary(oldServices, newServices);
if (serviceUpdateResourceSummary) { if (serviceUpdateResourceSummary !== null) {
resources.push(serviceUpdateResourceSummary); serviceUpdateResourceSummary.forEach((updateSummary) => {
resources.push(updateSummary);
});
} }
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS || oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) { if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS || oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
@ -224,10 +235,41 @@ function getVolumeClaimUpdateResourceSummary(oldPVC, newPVC) {
} }
// getServiceUpdateResourceSummary replicates KubernetesServiceService.patch // getServiceUpdateResourceSummary replicates KubernetesServiceService.patch
function getServiceUpdateResourceSummary(oldService, newService) { function getServiceUpdateResourceSummary(oldServices, newServices) {
const payload = KubernetesServiceConverter.patchPayload(oldService, newService); let summary = [];
if (payload.length) { newServices.forEach((newService) => {
return { action: UPDATE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP }; const oldServiceMatched = _.find(oldServices, { Name: newService.Name });
if (oldServiceMatched) {
const payload = KubernetesServiceConverter.patchPayload(oldServiceMatched, newService);
if (payload.length) {
const serviceUpdate = {
action: UPDATE,
kind: KubernetesResourceTypes.SERVICE,
name: oldServiceMatched.Name,
type: oldServiceMatched.Type || KubernetesServiceTypes.CLUSTER_IP,
};
summary.push(serviceUpdate);
}
} else {
const emptyService = new KubernetesService();
const payload = KubernetesServiceConverter.patchPayload(emptyService, newService);
if (payload.length) {
const serviceCreate = { action: CREATE, kind: KubernetesResourceTypes.SERVICE, name: newService.Name, type: newService.Type || KubernetesServiceTypes.CLUSTER_IP };
summary.push(serviceCreate);
}
}
});
oldServices.forEach((oldService) => {
const newServiceMatched = _.find(newServices, { Name: oldService.Name });
if (!newServiceMatched) {
const serviceDelete = { action: DELETE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP };
summary.push(serviceDelete);
}
});
if (summary.length !== 0) {
return summary;
} }
return null; return null;
} }