143 lines
4.9 KiB
TypeScript
143 lines
4.9 KiB
TypeScript
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<Cluster>,
|
|
) {}
|
|
|
|
/**
|
|
* 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<Cluster> {
|
|
// 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<Cluster[]> {
|
|
return this.clustersRepository.find({
|
|
select: ['id', 'name', 'description', 'status', 'apiServer', 'region', 'provider', 'isDefault', 'createdAt'],
|
|
order: { createdAt: 'DESC' },
|
|
});
|
|
}
|
|
|
|
async findOne(id: string): Promise<Cluster> {
|
|
const cluster = await this.clustersRepository.findOne({ where: { id } });
|
|
if (!cluster) {
|
|
throw new NotFoundException('Cluster not found');
|
|
}
|
|
return cluster;
|
|
}
|
|
|
|
async getDefault(): Promise<Cluster> {
|
|
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<Cluster> {
|
|
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<void> {
|
|
const cluster = await this.findOne(id);
|
|
await this.clustersRepository.remove(cluster);
|
|
}
|
|
}
|