From d6394be79a1414817a56ef7227e43a3a29850e1f Mon Sep 17 00:00:00 2001 From: James Sprow Date: Sat, 11 Apr 2026 19:56:17 -0600 Subject: [PATCH 1/5] fix: resolve race condition causing false 'domain already taken' on app edit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In mounted(), getDomains() and loadPipelineAndApp() ran concurrently. loadApp() inside loadPipelineAndApp() was not awaited, so getDomains() resolved first with this.ingress.hosts still empty — the app's own domain was never whitelisted from takenDomains. - Make loadApp() async and return the promise - Await loadApp() inside loadPipelineAndApp() before getDomains() runs - Fix whiteListDomains() splice-while-iterating bug by using Set + filter Fixes #738 --- client/src/components/apps/form.vue | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/client/src/components/apps/form.vue b/client/src/components/apps/form.vue index d0d81e98..710ad0d9 100644 --- a/client/src/components/apps/form.vue +++ b/client/src/components/apps/form.vue @@ -1824,14 +1824,8 @@ export default defineComponent({ this.sslIndex.splice(index, 1); }, whiteListDomains(domainsList: string[]) { - for (let i = 0; i < domainsList.length; i++) { - this.ingress.hosts.forEach((host) => { - if (host.host == domainsList[i]) { - domainsList.splice(i, 1); - } - }); - } - return domainsList; + const ownHosts = new Set(this.ingress.hosts.map((h) => h.host)); + return domainsList.filter((d) => !ownHosts.has(d)); }, async getDomains() { return axios.get("/api/kubernetes/domains").then((response) => { @@ -2001,7 +1995,7 @@ export default defineComponent({ } if (this.app != "new") { - this.loadApp(); + await this.loadApp(); } }); }, @@ -2091,9 +2085,9 @@ export default defineComponent({ console.log(error); }); }, - loadApp() { + async loadApp() { if (this.app !== "new") { - axios + return axios .get(`/api/apps/${this.pipeline}/${this.phase}/${this.app}`) .then((response) => { this.resourceVersion = response.data.metadata.resourceVersion; @@ -2200,8 +2194,6 @@ export default defineComponent({ this.buildpack.run.readOnlyAppStorage = true; } - // remove loaded domain from taken domains - this.takenDomains = this.whiteListDomains(this.takenDomains); }); } }, From e14675ad70ae22bb870d5ff79990f0877874e323 Mon Sep 17 00:00:00 2001 From: James Sprow Date: Sat, 11 Apr 2026 20:01:47 -0600 Subject: [PATCH 2/5] fix: null-safe guard pipelineData.git.repository and securityContext in save MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For docker-strategy pipelines, pipelineData.git.repository is undefined. updateApp() and createApp() both accessed .ssh_url on it unconditionally, throwing a TypeError that was silently swallowed by the catch block — causing Save to appear to work but make no network request. Also guarded postdata.image.run.securityContext before typeof checks for apps where securityContext is absent from the spec. Refs #738 --- client/src/components/apps/form.vue | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/src/components/apps/form.vue b/client/src/components/apps/form.vue index 710ad0d9..a573df89 100644 --- a/client/src/components/apps/form.vue +++ b/client/src/components/apps/form.vue @@ -2289,11 +2289,11 @@ export default defineComponent({ async updateApp() { this.loading = true; try { - if (this.gitrepo.ssh_url == this.pipelineData.git.repository.ssh_url) { + if (this.pipelineData.git?.repository && this.gitrepo.ssh_url == this.pipelineData.git.repository.ssh_url) { this.gitrepo = this.pipelineData.git.repository; } - if (this.gitrepo.admin == false) { + if (this.gitrepo.admin == false && this.gitrepo.ssh_url) { //this.gitrepo.clone_url = this.gitrepo.ssh_url.replace(':', '/').replace('git@', 'https://'); // eslint-disable-next-line no-useless-escape const regex = @@ -2373,12 +2373,12 @@ export default defineComponent({ healthcheck: this.healthcheck, }; - if (typeof postdata.image.run.securityContext.runAsUser === "string") { + if (postdata.image.run?.securityContext && typeof postdata.image.run.securityContext.runAsUser === "string") { postdata.image.run.securityContext.runAsUser = parseInt( postdata.image.run.securityContext.runAsUser ); } - if (typeof postdata.image.run.securityContext.runAsGroup === "string") { + if (postdata.image.run?.securityContext && typeof postdata.image.run.securityContext.runAsGroup === "string") { postdata.image.run.securityContext.runAsGroup = parseInt( postdata.image.run.securityContext.runAsGroup ); @@ -2409,11 +2409,11 @@ export default defineComponent({ } } - if (this.gitrepo.ssh_url == this.pipelineData.git.repository.ssh_url) { + if (this.pipelineData.git?.repository && this.gitrepo.ssh_url == this.pipelineData.git.repository.ssh_url) { this.gitrepo = this.pipelineData.git.repository; } - if (this.gitrepo.admin == false) { + if (this.gitrepo.admin == false && this.gitrepo.ssh_url) { // eslint-disable-next-line no-useless-escape const regex = /(git@|ssh:|http[s]?:\/\/)([\w.]+)(:|\/)([\w/\-~]+)(\.git)?/; @@ -2491,12 +2491,12 @@ export default defineComponent({ postdata.image.run = {} as BuildpackStepConfig; } - if (typeof postdata.image.run.securityContext.runAsUser === "string") { + if (postdata.image.run?.securityContext && typeof postdata.image.run.securityContext.runAsUser === "string") { postdata.image.run.securityContext.runAsUser = parseInt( postdata.image.run.securityContext.runAsUser ); } - if (typeof postdata.image.run.securityContext.runAsGroup === "string") { + if (postdata.image.run?.securityContext && typeof postdata.image.run.securityContext.runAsGroup === "string") { postdata.image.run.securityContext.runAsGroup = parseInt( postdata.image.run.securityContext.runAsGroup ); From 42c3a7655695a70bde8edfc1c0ad08afa56c1620 Mon Sep 17 00:00:00 2001 From: James Sprow Date: Sat, 11 Apr 2026 20:09:33 -0600 Subject: [PATCH 3/5] fix: add async to loadPipelineAndApp then() callback (TS1308) --- client/src/components/apps/form.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/apps/form.vue b/client/src/components/apps/form.vue index a573df89..02f6153e 100644 --- a/client/src/components/apps/form.vue +++ b/client/src/components/apps/form.vue @@ -1920,7 +1920,7 @@ export default defineComponent({ this.ingress.hosts[0].host = name + "." + this.pipelineData.domain; }, async loadPipelineAndApp() { - return axios.get("/api/pipelines/" + this.pipeline).then((response) => { + return axios.get("/api/pipelines/" + this.pipeline).then(async (response) => { this.pipelineData = response.data; if (this.pipelineData.dockerimage) { From 5a241cbec9b3d862d092640ae3d60974e4608dcc Mon Sep 17 00:00:00 2001 From: James Sprow Date: Sat, 11 Apr 2026 20:15:48 -0600 Subject: [PATCH 4/5] fix: run getDomains() after loadPipelineAndApp() to fix domain-already-taken MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getDomains() was in the Promise.all alongside loadPipelineAndApp(), so it could resolve and call whiteListDomains() before loadApp() had populated this.ingress.hosts — meaning the app's own domain was never whitelisted. Moving getDomains() to run sequentially after the parallel block ensures this.ingress.hosts is always populated when whiteListDomains() runs. Refs #738 --- client/src/components/apps/form.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/components/apps/form.vue b/client/src/components/apps/form.vue index 02f6153e..798ec26e 100644 --- a/client/src/components/apps/form.vue +++ b/client/src/components/apps/form.vue @@ -1793,8 +1793,10 @@ export default defineComponent({ this.loadPodsizeList(), this.loadBuildpacks(), this.loadClusterIssuers(), - this.getDomains(), ]); + // getDomains must run after loadPipelineAndApp (which awaits loadApp) so + // that this.ingress.hosts is populated before whiteListDomains() is called. + await this.getDomains(); if (this.$route.query.template) { const template = this.$route.query.template as string; From aaa4d808cc263252f111fa95a33c6725e6057b27 Mon Sep 17 00:00:00 2001 From: James Sprow Date: Sat, 11 Apr 2026 20:21:12 -0600 Subject: [PATCH 5/5] fix: guard ingress.tls undefined in setSSL() and sslIndex loop Apps without TLS have ingress.tls=undefined after loadApp() overwrites the initial empty array. setSSL() checked tls?.length==0 which doesn't catch undefined, so tls[0] crashed. Also guarded the sslIndex population loop in loadApp() with optional chaining. Refs #738 --- client/src/components/apps/form.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/apps/form.vue b/client/src/components/apps/form.vue index 798ec26e..9d5cb346 100644 --- a/client/src/components/apps/form.vue +++ b/client/src/components/apps/form.vue @@ -2183,7 +2183,7 @@ export default defineComponent({ // iterate over ingress hosts and fill sslIndex for (let i = 0; i < this.ingress.hosts.length; i++) { this.sslIndex.push( - this.ingress.tls[0].hosts.includes(this.ingress.hosts[i].host) + this.ingress.tls?.[0]?.hosts.includes(this.ingress.hosts[i].host) ?? false ); } @@ -2200,7 +2200,7 @@ export default defineComponent({ } }, setSSL() { - if (this.ingress.tls?.length == 0) { + if (!this.ingress.tls || this.ingress.tls.length == 0) { this.ingress.tls = [{ hosts: [], secretName: this.name + "-tls" }]; } this.ingress.tls[0].hosts = [];