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 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-02 16:25:43 +03:30
parent 0b08b995f0
commit 6df11b738f
3 changed files with 50 additions and 4 deletions
+32 -3
View File
@@ -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<Deployment>,
) {}
/**
* 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<string | null> {
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'})`);
}
}