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:
keyhan
2026-05-26 19:15:06 +03:30
parent 1ade52825c
commit d7594df9a0
20 changed files with 547 additions and 237 deletions
+157 -18
View File
@@ -59,6 +59,21 @@ export interface LogStatsResult {
period: string;
}
export type LoggingDeployStatus = 'not_installed' | 'installing' | 'failed' | 'ready';
export interface LoggingDeployState {
status: LoggingDeployStatus;
helmReleaseStatus?: string | null;
health: {
status: string;
clusterName: string;
numberOfNodes: number;
activePrimaryShards: number;
activeShards: number;
kibanaReady: boolean;
} | null;
}
/**
* Central Elasticsearch management service.
* Deploys a shared Elasticsearch + Kibana stack in a dedicated namespace
@@ -358,17 +373,124 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
}
/**
* Check if central Elasticsearch is deployed in a cluster
* Check if central Elasticsearch is deployed and healthy in a cluster.
*/
async isDeployed(clusterId?: string): Promise<boolean> {
const { appsApi } = await this.getK8sClients(clusterId);
const state = await this.getDeployState(clusterId);
return state.status === 'ready';
}
/**
* Detailed logging stack state (Helm release + pod readiness).
*/
async getDeployState(clusterId?: string): Promise<LoggingDeployState> {
const { cluster } = await this.getK8sClients(clusterId);
const helmStatus = await this.helmService.status(
LOGGING_HELM_RELEASE,
LOGGING_HELM_NAMESPACE,
cluster.kubeconfig,
);
let hasEsWorkload = false;
try {
const kc = new k8s.KubeConfig();
kc.loadFromString(cluster.kubeconfig);
const appsApi = kc.makeApiClient(k8s.AppsV1Api);
await appsApi.readNamespacedStatefulSet(this.ES_NAME, this.ES_NAMESPACE);
hasEsWorkload = true;
} catch {
hasEsWorkload = false;
}
if (!helmStatus && !hasEsWorkload) {
return { status: 'not_installed', helmReleaseStatus: null, health: null };
}
const health = await this.getHealth(clusterId);
const helmReleaseStatus = helmStatus?.status || null;
if (health?.status === 'green') {
return { status: 'ready', helmReleaseStatus, health };
}
if (helmReleaseStatus === 'failed' || helmReleaseStatus === 'pending-install') {
return { status: 'failed', helmReleaseStatus, health };
}
if (hasEsWorkload || helmReleaseStatus === 'deployed') {
return { status: 'installing', helmReleaseStatus, health };
}
return { status: 'installing', helmReleaseStatus, health };
}
private resolveLoggingStorageClass(): string | undefined {
const storageClass = this.configService.get<string>('platform.storageClass');
return storageClass?.trim() || undefined;
}
/**
* Ensure platform StorageClass exists before Helm creates PVCs (same as app deploy chart).
*/
private async ensureClusterStorageClass(kubeconfig: string): Promise<void> {
const storageClass = this.resolveLoggingStorageClass();
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 appsApi.readNamespacedStatefulSet(this.ES_NAME, this.ES_NAMESPACE);
return true;
} catch {
return false;
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})`);
}
private buildLoggingHelmValues(): {
elasticPassword: string;
fluentbitPassword: string;
kibanaSystemPassword: string;
storageClass?: string;
images: {
elasticsearch?: string;
kibana?: string;
busybox?: string;
curl?: string;
};
} {
const storageClass = this.resolveLoggingStorageClass();
return {
elasticPassword: this.ELASTIC_PASSWORD,
fluentbitPassword: this.FLUENTBIT_PASSWORD,
kibanaSystemPassword: this.KIBANA_SYSTEM_PASSWORD,
...(storageClass ? { storageClass } : {}),
images: {
elasticsearch: this.configService.get<string>('elasticsearch.images.elasticsearch'),
kibana: this.configService.get<string>('elasticsearch.images.kibana'),
busybox: this.configService.get<string>('elasticsearch.images.busybox'),
curl: this.configService.get<string>('elasticsearch.images.curl'),
},
};
}
/**
@@ -432,28 +554,45 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
}
/**
* Deploy central Elasticsearch + Kibana stack via Helm.
* Deploy or repair central Elasticsearch + Kibana stack via Helm.
* Always runs upgrade --install; does not wait for pods (avoids timeout errors on slow image pulls).
*/
async deploy(clusterId?: string): Promise<{ esPassword: string; kibanaUrl: string }> {
async deploy(clusterId?: string): Promise<{ esPassword: string; kibanaUrl: string; deploying: boolean }> {
const { cluster } = await this.getK8sClients(clusterId);
await this.helmService.installLoggingStack(cluster.kubeconfig, {
elasticPassword: this.ELASTIC_PASSWORD,
fluentbitPassword: this.FLUENTBIT_PASSWORD,
kibanaSystemPassword: this.KIBANA_SYSTEM_PASSWORD,
images: {
elasticsearch: this.configService.get<string>('elasticsearch.images.elasticsearch'),
kibana: this.configService.get<string>('elasticsearch.images.kibana'),
},
});
await this.ensureClusterStorageClass(cluster.kubeconfig);
const helmValues = this.buildLoggingHelmValues();
try {
await this.helmService.installLoggingStack(cluster.kubeconfig, helmValues, {
wait: false,
timeout: '10m',
});
} catch (error: any) {
const release = await this.helmService.status(
LOGGING_HELM_RELEASE,
LOGGING_HELM_NAMESPACE,
cluster.kubeconfig,
);
if (!release) {
throw error;
}
this.logger.warn(
`Helm logging install reported an error but release exists (${release.status}); continuing: ${error.message}`,
);
}
this.logger.log(
`Central logging stack deployed via Helm (${LOGGING_HELM_RELEASE} in ${LOGGING_HELM_NAMESPACE})`,
`Central logging stack applied via Helm (${LOGGING_HELM_RELEASE} in ${LOGGING_HELM_NAMESPACE})`,
);
const state = await this.getDeployState(clusterId);
return {
esPassword: this.ELASTIC_PASSWORD,
kibanaUrl: `http://kibana.${this.ES_NAMESPACE}.svc.cluster.local:5601`,
deploying: state.status !== 'ready',
};
}