diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js
index e674de8de..39e9f1282 100644
--- a/app/kubernetes/helpers/application/index.js
+++ b/app/kubernetes/helpers/application/index.js
@@ -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;
}
});
diff --git a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/CreateIngressView.tsx b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/CreateIngressView.tsx
index 1ec1a5932..ec93dfff1 100644
--- a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/CreateIngressView.tsx
+++ b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/CreateIngressView.tsx
@@ -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(
() =>
@@ -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(
diff --git a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/IngressForm.tsx b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/IngressForm.tsx
index f14abe94c..7e22d7c61 100644
--- a/app/kubernetes/react/views/networks/ingresses/CreateIngressView/IngressForm.tsx
+++ b/app/kubernetes/react/views/networks/ingresses/CreateIngressView/IngressForm.tsx
@@ -290,7 +290,8 @@ export function IngressForm({
)}
removeIngressHost(hostIndex)}
@@ -534,7 +535,8 @@ export function IngressForm({
removeIngressRoute(hostIndex, pathIndex)}
diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js
index 8a818289a..bd95ca5b3 100644
--- a/app/kubernetes/views/applications/create/createApplicationController.js
+++ b/app/kubernetes/views/applications/create/createApplicationController.js
@@ -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 */
@@ -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 */
@@ -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);
@@ -1028,13 +1041,100 @@ class KubernetesCreateApplicationController {
}
}
- deployApplication() {
- if (this.state.isEdit) {
- this.ModalService.confirmUpdate('Updating the application may cause a service interruption. Do you wish to continue?', (confirmed) => {
- if (confirmed) {
- return this.$async(this.updateApplicationAsync);
+ 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 = `
+
+ Updating the application may cause a service interruption.
+ ${noMatchSentence}
+
+ `;
+ 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, [], '');
+ }
+ });
+ }
+ }
+
+ // 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);
}
@@ -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) {
diff --git a/app/portainer/services/modal.service/prompt.ts b/app/portainer/services/modal.service/prompt.ts
index dcfc27e34..ebea5800b 100644
--- a/app/portainer/services/modal.service/prompt.ts
+++ b/app/portainer/services/modal.service/prompt.ts
@@ -18,6 +18,7 @@ interface InputOption {
interface PromptOptions {
title: string;
+ message?: string;
inputType?:
| 'text'
| 'textarea'
@@ -45,9 +46,12 @@ export async function promptAsync(options: Omit) {
});
}
+// the ts-ignore is required because the bootbox typings are not up to date
+// remove the ts-ignore when the typings are updated in
export function prompt(options: PromptOptions) {
const box = bootbox.prompt({
title: options.title,
+ message: options.message || '',
inputType: options.inputType,
inputOptions: options.inputOptions,
buttons: options.buttons ? confirmButtons(options.buttons) : undefined,
@@ -84,6 +88,32 @@ export function confirmContainerDeletion(
});
}
+export function confirmUpdateAppIngress(
+ title: string,
+ message: string,
+ inputText: string,
+ callback: PromptCallback
+) {
+ prompt({
+ title: buildTitle(title),
+ inputType: 'checkbox',
+ message,
+ inputOptions: [
+ {
+ text: `${inputText} `,
+ value: '1',
+ },
+ ],
+ buttons: {
+ confirm: {
+ label: 'Update',
+ className: 'btn-primary',
+ },
+ },
+ callback,
+ });
+}
+
export function selectRegistry(options: PromptOptions) {
prompt(options);
}
diff --git a/package.json b/package.json
index 3847ad467..113234da6 100644
--- a/package.json
+++ b/package.json
@@ -165,7 +165,7 @@
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"@types/angular": "^1.8.3",
- "@types/bootbox": "^5.2.2",
+ "@types/bootbox": "^5.2.4",
"@types/file-saver": "^2.0.4",
"@types/jest": "^27.0.3",
"@types/jquery": "^3.5.10",
diff --git a/yarn.lock b/yarn.lock
index 319a2bcfa..3854e115f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4455,10 +4455,10 @@
dependencies:
"@types/node" "*"
-"@types/bootbox@^5.2.2":
- version "5.2.3"
- resolved "https://registry.yarnpkg.com/@types/bootbox/-/bootbox-5.2.3.tgz#86aa918eb4df2499631887bb7b6b23f0195a751d"
- integrity sha512-6O9474usap0SRkRhPYhmtrAWPfQ2Kwb5WsSxVkM8uT5FwRp/TQijSrhg344r+zJb4K38b96DlXaqs/BrW4Banw==
+"@types/bootbox@^5.2.4":
+ version "5.2.4"
+ resolved "https://registry.yarnpkg.com/@types/bootbox/-/bootbox-5.2.4.tgz#b86363715f7cd2b60edcc70217ad67c919a1942a"
+ integrity sha512-YYywaPrgRtLgui/dhZujO8ZLw4vFW7eRgRbL/6MO7RG6Hah08gZmeOQv7jKZaltWafixZEPmmFKMSw9qC2rlbw==
dependencies:
"@types/jquery" "*"