Add per-cluster Tools Management and stop auto-installing side tools.

Introduce a catalog-driven Tools Management section under Clusters so
admins can install/uninstall infrastructure tools per cluster: cert-manager
(Helm/jetstack), ClusterIssuer (email + HTTP01 form, depends on cert-manager),
and central Elasticsearch. Cluster creation no longer auto-installs Elastic
or the cloudhost-node-cluster-dns DaemonSet; build bootstrap stays automatic.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-05-31 16:55:51 +03:30
parent be2587dcf5
commit 786689e0fd
9 changed files with 798 additions and 247 deletions
+2 -96
View File
@@ -20,7 +20,6 @@ import { ClusterAllocationLog } from './entities/cluster-allocation-log.entity';
import { CreateClusterDto, UpdateClusterDto } from './dto/cluster.dto';
import { CreateClusterPoolDto, UpdateClusterPoolDto } from './dto/cluster-pool.dto';
import { ClusterStatus } from '../common/enums';
import { ElasticsearchService } from '../kubernetes/elasticsearch.service';
import { RegistryService } from '../kubernetes/registry.service';
import { CreateApplicationDto } from '../applications/dto/application.dto';
import { registerKubeconfigNoProxy } from '../common/kubernetes-proxy.util';
@@ -53,8 +52,6 @@ export class ClustersService implements OnModuleInit, OnModuleDestroy {
private allocationLogsRepository: Repository<ClusterAllocationLog>,
private dataSource: DataSource,
private configService: ConfigService,
@Inject(forwardRef(() => ElasticsearchService))
private elasticsearchService: ElasticsearchService,
@Inject(forwardRef(() => RegistryService))
private registryService: RegistryService,
) {}
@@ -140,10 +137,8 @@ export class ClustersService implements OnModuleInit, OnModuleDestroy {
this.logger.error(`Failed to bootstrap cluster "${saved.name}": ${err.message}`);
});
// Deploy central logging (Elasticsearch + Kibana) via Helm
this.elasticsearchService.deploy(saved.id).catch((err) => {
this.logger.error(`Failed to deploy central logging on "${saved.name}": ${err.message}`);
});
// Infrastructure tools (central logging, cert-manager, ClusterIssuer, …) are no
// longer auto-installed. Install them on demand via Cluster → Tools Management.
this.recordHealthSnapshot(saved, {
status: 'healthy',
@@ -1005,100 +1000,11 @@ export class ClustersService implements OnModuleInit, OnModuleDestroy {
}
}
await this.ensureNodeClusterDns(coreApi, appsApi);
await this.ensureK3sRegistryMirrors(appsApi, registryUrl);
this.logger.log(`✅ Cluster bootstrap complete — registry: ${registryUrl}`);
}
/**
* Kubelet/containerd pull images on the host network stack, which uses the node's
* resolver (often systemd-resolved) — not pod DNS. Forward *.cluster.local to CoreDNS
* so registry.cloudhost-builds.svc.cluster.local resolves during image pulls.
*/
private async ensureNodeClusterDns(
coreApi: k8s.CoreV1Api,
appsApi: k8s.AppsV1Api,
): Promise<void> {
const dsName = 'cloudhost-node-cluster-dns';
const namespace = 'kube-system';
let clusterDnsIp = '10.43.0.10';
try {
const dnsSvc = await coreApi.readNamespacedService('kube-dns', namespace);
clusterDnsIp = dnsSvc.body.spec?.clusterIP || clusterDnsIp;
} catch (err: any) {
this.logger.warn(
`Could not read kube-dns ClusterIP (${err.message}); using ${clusterDnsIp}`,
);
}
const configureScript = [
'set -e',
'CONF=/host/etc/systemd/resolved.conf.d/k8s-cluster-dns.conf',
'mkdir -p /host/etc/systemd/resolved.conf.d',
`cat > /tmp/k8s-cluster-dns.conf <<'EOF'`,
'[Resolve]',
`DNS=${clusterDnsIp}`,
'Domains=~cluster.local',
'EOF',
'if [ ! -f "$CONF" ] || ! cmp -s /tmp/k8s-cluster-dns.conf "$CONF"; then',
' cp /tmp/k8s-cluster-dns.conf "$CONF"',
' echo "Updated k8s-cluster-dns.conf"',
' if nsenter -t 1 -m -u -i -n -p -- systemctl is-active systemd-resolved >/dev/null 2>&1; then',
' nsenter -t 1 -m -u -i -n -p -- systemctl restart systemd-resolved',
' fi',
'fi',
'sleep infinity',
].join('\n');
const daemonSet: k8s.V1DaemonSet = {
metadata: {
name: dsName,
namespace,
labels: { 'app.kubernetes.io/managed-by': 'cloudhost' },
},
spec: {
selector: { matchLabels: { app: dsName } },
template: {
metadata: { labels: { app: dsName } },
spec: {
hostPID: true,
tolerations: [{ operator: 'Exists' }],
containers: [
{
name: 'configure',
image: 'rancher/mirrored-library-busybox:1.36.1',
command: ['/bin/sh', '-ec'],
args: [configureScript],
securityContext: { privileged: true },
volumeMounts: [{ name: 'etc', mountPath: '/host/etc' }],
},
],
volumes: [
{
name: 'etc',
hostPath: { path: '/etc', type: 'Directory' },
},
],
},
},
},
};
try {
await appsApi.readNamespacedDaemonSet(dsName, namespace);
await appsApi.replaceNamespacedDaemonSet(dsName, namespace, daemonSet);
this.logger.log(`Updated DaemonSet "${dsName}" (cluster DNS ${clusterDnsIp})`);
} catch (err: any) {
if (err.statusCode === 404 || err.body?.code === 404) {
await appsApi.createNamespacedDaemonSet(namespace, daemonSet);
this.logger.log(`Created DaemonSet "${dsName}" (cluster DNS ${clusterDnsIp})`);
} else {
throw err;
}
}
}
/** In-cluster registry mirror for k3s/containerd (HTTP). Removes legacy external-registry DaemonSet if present. */
private async ensureK3sRegistryMirrors(
appsApi: k8s.AppsV1Api,