fix(k8s): restore direct K8s API deploy as fallback when Helm unavailable
- deployApplication now tries Helm first, falls back to direct K8s API - Restored all private methods: ensureNamespace, applySecret, applyDeployment, applyService, applyIngress, applyWordPressPvc, deployDatabase, createDbSecret, createPVC - Added ManifestContext interface - Added DATABASE_URL env var, PGDATA, health probes for DB, ingressClassName
This commit is contained in:
@@ -12,6 +12,27 @@ import { HelmService } from './helm.service';
|
|||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
interface ManifestContext {
|
||||||
|
appName: string;
|
||||||
|
namespace: string;
|
||||||
|
image: string;
|
||||||
|
port: number;
|
||||||
|
replicas: number;
|
||||||
|
cpuRequest: string;
|
||||||
|
cpuLimit: string;
|
||||||
|
memoryRequest: string;
|
||||||
|
memoryLimit: string;
|
||||||
|
envVars: Record<string, string>;
|
||||||
|
runtime: string;
|
||||||
|
databaseType: string;
|
||||||
|
domain: string;
|
||||||
|
subdomain: string;
|
||||||
|
dbUsername: string;
|
||||||
|
dbPassword: string;
|
||||||
|
dbVersion: string;
|
||||||
|
dbStorageSize: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class KubernetesService implements OnModuleInit {
|
export class KubernetesService implements OnModuleInit {
|
||||||
private readonly logger = new Logger(KubernetesService.name);
|
private readonly logger = new Logger(KubernetesService.name);
|
||||||
@@ -117,28 +138,480 @@ export class KubernetesService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deployApplication(app: Application, imageUri: string): Promise<Record<string, any>> {
|
async deployApplication(app: Application, imageUri: string): Promise<Record<string, any>> {
|
||||||
|
// Try Helm first, fall back to direct K8s API if Helm is unavailable
|
||||||
|
try {
|
||||||
|
return await this.deployViaHelm(app, imageUri);
|
||||||
|
} catch (helmError: any) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Helm deploy failed for ${app.name}, falling back to direct K8s API: ${helmError.message}`,
|
||||||
|
);
|
||||||
|
return await this.deployViaK8sApi(app, imageUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helm-based deployment ─────────────────────────────────────────
|
||||||
|
|
||||||
|
private async deployViaHelm(app: Application, imageUri: string): Promise<Record<string, any>> {
|
||||||
const kubeconfig = await this.getKubeconfig(app.clusterId);
|
const kubeconfig = await this.getKubeconfig(app.clusterId);
|
||||||
const values = this.buildHelmValues(app, imageUri);
|
const values = this.buildHelmValues(app, imageUri);
|
||||||
const namespace = values.app.namespace;
|
const namespace = values.app.namespace;
|
||||||
const releaseName = app.name;
|
const releaseName = app.name;
|
||||||
|
|
||||||
try {
|
const result = await this.helmService.installOrUpgrade(
|
||||||
const result = await this.helmService.installOrUpgrade(
|
releaseName,
|
||||||
releaseName,
|
namespace,
|
||||||
namespace,
|
values,
|
||||||
values,
|
kubeconfig,
|
||||||
kubeconfig,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.log(`Successfully deployed ${app.name} to namespace ${namespace} via Helm`);
|
this.logger.log(`Successfully deployed ${app.name} to namespace ${namespace} via Helm`);
|
||||||
return { helm: { release: releaseName, namespace, stdout: result.stdout }, values };
|
return { helm: { release: releaseName, namespace, stdout: result.stdout }, values };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Direct K8s API deployment (fallback) ──────────────────────────
|
||||||
|
|
||||||
|
private async deployViaK8sApi(app: Application, imageUri: string): Promise<Record<string, any>> {
|
||||||
|
const { coreApi, appsApi, networkingApi } = await this.getK8sClient(app.clusterId);
|
||||||
|
const domain = this.configService.get('platform.domain');
|
||||||
|
|
||||||
|
const context: ManifestContext = {
|
||||||
|
appName: app.name,
|
||||||
|
namespace: `user-${app.userId.split('-')[0]}`,
|
||||||
|
image: imageUri,
|
||||||
|
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: domain,
|
||||||
|
subdomain: app.subdomain || app.name,
|
||||||
|
dbUsername: app.dbUsername || 'appuser',
|
||||||
|
dbPassword: app.dbPassword || this.generatePassword(),
|
||||||
|
dbVersion: app.dbVersion || '',
|
||||||
|
dbStorageSize: app.dbStorageSize || '1Gi',
|
||||||
|
};
|
||||||
|
|
||||||
|
const manifests: Record<string, any> = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Ensure namespace exists
|
||||||
|
await this.ensureNamespace(coreApi, context.namespace);
|
||||||
|
|
||||||
|
// 2. Create/Update secrets for env vars
|
||||||
|
if (Object.keys(context.envVars).length > 0) {
|
||||||
|
manifests.secret = await this.applySecret(coreApi, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Deploy database if needed
|
||||||
|
if (context.databaseType !== DatabaseType.NONE) {
|
||||||
|
manifests.database = await this.deployDatabase(coreApi, appsApi, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.5 Create wp-content PVC for WordPress
|
||||||
|
if (context.runtime === AppRuntime.WORDPRESS) {
|
||||||
|
manifests.wpContentPvc = await this.applyWordPressPvc(coreApi, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Create Deployment
|
||||||
|
manifests.deployment = await this.applyDeployment(appsApi, context);
|
||||||
|
|
||||||
|
// 5. Create Service
|
||||||
|
manifests.service = await this.applyService(coreApi, context);
|
||||||
|
|
||||||
|
// 6. Create Ingress
|
||||||
|
manifests.ingress = await this.applyIngress(networkingApi, context);
|
||||||
|
|
||||||
|
this.logger.log(`Successfully deployed ${app.name} to namespace ${context.namespace} via K8s API`);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(`Failed to deploy ${app.name} via Helm:`, error.message);
|
this.logger.error(`Failed to deploy ${app.name} via K8s API:`, error.body || error.message);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return manifests;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Private K8s resource methods ──────────────────────────────────
|
||||||
|
|
||||||
|
private async ensureNamespace(coreApi: k8s.CoreV1Api, namespace: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await coreApi.readNamespace(namespace);
|
||||||
|
} catch {
|
||||||
|
await coreApi.createNamespace({
|
||||||
|
metadata: { name: namespace },
|
||||||
|
});
|
||||||
|
this.logger.log(`Created namespace: ${namespace}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Legacy private deploy methods removed — now handled by Helm chart ──
|
private async applySecret(coreApi: k8s.CoreV1Api, ctx: ManifestContext): Promise<any> {
|
||||||
|
const secretData: Record<string, string> = {};
|
||||||
|
for (const [key, value] of Object.entries(ctx.envVars)) {
|
||||||
|
secretData[key] = Buffer.from(String(value)).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = {
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'Secret',
|
||||||
|
metadata: {
|
||||||
|
name: `${ctx.appName}-env`,
|
||||||
|
namespace: ctx.namespace,
|
||||||
|
},
|
||||||
|
data: secretData,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await coreApi.replaceNamespacedSecret(`${ctx.appName}-env`, ctx.namespace, secret);
|
||||||
|
} catch {
|
||||||
|
await coreApi.createNamespacedSecret(ctx.namespace, secret);
|
||||||
|
}
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async applyDeployment(appsApi: k8s.AppsV1Api, ctx: ManifestContext): Promise<any> {
|
||||||
|
const envFrom: any[] = [];
|
||||||
|
if (Object.keys(ctx.envVars).length > 0) {
|
||||||
|
envFrom.push({ secretRef: { name: `${ctx.appName}-env` } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database connection env vars
|
||||||
|
const extraEnv: any[] = [];
|
||||||
|
if (ctx.databaseType === DatabaseType.POSTGRESQL) {
|
||||||
|
extraEnv.push(
|
||||||
|
{ name: 'DB_HOST', value: `${ctx.appName}-db` },
|
||||||
|
{ name: 'DB_PORT', value: '5432' },
|
||||||
|
{ name: 'DB_NAME', value: ctx.appName.replace(/-/g, '_') },
|
||||||
|
{ name: 'DB_USER', valueFrom: { secretKeyRef: { name: `${ctx.appName}-db-secret`, key: 'username' } } },
|
||||||
|
{ name: 'DB_PASSWORD', valueFrom: { secretKeyRef: { name: `${ctx.appName}-db-secret`, key: 'password' } } },
|
||||||
|
{ name: 'DATABASE_URL', value: `postgresql://$(DB_USER):$(DB_PASSWORD)@${ctx.appName}-db:5432/${ctx.appName.replace(/-/g, '_')}` },
|
||||||
|
);
|
||||||
|
} else if (ctx.databaseType === DatabaseType.MYSQL) {
|
||||||
|
extraEnv.push(
|
||||||
|
{ name: 'DB_HOST', value: `${ctx.appName}-db` },
|
||||||
|
{ name: 'DB_PORT', value: '3306' },
|
||||||
|
{ name: 'DB_NAME', value: ctx.appName.replace(/-/g, '_') },
|
||||||
|
{ name: 'DB_USER', valueFrom: { secretKeyRef: { name: `${ctx.appName}-db-secret`, key: 'username' } } },
|
||||||
|
{ name: 'DB_PASSWORD', valueFrom: { secretKeyRef: { name: `${ctx.appName}-db-secret`, key: 'password' } } },
|
||||||
|
{ name: 'DATABASE_URL', value: `mysql://$(DB_USER):$(DB_PASSWORD)@${ctx.appName}-db:3306/${ctx.appName.replace(/-/g, '_')}` },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WordPress-specific env vars
|
||||||
|
if (ctx.runtime === AppRuntime.WORDPRESS && ctx.databaseType !== DatabaseType.NONE) {
|
||||||
|
const dbPort = ctx.databaseType === DatabaseType.POSTGRESQL ? '5432' : '3306';
|
||||||
|
extraEnv.push(
|
||||||
|
{ name: 'WORDPRESS_DB_HOST', value: `${ctx.appName}-db:${dbPort}` },
|
||||||
|
{ name: 'WORDPRESS_DB_NAME', value: ctx.appName.replace(/-/g, '_') },
|
||||||
|
{ name: 'WORDPRESS_DB_USER', valueFrom: { secretKeyRef: { name: `${ctx.appName}-db-secret`, key: 'username' } } },
|
||||||
|
{ name: 'WORDPRESS_DB_PASSWORD', valueFrom: { secretKeyRef: { name: `${ctx.appName}-db-secret`, key: 'password' } } },
|
||||||
|
{ name: 'WORDPRESS_TABLE_PREFIX', value: 'wp_' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deployment: k8s.V1Deployment = {
|
||||||
|
apiVersion: 'apps/v1',
|
||||||
|
kind: 'Deployment',
|
||||||
|
metadata: {
|
||||||
|
name: ctx.appName,
|
||||||
|
namespace: ctx.namespace,
|
||||||
|
labels: { app: ctx.appName, runtime: ctx.runtime },
|
||||||
|
annotations: {
|
||||||
|
'kubernetes.io/change-cause': `Deploy ${ctx.image} at ${new Date().toISOString()}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
revisionHistoryLimit: 10,
|
||||||
|
replicas: ctx.replicas,
|
||||||
|
selector: { matchLabels: { app: ctx.appName } },
|
||||||
|
template: {
|
||||||
|
metadata: { labels: { app: ctx.appName, runtime: ctx.runtime } },
|
||||||
|
spec: {
|
||||||
|
containers: [
|
||||||
|
{
|
||||||
|
name: ctx.appName,
|
||||||
|
image: ctx.image,
|
||||||
|
imagePullPolicy: 'Always',
|
||||||
|
ports: [{ containerPort: ctx.port }],
|
||||||
|
envFrom,
|
||||||
|
env: extraEnv,
|
||||||
|
resources: {
|
||||||
|
requests: { cpu: ctx.cpuRequest, memory: ctx.memoryRequest },
|
||||||
|
limits: { cpu: ctx.cpuLimit, memory: ctx.memoryLimit },
|
||||||
|
},
|
||||||
|
readinessProbe: {
|
||||||
|
tcpSocket: { port: ctx.port as any },
|
||||||
|
initialDelaySeconds: 5,
|
||||||
|
periodSeconds: 5,
|
||||||
|
},
|
||||||
|
livenessProbe: {
|
||||||
|
tcpSocket: { port: ctx.port as any },
|
||||||
|
initialDelaySeconds: 15,
|
||||||
|
periodSeconds: 10,
|
||||||
|
failureThreshold: 5,
|
||||||
|
},
|
||||||
|
...(ctx.runtime === AppRuntime.WORDPRESS
|
||||||
|
? {
|
||||||
|
volumeMounts: [
|
||||||
|
{ name: 'wp-content', mountPath: '/var/www/html/wp-content' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
...(ctx.runtime === AppRuntime.WORDPRESS
|
||||||
|
? {
|
||||||
|
volumes: [
|
||||||
|
{
|
||||||
|
name: 'wp-content',
|
||||||
|
persistentVolumeClaim: { claimName: `${ctx.appName}-wp-content` },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await appsApi.replaceNamespacedDeployment(ctx.appName, ctx.namespace, deployment);
|
||||||
|
} catch {
|
||||||
|
await appsApi.createNamespacedDeployment(ctx.namespace, deployment);
|
||||||
|
}
|
||||||
|
return deployment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async applyWordPressPvc(coreApi: k8s.CoreV1Api, ctx: ManifestContext): Promise<any> {
|
||||||
|
const pvcName = `${ctx.appName}-wp-content`;
|
||||||
|
const pvc = {
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'PersistentVolumeClaim',
|
||||||
|
metadata: { name: pvcName, namespace: ctx.namespace, labels: { app: ctx.appName } },
|
||||||
|
spec: {
|
||||||
|
accessModes: ['ReadWriteOnce'],
|
||||||
|
resources: { requests: { storage: '2Gi' } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await coreApi.readNamespacedPersistentVolumeClaim(pvcName, ctx.namespace);
|
||||||
|
this.logger.log(`PVC ${pvcName} already exists, skipping`);
|
||||||
|
} catch {
|
||||||
|
await coreApi.createNamespacedPersistentVolumeClaim(ctx.namespace, pvc);
|
||||||
|
this.logger.log(`Created WordPress PVC: ${pvcName}`);
|
||||||
|
}
|
||||||
|
return pvc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async applyService(coreApi: k8s.CoreV1Api, ctx: ManifestContext): Promise<any> {
|
||||||
|
const service: k8s.V1Service = {
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'Service',
|
||||||
|
metadata: { name: ctx.appName, namespace: ctx.namespace },
|
||||||
|
spec: {
|
||||||
|
selector: { app: ctx.appName },
|
||||||
|
ports: [{ port: 80, targetPort: ctx.port as any, protocol: 'TCP' }],
|
||||||
|
type: 'ClusterIP',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await coreApi.replaceNamespacedService(ctx.appName, ctx.namespace, service);
|
||||||
|
} catch {
|
||||||
|
await coreApi.createNamespacedService(ctx.namespace, service);
|
||||||
|
}
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async applyIngress(networkingApi: k8s.NetworkingV1Api, ctx: ManifestContext): Promise<any> {
|
||||||
|
const host = `${ctx.subdomain}.${ctx.domain}`;
|
||||||
|
const ingress: k8s.V1Ingress = {
|
||||||
|
apiVersion: 'networking.k8s.io/v1',
|
||||||
|
kind: 'Ingress',
|
||||||
|
metadata: {
|
||||||
|
name: ctx.appName,
|
||||||
|
namespace: ctx.namespace,
|
||||||
|
annotations: {
|
||||||
|
'cert-manager.io/cluster-issuer': 'letsencrypt-prod',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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` }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await networkingApi.replaceNamespacedIngress(ctx.appName, ctx.namespace, ingress);
|
||||||
|
} catch {
|
||||||
|
await networkingApi.createNamespacedIngress(ctx.namespace, ingress);
|
||||||
|
}
|
||||||
|
return ingress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deployDatabase(
|
||||||
|
coreApi: k8s.CoreV1Api,
|
||||||
|
appsApi: k8s.AppsV1Api,
|
||||||
|
ctx: ManifestContext,
|
||||||
|
): Promise<any> {
|
||||||
|
const dbName = `${ctx.appName}-db`;
|
||||||
|
|
||||||
|
// Create DB secret
|
||||||
|
await this.createDbSecret(coreApi, ctx.namespace, ctx.appName, ctx.dbPassword, ctx.dbUsername);
|
||||||
|
|
||||||
|
// Create PVC for DB
|
||||||
|
await this.createPVC(coreApi, ctx.namespace, dbName, ctx.dbStorageSize);
|
||||||
|
|
||||||
|
// Deploy database
|
||||||
|
const isPostgres = ctx.databaseType === DatabaseType.POSTGRESQL;
|
||||||
|
const defaultDbVersion = isPostgres ? '16' : '8.0';
|
||||||
|
const dbVer = ctx.dbVersion || defaultDbVersion;
|
||||||
|
const image = isPostgres ? `postgres:${dbVer}-alpine` : `mysql:${dbVer}`;
|
||||||
|
const port = isPostgres ? 5432 : 3306;
|
||||||
|
const dataPath = isPostgres ? '/var/lib/postgresql/data' : '/var/lib/mysql';
|
||||||
|
|
||||||
|
const envVars = isPostgres
|
||||||
|
? [
|
||||||
|
{ name: 'PGDATA', value: '/var/lib/postgresql/data/pgdata' },
|
||||||
|
{ name: 'POSTGRES_DB', value: ctx.appName.replace(/-/g, '_') },
|
||||||
|
{ name: 'POSTGRES_USER', valueFrom: { secretKeyRef: { name: `${ctx.appName}-db-secret`, key: 'username' } } },
|
||||||
|
{ name: 'POSTGRES_PASSWORD', valueFrom: { secretKeyRef: { name: `${ctx.appName}-db-secret`, key: 'password' } } },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ name: 'MYSQL_DATABASE', value: ctx.appName.replace(/-/g, '_') },
|
||||||
|
{ name: 'MYSQL_USER', valueFrom: { secretKeyRef: { name: `${ctx.appName}-db-secret`, key: 'username' } } },
|
||||||
|
{ name: 'MYSQL_PASSWORD', valueFrom: { secretKeyRef: { name: `${ctx.appName}-db-secret`, key: 'password' } } },
|
||||||
|
{ name: 'MYSQL_ROOT_PASSWORD', valueFrom: { secretKeyRef: { name: `${ctx.appName}-db-secret`, key: 'password' } } },
|
||||||
|
];
|
||||||
|
|
||||||
|
const dbDeployment: k8s.V1Deployment = {
|
||||||
|
apiVersion: 'apps/v1',
|
||||||
|
kind: 'Deployment',
|
||||||
|
metadata: { name: dbName, namespace: ctx.namespace, labels: { app: dbName } },
|
||||||
|
spec: {
|
||||||
|
replicas: 1,
|
||||||
|
selector: { matchLabels: { app: dbName } },
|
||||||
|
template: {
|
||||||
|
metadata: { labels: { app: dbName } },
|
||||||
|
spec: {
|
||||||
|
containers: [
|
||||||
|
{
|
||||||
|
name: dbName,
|
||||||
|
image,
|
||||||
|
ports: [{ containerPort: port }],
|
||||||
|
env: envVars,
|
||||||
|
volumeMounts: [{ name: 'db-storage', mountPath: dataPath }],
|
||||||
|
resources: {
|
||||||
|
requests: { cpu: '100m', memory: '256Mi' },
|
||||||
|
limits: { cpu: '500m', memory: '512Mi' },
|
||||||
|
},
|
||||||
|
readinessProbe: isPostgres
|
||||||
|
? { exec: { command: ['pg_isready', '-U', ctx.dbUsername] }, initialDelaySeconds: 10, periodSeconds: 5, failureThreshold: 6 }
|
||||||
|
: { exec: { command: ['mysqladmin', 'ping', '-h', '127.0.0.1'] }, initialDelaySeconds: 10, periodSeconds: 5, failureThreshold: 6 },
|
||||||
|
livenessProbe: isPostgres
|
||||||
|
? { exec: { command: ['pg_isready', '-U', ctx.dbUsername] }, initialDelaySeconds: 30, periodSeconds: 10, failureThreshold: 5 }
|
||||||
|
: { exec: { command: ['mysqladmin', 'ping', '-h', '127.0.0.1'] }, initialDelaySeconds: 30, periodSeconds: 10, failureThreshold: 5 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
volumes: [
|
||||||
|
{ name: 'db-storage', persistentVolumeClaim: { claimName: dbName } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await appsApi.replaceNamespacedDeployment(dbName, ctx.namespace, dbDeployment);
|
||||||
|
} catch {
|
||||||
|
await appsApi.createNamespacedDeployment(ctx.namespace, dbDeployment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create DB Service
|
||||||
|
const dbService: k8s.V1Service = {
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'Service',
|
||||||
|
metadata: { name: dbName, namespace: ctx.namespace },
|
||||||
|
spec: {
|
||||||
|
selector: { app: dbName },
|
||||||
|
ports: [{ port, targetPort: port as any, protocol: 'TCP' }],
|
||||||
|
type: 'ClusterIP',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await coreApi.replaceNamespacedService(dbName, ctx.namespace, dbService);
|
||||||
|
} catch {
|
||||||
|
await coreApi.createNamespacedService(ctx.namespace, dbService);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { deployment: dbDeployment, service: dbService };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createDbSecret(
|
||||||
|
coreApi: k8s.CoreV1Api,
|
||||||
|
namespace: string,
|
||||||
|
appName: string,
|
||||||
|
password: string,
|
||||||
|
username: string = 'appuser',
|
||||||
|
): Promise<void> {
|
||||||
|
const secret = {
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'Secret',
|
||||||
|
metadata: { name: `${appName}-db-secret`, namespace },
|
||||||
|
data: {
|
||||||
|
username: Buffer.from(username).toString('base64'),
|
||||||
|
password: Buffer.from(password).toString('base64'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await coreApi.replaceNamespacedSecret(`${appName}-db-secret`, namespace, secret);
|
||||||
|
} catch {
|
||||||
|
await coreApi.createNamespacedSecret(namespace, secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createPVC(
|
||||||
|
coreApi: k8s.CoreV1Api,
|
||||||
|
namespace: string,
|
||||||
|
name: string,
|
||||||
|
size: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const pvc: k8s.V1PersistentVolumeClaim = {
|
||||||
|
apiVersion: 'v1',
|
||||||
|
kind: 'PersistentVolumeClaim',
|
||||||
|
metadata: { name, namespace },
|
||||||
|
spec: {
|
||||||
|
accessModes: ['ReadWriteOnce'],
|
||||||
|
resources: { requests: { storage: size } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await coreApi.readNamespacedPersistentVolumeClaim(name, namespace);
|
||||||
|
// PVC exists, don't recreate
|
||||||
|
} catch {
|
||||||
|
await coreApi.createNamespacedPersistentVolumeClaim(namespace, pvc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getPodLogs(app: Application): Promise<string> {
|
async getPodLogs(app: Application): Promise<string> {
|
||||||
const { coreApi } = await this.getK8sClient(app.clusterId);
|
const { coreApi } = await this.getK8sClient(app.clusterId);
|
||||||
|
|||||||
Reference in New Issue
Block a user