From 6df11b738fd7ecfd5580f807ffd04eca59a02fb7 Mon Sep 17 00:00:00 2001 From: keyhan Date: Tue, 2 Jun 2026 16:25:43 +0330 Subject: [PATCH] Restore the preview host and its SSL when a custom domain is removed. Removing a custom domain left the Ingress with a stale custom-domain rule and no preview host, so the platform preview URL 404'd. updateIngress now resolves the app's stable preview number from its latest deployment and re-emits the preview host (with cert-manager TLS) whenever no verified custom domain is set, and removeCustomDomain re-applies the Ingress. Co-Authored-By: Claude Opus 4.7 --- backend/src/applications/domain.service.ts | 16 +++++++++ backend/src/kubernetes/kubernetes.module.ts | 3 +- backend/src/kubernetes/kubernetes.service.ts | 35 ++++++++++++++++++-- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/backend/src/applications/domain.service.ts b/backend/src/applications/domain.service.ts index 91c2154..902a7ef 100644 --- a/backend/src/applications/domain.service.ts +++ b/backend/src/applications/domain.service.ts @@ -162,6 +162,22 @@ export class DomainService { const saved = await this.appRepo.save(app); this.logger.log(`Custom domain removed for ${app.name}`); + + // Re-apply the Ingress so the platform preview host (e.g. + // -.3fase.ir) is restored and routed again — and cert-manager + // (re)issues its TLS cert. Without this the Ingress would keep the now-stale + // custom-domain rule and serve a 404 on the preview URL. Best-effort: a + // failure must not fail the remove call; the next deploy re-applies it too. + try { + await this.kubernetesService.updateIngress(saved); + this.logger.log(`Ingress restored to preview host for ${app.name}`); + } catch (ingressErr: any) { + this.logger.warn( + `Custom domain removed but ingress restore failed for ${app.name}: ${ingressErr.message}. ` + + `It will be retried on the next deployment.`, + ); + } + return saved; } diff --git a/backend/src/kubernetes/kubernetes.module.ts b/backend/src/kubernetes/kubernetes.module.ts index 8f04e60..99bcd91 100644 --- a/backend/src/kubernetes/kubernetes.module.ts +++ b/backend/src/kubernetes/kubernetes.module.ts @@ -8,9 +8,10 @@ import { ElasticsearchController } from './elasticsearch.controller'; import { LogsController } from './logs.controller'; import { ClustersModule } from '../clusters/clusters.module'; import { Application } from '../applications/entities/application.entity'; +import { Deployment } from '../deployments/entities/deployment.entity'; @Module({ - imports: [forwardRef(() => ClustersModule), TypeOrmModule.forFeature([Application])], + imports: [forwardRef(() => ClustersModule), TypeOrmModule.forFeature([Application, Deployment])], controllers: [ElasticsearchController, LogsController], providers: [KubernetesService, HelmService, RegistryService, ElasticsearchService], exports: [KubernetesService, HelmService, RegistryService, ElasticsearchService], diff --git a/backend/src/kubernetes/kubernetes.service.ts b/backend/src/kubernetes/kubernetes.service.ts index 697ecd5..4a37488 100644 --- a/backend/src/kubernetes/kubernetes.service.ts +++ b/backend/src/kubernetes/kubernetes.service.ts @@ -1,5 +1,7 @@ import { Injectable, Logger, OnModuleInit, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; import * as k8s from '@kubernetes/client-node'; import * as fs from 'fs'; import * as path from 'path'; @@ -8,6 +10,7 @@ import { promisify } from 'util'; import { PassThrough } from 'stream'; import { ClustersService } from '../clusters/clusters.service'; import { Application } from '../applications/entities/application.entity'; +import { Deployment } from '../deployments/entities/deployment.entity'; import { ensureAppUrlEnv } from '../applications/app-url.util'; import { AppRuntime, @@ -74,8 +77,27 @@ export class KubernetesService implements OnModuleInit { private clustersService: ClustersService, private helmService: HelmService, private registryService: RegistryService, + @InjectRepository(Deployment) + private deploymentsRepository: Repository, ) {} + /** + * Stable per-app preview number, reused from the latest deployment that has + * one. Used to restore the preview host on the Ingress (e.g. after a custom + * domain is removed) without triggering a fresh deploy. + */ + private async resolvePreviewNumber(applicationId: string): Promise { + const existing = await this.deploymentsRepository + .createQueryBuilder('d') + .select('d.previewSubdomain', 'previewSubdomain') + .where('d.applicationId = :applicationId', { applicationId }) + .andWhere('d.previewSubdomain IS NOT NULL') + .orderBy('d.createdAt', 'DESC') + .limit(1) + .getRawOne<{ previewSubdomain: string }>(); + return existing?.previewSubdomain || null; + } + onModuleInit() { // Helm chart is used for deployments — no local template loading needed } @@ -456,13 +478,20 @@ export class KubernetesService implements OnModuleInit { const customDomain = (app.customDomain && app.customDomainStatus === CustomDomainStatus.VERIFIED) ? app.customDomain : undefined; + // When there's no verified custom domain, restore the stable preview host so + // the app stays reachable (and keeps its TLS) — e.g. after a custom domain is + // removed. buildHelmValues only emits a preview host when there's no + // app.customDomain at all, so we only resolve it when none is set. + const previewNumber = app.customDomain + ? null + : await this.resolvePreviewNumber(app.id); + try { const kubeconfig = await this.getKubeconfig(app.clusterId); const imageUri = app.latestImageTag ? this.registryService.normalizeImageReference(app.latestImageTag) : ''; - const values = this.buildHelmValues(app, imageUri); - // Preview host is managed per-deployment; Helm chart only needs the main ingress host here. + const values = this.buildHelmValues(app, imageUri, previewNumber); await this.helmService.installOrUpgrade(app.name, namespace, values, kubeconfig); this.logger.log(`Updated ingress for ${app.name} via Helm (customDomain: ${customDomain || 'none'})`); } catch (helmError: any) { @@ -498,7 +527,7 @@ export class KubernetesService implements OnModuleInit { ownerId: app.userId, applicationId: app.id, }; - await this.applyIngress(networkingApi, ctx, customDomain); + await this.applyIngress(networkingApi, ctx, customDomain, previewNumber); this.logger.log(`Updated ingress for ${app.name} via K8s API (customDomain: ${customDomain || 'none'})`); } }