Use in-cluster registry for builds and deploys; improve logging and cluster ops.
Remove external registry Ingress (repo.3fase.ir) and route Kaniko push and app pulls through the internal ClusterIP registry. Add RegistryService, ensure StorageClass and pull secrets on deploy, make Elasticsearch install/repair more resilient, and add per-cluster Deploy Elastic controls in admin UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
isManagedProductType,
|
||||
} from '../common/enums';
|
||||
import { HelmService } from './helm.service';
|
||||
import { RegistryService } from './registry.service';
|
||||
import { registerKubeconfigNoProxy } from '../common/kubernetes-proxy.util';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -72,6 +73,7 @@ export class KubernetesService implements OnModuleInit {
|
||||
private configService: ConfigService,
|
||||
private clustersService: ClustersService,
|
||||
private helmService: HelmService,
|
||||
private registryService: RegistryService,
|
||||
) {}
|
||||
|
||||
onModuleInit() {
|
||||
@@ -161,10 +163,45 @@ export class KubernetesService implements OnModuleInit {
|
||||
return { storageClass, createStorageClass, storageProvisioner };
|
||||
}
|
||||
|
||||
/** Create platform StorageClass on the target cluster when configured (Helm chart + K8s API fallback). */
|
||||
private async ensurePlatformStorageClass(kubeconfig: string): Promise<void> {
|
||||
const storageClass = this.configService.get<string>('platform.storageClass')?.trim();
|
||||
const createStorageClass = this.configService.get<boolean>('platform.createStorageClass') === true;
|
||||
if (!storageClass || !createStorageClass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const kc = new k8s.KubeConfig();
|
||||
kc.loadFromString(kubeconfig);
|
||||
const storageApi = kc.makeApiClient(k8s.StorageV1Api);
|
||||
const provisioner =
|
||||
this.configService.get<string>('platform.storageProvisioner') || 'rancher.io/local-path';
|
||||
|
||||
try {
|
||||
await storageApi.readStorageClass(storageClass);
|
||||
return;
|
||||
} catch (err: any) {
|
||||
if (err.statusCode !== 404 && err.body?.code !== 404) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await storageApi.createStorageClass({
|
||||
apiVersion: 'storage.k8s.io/v1',
|
||||
kind: 'StorageClass',
|
||||
metadata: { name: storageClass },
|
||||
provisioner,
|
||||
allowVolumeExpansion: true,
|
||||
reclaimPolicy: 'Delete',
|
||||
volumeBindingMode: 'WaitForFirstConsumer',
|
||||
});
|
||||
this.logger.log(`Created StorageClass "${storageClass}" (provisioner: ${provisioner})`);
|
||||
}
|
||||
|
||||
/** Helm values for managed_database / managed_redis / managed_rabbitmq (no app workload). */
|
||||
private buildManagedHelmValues(app: Application): Record<string, any> {
|
||||
const namespace = `user-${app.userId.split('-')[0]}`;
|
||||
const pullRegistryUrl = this.configService.get<string>('registry.pullUrl') || 'localhost:30500';
|
||||
const pullRegistryUrl = this.registryService.getRegistryUrl();
|
||||
const isPostgres = app.databaseType === DatabaseType.POSTGRESQL;
|
||||
const productType = app.productType;
|
||||
|
||||
@@ -244,7 +281,7 @@ export class KubernetesService implements OnModuleInit {
|
||||
|
||||
private buildHelmValues(app: Application, imageUri: string): Record<string, any> {
|
||||
const domain = this.configService.get('platform.domain');
|
||||
const pullRegistryUrl = this.configService.get<string>('registry.pullUrl') || 'localhost:30500';
|
||||
const pullRegistryUrl = this.registryService.getRegistryUrl();
|
||||
const isWordPress = app.runtime === AppRuntime.WORDPRESS;
|
||||
const hasDb = app.databaseType !== DatabaseType.NONE;
|
||||
const isPostgres = app.databaseType === DatabaseType.POSTGRESQL;
|
||||
@@ -332,14 +369,20 @@ export class KubernetesService implements OnModuleInit {
|
||||
if (isManagedProductType(app.productType)) {
|
||||
return this.deployManagedService(app);
|
||||
}
|
||||
|
||||
const workloadImage = this.registryService.normalizeImageReference(imageUri);
|
||||
if (workloadImage !== imageUri) {
|
||||
this.logger.log(`Using in-cluster registry image for ${app.name}: ${workloadImage}`);
|
||||
}
|
||||
|
||||
// Try Helm first, fall back to direct K8s API if Helm is unavailable
|
||||
try {
|
||||
return await this.deployViaHelm(app, imageUri);
|
||||
return await this.deployViaHelm(app, workloadImage);
|
||||
} 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);
|
||||
return await this.deployViaK8sApi(app, workloadImage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +442,7 @@ export class KubernetesService implements OnModuleInit {
|
||||
try {
|
||||
const kubeconfig = await this.getKubeconfig(app.clusterId);
|
||||
const imageUri = app.latestImageTag
|
||||
? `${this.configService.get<string>('registry.pullUrl') || 'localhost:30500'}/${app.name}:${app.latestImageTag}`
|
||||
? this.registryService.normalizeImageReference(app.latestImageTag)
|
||||
: '';
|
||||
const values = this.buildHelmValues(app, imageUri);
|
||||
await this.helmService.installOrUpgrade(app.name, namespace, values, kubeconfig);
|
||||
@@ -446,8 +489,11 @@ export class KubernetesService implements OnModuleInit {
|
||||
|
||||
private async deployViaHelm(app: Application, imageUri: string): Promise<Record<string, any>> {
|
||||
const kubeconfig = await this.getKubeconfig(app.clusterId);
|
||||
await this.ensurePlatformStorageClass(kubeconfig);
|
||||
const { coreApi } = await this.getK8sClient(app.clusterId);
|
||||
const values = this.buildHelmValues(app, imageUri);
|
||||
const namespace = values.app.namespace;
|
||||
const namespace = values.app.namespace as string;
|
||||
await this.registryService.ensureRegistryPullSecret(coreApi, namespace);
|
||||
const releaseName = app.name;
|
||||
|
||||
const result = await this.helmService.installOrUpgrade(
|
||||
@@ -463,6 +509,7 @@ export class KubernetesService implements OnModuleInit {
|
||||
|
||||
private async deployManagedViaHelm(app: Application): Promise<Record<string, any>> {
|
||||
const kubeconfig = await this.getKubeconfig(app.clusterId);
|
||||
await this.ensurePlatformStorageClass(kubeconfig);
|
||||
const values = this.buildManagedHelmValues(app);
|
||||
const namespace = values.app.namespace;
|
||||
const releaseName = app.name;
|
||||
@@ -482,6 +529,8 @@ export class KubernetesService implements OnModuleInit {
|
||||
|
||||
private async deployManagedViaK8sApi(app: Application): Promise<Record<string, any>> {
|
||||
const { coreApi, appsApi } = await this.getK8sClient(app.clusterId);
|
||||
const kubeconfig = await this.getKubeconfig(app.clusterId);
|
||||
await this.ensurePlatformStorageClass(kubeconfig);
|
||||
const namespace = `user-${app.userId.split('-')[0]}`;
|
||||
const context: ManifestContext = {
|
||||
appName: app.name,
|
||||
@@ -546,6 +595,8 @@ export class KubernetesService implements OnModuleInit {
|
||||
return this.deployManagedViaK8sApi(app);
|
||||
}
|
||||
const { coreApi, appsApi, networkingApi } = await this.getK8sClient(app.clusterId);
|
||||
const kubeconfig = await this.getKubeconfig(app.clusterId);
|
||||
await this.ensurePlatformStorageClass(kubeconfig);
|
||||
const domain = this.configService.get('platform.domain');
|
||||
|
||||
const context: ManifestContext = {
|
||||
@@ -585,6 +636,9 @@ export class KubernetesService implements OnModuleInit {
|
||||
// 1. Ensure namespace exists
|
||||
await this.ensureNamespace(coreApi, context.namespace);
|
||||
|
||||
// 1b. Image pull secret for in-cluster registry
|
||||
await this.registryService.ensureRegistryPullSecret(coreApi, context.namespace);
|
||||
|
||||
// 2. Create/Update secrets for env vars
|
||||
if (Object.keys(context.envVars).length > 0) {
|
||||
manifests.secret = await this.applySecret(coreApi, context);
|
||||
|
||||
Reference in New Issue
Block a user