Skip to content

Commit

Permalink
fix(application): edit cluster ip services EE-4328 (portainer#7775)
Browse files Browse the repository at this point in the history
  • Loading branch information
testA113 committed Oct 7, 2022
1 parent 819dc4d commit 315c1c7
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default class KubeServicesItemViewController {
const route = new KubernetesIngressServiceRoute();
route.ServiceName = this.serviceName;

if (this.serviceType === KubernetesApplicationPublishingTypes.CLUSTER_IP && this.originalIngresses.length > 0) {
if (this.serviceType === KubernetesApplicationPublishingTypes.CLUSTER_IP && this.originalIngresses && this.originalIngresses.length > 0) {
if (!route.IngressName) {
route.IngressName = this.originalIngresses[0].Name;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,82 +154,7 @@
</div>
</div>

<div class="form-group !mx-0 !pl-0 col-sm-3" ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP && $ctrl.ingressType">
<div class="input-group input-group-sm">
<span class="input-group-addon">Ingress</span>
<select
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>
<span>
<div class="small mt-5 text-warning">
<div ng-messages="serviceForm['ingress_port_'+$index].$error">
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Ingress selection is required.</p>
</div>
</div>
</span>
</div>

<div class="form-group !mx-0 !pl-0 col-sm-3" ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP && $ctrl.ingressType">
<div class="input-group input-group-sm">
<span class="input-group-addon">Hostname</span>
<select
class="form-control"
name="hostname_port_{{ $index }}"
ng-model="servicePort.ingress.Host"
required
ng-disabled="$ctrl.originalIngresses.length === 0"
ng-options="host as host for host in ($ctrl.originalIngresses | filter:{ Name: servicePort.ingress.IngressName })[0].Hosts"
data-cy="k8sAppCreate-hostnamePort_{{ $index }}"
>
<option selected disabled hidden value="">Select a hostname</option>
</select>
</div>
<span>
<div class="small mt-1 text-warning">
<div ng-messages="serviceForm['hostname_port_'+$index].$error">
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Hostname is required.</p>
</div>
</div>
</span>
</div>

<div class="form-group !mx-0 !pl-0 col-sm-3 clear-both" ng-if="$ctrl.serviceType === $ctrl.KubernetesApplicationPublishingTypes.CLUSTER_IP && $ctrl.ingressType">
<div class="input-group input-group-sm">
<span class="input-group-addon required">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>
<span>
<div class="small mt-1 text-warning">
<div ng-messages="serviceForm['ingress_route_'+$index].$error">
<p class="vertical-center" ng-message="required"><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> Route is required.</p>
<p class="vertical-center" ng-message="pattern"
><pr-icon icon="'alert-triangle'" mode="'warning'" feather="true"></pr-icon> 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>
</span>
</div>

<div class="form-group !mx-0 !pl-0 col-sm-2">
<div class="form-group !mx-0 !pl-0 col-sm-3">
<div class="input-group input-group-sm">
<div class="btn-group btn-group-sm">
<label
Expand Down
2 changes: 1 addition & 1 deletion app/kubernetes/components/kube-services/kube-services.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

<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>
<div class="text-muted vertical-center">
<pr-icon ng-if="$ctrl.serviceType(service.Type) === 'ClusterIP'" icon="'list'" feather="true"></pr-icon>
<pr-icon ng-if="$ctrl.serviceType(service.Type) === 'LoadBalancer'" icon="'svg-dataflow'"></pr-icon>
Expand Down
14 changes: 9 additions & 5 deletions app/kubernetes/helpers/application/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,13 +308,17 @@ class KubernetesApplicationHelper {
svcport.targetPort = port.targetPort;

app.Ingresses.value.forEach((ingress) => {
const ingressMatched = _.find(ingress.Paths, { ServiceName: service.metadata.name });
if (ingressMatched) {
const ingressNameMatched = ingress.Paths.find((ingPath) => ingPath.ServiceName === service.metadata.name);
const ingressPortMatched = ingress.Paths.find((ingPath) => ingPath.Port === port.port);
// only add ingress info to the port if the ingress serviceport matches the port in the service
if (ingressPortMatched) {
svcport.ingress = {
IngressName: ingressMatched.IngressName,
Host: ingressMatched.Host,
Path: ingressMatched.Path,
IngressName: ingressPortMatched.IngressName,
Host: ingressPortMatched.Host,
Path: ingressPortMatched.Path,
};
}
if (ingressNameMatched) {
svc.Ingress = true;
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,21 @@ export function CreateIngressView() {
{ label: 'Select a service', value: '' },
...(servicesOptions || []),
];
const servicePorts = clusterIpServices
? Object.fromEntries(
clusterIpServices?.map((service) => [
service.Name,
service.Ports.map((port) => ({
label: String(port.Port),
value: String(port.Port),
})),
])
)
: {};
const servicePorts = useMemo(
() =>
clusterIpServices
? Object.fromEntries(
clusterIpServices?.map((service) => [
service.Name,
service.Ports.map((port) => ({
label: String(port.Port),
value: String(port.Port),
})),
])
)
: {},
[clusterIpServices]
);

const existingIngressClass = useMemo(
() =>
Expand Down Expand Up @@ -222,6 +226,32 @@ export function CreateIngressView() {
params.namespace,
]);

useEffect(() => {
// for each path in each host, if the service port doesn't exist as an option, change it to the first option
if (ingressRule?.Hosts?.length) {
ingressRule.Hosts.forEach((host, hIndex) => {
host?.Paths?.forEach((path, pIndex) => {
const serviceName = path.ServiceName;
const currentServicePorts = servicePorts[serviceName]?.map(
(p) => p.value
);
if (
currentServicePorts?.length &&
!currentServicePorts?.includes(String(path.ServicePort))
) {
handlePathChange(
hIndex,
pIndex,
'ServicePort',
currentServicePorts[0]
);
}
});
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ingressRule, servicePorts]);

useEffect(() => {
if (namespace.length > 0) {
validate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,8 @@ export function IngressForm({
)}

<Button
className="btn btn-sm btn-dangerlight ml-2"
className="btn btn-sm ml-2"
color="dangerlight"
type="button"
data-cy={`k8sAppCreate-rmHostButton_${hostIndex}`}
onClick={() => removeIngressHost(hostIndex)}
Expand Down Expand Up @@ -534,7 +535,8 @@ export function IngressForm({

<div className="form-group !pl-0 col-sm-1 !m-0">
<Button
className="btn btn-sm btn-dangerlight btn-only-icon !ml-0 vertical-center"
className="btn btn-sm btn-only-icon !ml-0 vertical-center"
color="dangerlight"
type="button"
data-cy={`k8sAppCreate-rmPortButton_${hostIndex}-${pathIndex}`}
onClick={() => removeIngressRoute(hostIndex, pathIndex)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import KubernetesApplicationHelper from 'Kubernetes/helpers/application/index';
import KubernetesVolumeHelper from 'Kubernetes/helpers/volumeHelper';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { KubernetesNodeHelper } from 'Kubernetes/node/helper';
import { updateIngress, getIngresses } from '@/kubernetes/react/views/networks/ingresses/service';
import { confirmUpdateAppIngress } from '@/portainer/services/modal.service/prompt';

class KubernetesCreateApplicationController {
/* #region CONSTRUCTOR */
Expand Down Expand Up @@ -144,6 +146,8 @@ class KubernetesCreateApplicationController {
this.setPullImageValidity = this.setPullImageValidity.bind(this);
this.onChangeFileContent = this.onChangeFileContent.bind(this);
this.onServicePublishChange = this.onServicePublishChange.bind(this);
this.checkIngressesToUpdate = this.checkIngressesToUpdate.bind(this);
this.confirmUpdateApplicationAsync = this.confirmUpdateApplicationAsync.bind(this);
}
/* #endregion */

Expand Down Expand Up @@ -1015,7 +1019,16 @@ class KubernetesCreateApplicationController {
}
}

async updateApplicationAsync() {
async updateApplicationAsync(ingressesToUpdate, rulePlural) {
if (ingressesToUpdate.length) {
try {
await Promise.all(ingressesToUpdate.map((ing) => updateIngress(this.endpoint.Id, ing)));
this.Notifications.success('Success', `Ingress ${rulePlural} successfully updated`);
} catch (error) {
this.Notifications.error('Failure', error, 'Unable to update ingress');
}
}

try {
this.state.actionInProgress = true;
await this.KubernetesApplicationService.patch(this.savedFormValues, this.formValues);
Expand All @@ -1028,13 +1041,100 @@ class KubernetesCreateApplicationController {
}
}

deployApplication() {
if (this.state.isEdit) {
async confirmUpdateApplicationAsync() {
const [ingressesToUpdate, servicePortsToUpdate] = await this.checkIngressesToUpdate();
// if there is an ingressesToUpdate, then show a warning modal with asking if they want to update the ingresses
if (ingressesToUpdate.length) {
const rulePlural = ingressesToUpdate.length > 1 ? 'rules' : 'rule';
const noMatchSentence =
servicePortsToUpdate.length > 1
? `Service ports in this application no longer match the ingress ${rulePlural}.`
: `A service port in this application no longer matches the ingress ${rulePlural} which may break ingress rule paths.`;
const message = `
<ul class="ml-3">
<li>Updating the application may cause a service interruption.</li>
<li>${noMatchSentence}</li>
</ul>
`;
const inputLabel = `Update ingress ${rulePlural} to match the service port changes`;
confirmUpdateAppIngress(`Are you sure?`, message, inputLabel, (value) => {
if (value === null) {
return;
}
if (value.length === 0) {
return this.$async(this.updateApplicationAsync, [], '');
}
if (value[0] === '1') {
return this.$async(this.updateApplicationAsync, ingressesToUpdate, rulePlural);
}
});
} else {
this.ModalService.confirmUpdate('Updating the application may cause a service interruption. Do you wish to continue?', (confirmed) => {
if (confirmed) {
return this.$async(this.updateApplicationAsync);
return this.$async(this.updateApplicationAsync, [], '');
}
});
}
}

// check if service ports with ingresses have changed and allow the user to update the ingress to the new port values with a modal
async checkIngressesToUpdate() {
let ingressesToUpdate = [];
let servicePortsToUpdate = [];
const fullIngresses = await getIngresses(this.endpoint.Id, this.formValues.ResourcePool.Namespace.Name);
this.formValues.Services.forEach((updatedService) => {
const oldServiceIndex = this.oldFormValues.Services.findIndex((oldService) => oldService.Name === updatedService.Name);
const numberOfPortsInOldService = this.oldFormValues.Services[oldServiceIndex] && this.oldFormValues.Services[oldServiceIndex].Ports.length;
// if the service has an ingress and there is the same number of ports or more in the updated service
if (updatedService.Ingress && numberOfPortsInOldService && numberOfPortsInOldService <= updatedService.Ports.length) {
const updatedOldPorts = updatedService.Ports.slice(0, numberOfPortsInOldService);
const ingressesForService = fullIngresses.filter((ing) => {
const ingServiceNames = ing.Paths.map((path) => path.ServiceName);
if (ingServiceNames.includes(updatedService.Name)) {
return true;
}
});
ingressesForService.forEach((ingressForService) => {
updatedOldPorts.forEach((servicePort, pIndex) => {
if (servicePort.ingress) {
// if there isn't a ingress path that has a matching service name and port
const doesIngressPathMatchServicePort = ingressForService.Paths.find((ingPath) => ingPath.ServiceName === updatedService.Name && ingPath.Port === servicePort.port);
if (!doesIngressPathMatchServicePort) {
// then find the ingress path index to update by looking for the matching port in the old form values
const oldServicePort = this.oldFormValues.Services[oldServiceIndex].Ports[pIndex].port;
const newServicePort = servicePort.port;

const ingressPathIndex = ingressForService.Paths.findIndex((ingPath) => {
return ingPath.ServiceName === updatedService.Name && ingPath.Port === oldServicePort;
});
if (ingressPathIndex !== -1) {
// if the ingress to update isn't in the ingressesToUpdate list
const ingressUpdateIndex = ingressesToUpdate.findIndex((ing) => ing.Name === ingressForService.Name);
if (ingressUpdateIndex === -1) {
// then add it to the list with the new port
const ingressToUpdate = angular.copy(ingressForService);
ingressToUpdate.Paths[ingressPathIndex].Port = newServicePort;
ingressesToUpdate.push(ingressToUpdate);
} else {
// if the ingress is already in the list, then update the path with the new port
ingressesToUpdate[ingressUpdateIndex].Paths[ingressPathIndex].Port = newServicePort;
}
if (!servicePortsToUpdate.includes(newServicePort)) {
servicePortsToUpdate.push(newServicePort);
}
}
}
}
});
});
}
});
return [ingressesToUpdate, servicePortsToUpdate];
}

deployApplication() {
if (this.state.isEdit) {
return this.$async(this.confirmUpdateApplicationAsync);
} else {
return this.$async(this.deployApplicationAsync);
}
Expand Down Expand Up @@ -1154,6 +1254,8 @@ class KubernetesCreateApplicationController {

this.formValues.IsPublishingService = this.formValues.PublishedPorts.length > 0;

this.oldFormValues = angular.copy(this.formValues);

this.updateNamespaceLimits();
this.updateSliders();
} catch (err) {
Expand Down
Loading

0 comments on commit 315c1c7

Please sign in to comment.