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:
@@ -162,6 +162,22 @@ export class DomainService {
|
|||||||
|
|
||||||
const saved = await this.appRepo.save(app);
|
const saved = await this.appRepo.save(app);
|
||||||
this.logger.log(`Custom domain removed for ${app.name}`);
|
this.logger.log(`Custom domain removed for ${app.name}`);
|
||||||
|
|
||||||
|
// Re-apply the Ingress so the platform preview host (e.g.
|
||||||
|
// <prefix>-<num>.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;
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import { ElasticsearchController } from './elasticsearch.controller';
|
|||||||
import { LogsController } from './logs.controller';
|
import { LogsController } from './logs.controller';
|
||||||
import { ClustersModule } from '../clusters/clusters.module';
|
import { ClustersModule } from '../clusters/clusters.module';
|
||||||
import { Application } from '../applications/entities/application.entity';
|
import { Application } from '../applications/entities/application.entity';
|
||||||
|
import { Deployment } from '../deployments/entities/deployment.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [forwardRef(() => ClustersModule), TypeOrmModule.forFeature([Application])],
|
imports: [forwardRef(() => ClustersModule), TypeOrmModule.forFeature([Application, Deployment])],
|
||||||
controllers: [ElasticsearchController, LogsController],
|
controllers: [ElasticsearchController, LogsController],
|
||||||
providers: [KubernetesService, HelmService, RegistryService, ElasticsearchService],
|
providers: [KubernetesService, HelmService, RegistryService, ElasticsearchService],
|
||||||
exports: [KubernetesService, HelmService, RegistryService, ElasticsearchService],
|
exports: [KubernetesService, HelmService, RegistryService, ElasticsearchService],
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Injectable, Logger, OnModuleInit, BadRequestException } from '@nestjs/common';
|
import { Injectable, Logger, OnModuleInit, BadRequestException } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
import * as k8s from '@kubernetes/client-node';
|
import * as k8s from '@kubernetes/client-node';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@@ -8,6 +10,7 @@ import { promisify } from 'util';
|
|||||||
import { PassThrough } from 'stream';
|
import { PassThrough } from 'stream';
|
||||||
import { ClustersService } from '../clusters/clusters.service';
|
import { ClustersService } from '../clusters/clusters.service';
|
||||||
import { Application } from '../applications/entities/application.entity';
|
import { Application } from '../applications/entities/application.entity';
|
||||||
|
import { Deployment } from '../deployments/entities/deployment.entity';
|
||||||
import { ensureAppUrlEnv } from '../applications/app-url.util';
|
import { ensureAppUrlEnv } from '../applications/app-url.util';
|
||||||
import {
|
import {
|
||||||
AppRuntime,
|
AppRuntime,
|
||||||
@@ -74,8 +77,27 @@ export class KubernetesService implements OnModuleInit {
|
|||||||
private clustersService: ClustersService,
|
private clustersService: ClustersService,
|
||||||
private helmService: HelmService,
|
private helmService: HelmService,
|
||||||
private registryService: RegistryService,
|
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() {
|
onModuleInit() {
|
||||||
// Helm chart is used for deployments — no local template loading needed
|
// 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)
|
const customDomain = (app.customDomain && app.customDomainStatus === CustomDomainStatus.VERIFIED)
|
||||||
? app.customDomain : undefined;
|
? 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 {
|
try {
|
||||||
const kubeconfig = await this.getKubeconfig(app.clusterId);
|
const kubeconfig = await this.getKubeconfig(app.clusterId);
|
||||||
const imageUri = app.latestImageTag
|
const imageUri = app.latestImageTag
|
||||||
? this.registryService.normalizeImageReference(app.latestImageTag)
|
? this.registryService.normalizeImageReference(app.latestImageTag)
|
||||||
: '';
|
: '';
|
||||||
const values = this.buildHelmValues(app, imageUri);
|
const values = this.buildHelmValues(app, imageUri, previewNumber);
|
||||||
// Preview host is managed per-deployment; Helm chart only needs the main ingress host here.
|
|
||||||
await this.helmService.installOrUpgrade(app.name, namespace, values, kubeconfig);
|
await this.helmService.installOrUpgrade(app.name, namespace, values, kubeconfig);
|
||||||
this.logger.log(`Updated ingress for ${app.name} via Helm (customDomain: ${customDomain || 'none'})`);
|
this.logger.log(`Updated ingress for ${app.name} via Helm (customDomain: ${customDomain || 'none'})`);
|
||||||
} catch (helmError: any) {
|
} catch (helmError: any) {
|
||||||
@@ -498,7 +527,7 @@ export class KubernetesService implements OnModuleInit {
|
|||||||
ownerId: app.userId,
|
ownerId: app.userId,
|
||||||
applicationId: app.id,
|
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'})`);
|
this.logger.log(`Updated ingress for ${app.name} via K8s API (customDomain: ${customDomain || 'none'})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user