feat: add custom domain support with SSL, DNS verification, and billing
Users can assign a custom domain to their app with automatic SSL via cert-manager. Includes DNS verification flow (CNAME check), Persian instructions, admin-configurable pricing via PlatformSetting, and integration into the deploy wizard cost calculation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import { promisify } from 'util';
|
||||
import { PassThrough } from 'stream';
|
||||
import { ClustersService } from '../clusters/clusters.service';
|
||||
import { Application } from '../applications/entities/application.entity';
|
||||
import { AppRuntime, DatabaseType } from '../common/enums';
|
||||
import { AppRuntime, DatabaseType, CustomDomainStatus } from '../common/enums';
|
||||
import { HelmService } from './helm.service';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -118,6 +118,9 @@ export class KubernetesService implements OnModuleInit {
|
||||
subdomain: app.subdomain || app.name,
|
||||
domain: domain,
|
||||
clusterIssuer: 'letsencrypt-prod',
|
||||
customDomain: (app.customDomain && app.customDomainStatus === CustomDomainStatus.VERIFIED)
|
||||
? app.customDomain
|
||||
: '',
|
||||
},
|
||||
registry: {
|
||||
url: pullRegistryUrl,
|
||||
@@ -182,6 +185,57 @@ export class KubernetesService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
async updateIngress(app: Application): Promise<void> {
|
||||
const domain = this.configService.get('platform.domain');
|
||||
const subdomain = app.subdomain || app.name;
|
||||
const namespace = `user-${app.userId.split('-')[0]}`;
|
||||
const customDomain = (app.customDomain && app.customDomainStatus === CustomDomainStatus.VERIFIED)
|
||||
? app.customDomain : undefined;
|
||||
|
||||
try {
|
||||
const kubeconfig = await this.getKubeconfig(app.clusterId);
|
||||
const imageUri = app.latestImageTag
|
||||
? `${this.configService.get<string>('registry.pullUrl') || 'localhost:30500'}/${app.name}:${app.latestImageTag}`
|
||||
: '';
|
||||
const values = this.buildHelmValues(app, imageUri);
|
||||
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) {
|
||||
this.logger.warn(`Helm ingress update failed for ${app.name}, using direct K8s API: ${helmError.message}`);
|
||||
const { networkingApi } = await this.getK8sClient(app.clusterId);
|
||||
const ctx: ManifestContext = {
|
||||
appName: app.name,
|
||||
namespace,
|
||||
image: '',
|
||||
port: app.port,
|
||||
replicas: app.replicas,
|
||||
cpuRequest: app.cpuRequest,
|
||||
cpuLimit: app.cpuLimit,
|
||||
memoryRequest: app.memoryRequest,
|
||||
memoryLimit: app.memoryLimit,
|
||||
envVars: app.envVars || {},
|
||||
runtime: app.runtime,
|
||||
databaseType: app.databaseType,
|
||||
domain,
|
||||
subdomain,
|
||||
dbUsername: app.dbUsername || '',
|
||||
dbPassword: app.dbPassword || '',
|
||||
dbVersion: app.dbVersion || '',
|
||||
dbStorageSize: app.dbStorageSize || '1Gi',
|
||||
appStorageSize: app.appStorageSize || '2Gi',
|
||||
enableRedis: app.enableRedis || false,
|
||||
redisVersion: app.redisVersion || '7.2',
|
||||
enableRabbitmq: app.enableRabbitmq || false,
|
||||
rabbitmqVersion: app.rabbitmqVersion || '3.13',
|
||||
enableElasticsearch: app.enableElasticsearch || false,
|
||||
elasticsearchVersion: app.elasticsearchVersion || '8.12',
|
||||
logPaths: app.logPaths || [],
|
||||
};
|
||||
await this.applyIngress(networkingApi, ctx, customDomain);
|
||||
this.logger.log(`Updated ingress for ${app.name} via K8s API (customDomain: ${customDomain || 'none'})`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helm-based deployment ─────────────────────────────────────────
|
||||
|
||||
private async deployViaHelm(app: Application, imageUri: string): Promise<Record<string, any>> {
|
||||
@@ -278,7 +332,9 @@ export class KubernetesService implements OnModuleInit {
|
||||
manifests.service = await this.applyService(coreApi, context);
|
||||
|
||||
// 6. Create Ingress
|
||||
manifests.ingress = await this.applyIngress(networkingApi, context);
|
||||
const customDomain = (app.customDomain && app.customDomainStatus === CustomDomainStatus.VERIFIED)
|
||||
? app.customDomain : undefined;
|
||||
manifests.ingress = await this.applyIngress(networkingApi, context, customDomain);
|
||||
|
||||
this.logger.log(`Successfully deployed ${app.name} to namespace ${context.namespace} via K8s API`);
|
||||
} catch (error: any) {
|
||||
@@ -718,8 +774,40 @@ export class KubernetesService implements OnModuleInit {
|
||||
return service;
|
||||
}
|
||||
|
||||
private async applyIngress(networkingApi: k8s.NetworkingV1Api, ctx: ManifestContext): Promise<any> {
|
||||
private async applyIngress(networkingApi: k8s.NetworkingV1Api, ctx: ManifestContext, customDomain?: string): Promise<any> {
|
||||
const host = `${ctx.subdomain}.${ctx.domain}`;
|
||||
const rules: k8s.V1IngressRule[] = [
|
||||
{
|
||||
host,
|
||||
http: {
|
||||
paths: [
|
||||
{
|
||||
path: '/',
|
||||
pathType: 'Prefix',
|
||||
backend: { service: { name: ctx.appName, port: { number: 80 } } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
const tlsHosts = [host];
|
||||
|
||||
if (customDomain) {
|
||||
rules.push({
|
||||
host: customDomain,
|
||||
http: {
|
||||
paths: [
|
||||
{
|
||||
path: '/',
|
||||
pathType: 'Prefix',
|
||||
backend: { service: { name: ctx.appName, port: { number: 80 } } },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
tlsHosts.push(customDomain);
|
||||
}
|
||||
|
||||
const ingress: k8s.V1Ingress = {
|
||||
apiVersion: 'networking.k8s.io/v1',
|
||||
kind: 'Ingress',
|
||||
@@ -732,21 +820,8 @@ export class KubernetesService implements OnModuleInit {
|
||||
},
|
||||
spec: {
|
||||
ingressClassName: 'nginx',
|
||||
rules: [
|
||||
{
|
||||
host,
|
||||
http: {
|
||||
paths: [
|
||||
{
|
||||
path: '/',
|
||||
pathType: 'Prefix',
|
||||
backend: { service: { name: ctx.appName, port: { number: 80 } } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
tls: [{ hosts: [host], secretName: `${ctx.appName}-tls` }],
|
||||
rules,
|
||||
tls: [{ hosts: tlsHosts, secretName: `${ctx.appName}-tls` }],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user