import { Injectable, NotFoundException, Logger, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import * as k8s from '@kubernetes/client-node'; import { Cluster } from './entities/cluster.entity'; import { CreateClusterDto, UpdateClusterDto } from './dto/cluster.dto'; import { ClusterStatus } from '../common/enums'; @Injectable() export class ClustersService { private readonly logger = new Logger(ClustersService.name); constructor( @InjectRepository(Cluster) private clustersRepository: Repository, ) {} /** * Test connection to a Kubernetes cluster using its kubeconfig. * Calls the /version endpoint to verify the cluster is reachable. */ async testConnection(kubeconfig: string): Promise<{ connected: boolean; version?: string; error?: string }> { try { const kc = new k8s.KubeConfig(); kc.loadFromString(kubeconfig); const versionApi = kc.makeApiClient(k8s.VersionApi); const result = await versionApi.getCode(); const info = result.body; this.logger.log(`Cluster connection OK: Kubernetes ${info.gitVersion}`); return { connected: true, version: info.gitVersion, }; } catch (err: any) { const message = err?.body?.message || err?.message || 'Unknown connection error'; this.logger.warn(`Cluster connection failed: ${message}`); return { connected: false, error: message, }; } } async create(dto: CreateClusterDto): Promise { // Validate kubeconfig by testing actual connection const connectionTest = await this.testConnection(dto.kubeconfig); if (!connectionTest.connected) { throw new BadRequestException( `Cannot connect to Kubernetes cluster: ${connectionTest.error}`, ); } if (dto.isDefault === true) { const existingDefaults = await this.clustersRepository.find({ where: { isDefault: true } }); for (const c of existingDefaults) { c.isDefault = false; await this.clustersRepository.save(c); } } const cluster = this.clustersRepository.create({ ...dto, status: ClusterStatus.ACTIVE, // Connection verified — mark active }); const saved = await this.clustersRepository.save(cluster); this.logger.log(`Cluster "${saved.name}" registered (active) — K8s ${connectionTest.version}`); return saved; } async findAll(): Promise { return this.clustersRepository.find({ select: ['id', 'name', 'description', 'status', 'apiServer', 'region', 'provider', 'isDefault', 'createdAt'], order: { createdAt: 'DESC' }, }); } async findOne(id: string): Promise { const cluster = await this.clustersRepository.findOne({ where: { id } }); if (!cluster) { throw new NotFoundException('Cluster not found'); } return cluster; } async getDefault(): Promise { const cluster = await this.clustersRepository.findOne({ where: { isDefault: true } }); if (!cluster) { throw new NotFoundException('No default cluster configured'); } return cluster; } async update(id: string, dto: UpdateClusterDto): Promise { const cluster = await this.findOne(id); // If kubeconfig is being updated, re-test connection if (dto.kubeconfig) { const connectionTest = await this.testConnection(dto.kubeconfig); if (!connectionTest.connected) { throw new BadRequestException( `Cannot connect to Kubernetes cluster: ${connectionTest.error}`, ); } dto.status = ClusterStatus.ACTIVE; this.logger.log(`Cluster "${cluster.name}" kubeconfig updated — connection verified (K8s ${connectionTest.version})`); } if (dto.isDefault === true) { const existingDefaults = await this.clustersRepository.find({ where: { isDefault: true } }); for (const c of existingDefaults) { if (c.id !== id) { c.isDefault = false; await this.clustersRepository.save(c); } } } Object.assign(cluster, dto); return this.clustersRepository.save(cluster); } /** * Manually test connectivity to an existing cluster. * Updates status to active/inactive based on result. */ async testClusterById(id: string): Promise<{ connected: boolean; version?: string; error?: string }> { const cluster = await this.findOne(id); const result = await this.testConnection(cluster.kubeconfig); cluster.status = result.connected ? ClusterStatus.ACTIVE : ClusterStatus.INACTIVE; await this.clustersRepository.save(cluster); this.logger.log(`Cluster "${cluster.name}" test: ${result.connected ? 'ACTIVE' : 'INACTIVE'}`); return result; } async delete(id: string): Promise { const cluster = await this.findOne(id); await this.clustersRepository.remove(cluster); } }