Files
cloud-host/backend/src/clusters/clusters.service.ts
T
keyhan 33be1649c4 init
2026-04-05 15:22:01 +03:30

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);
}
}