feat: admin user management (create/search/role) and cluster resource monitoring
- Add POST /users endpoint for admin to create users with hashed passwords - Add GET /users?search= with ILike search on email/firstName/lastName - Add PATCH /users/:id/role for role assignment (user/admin) - Return appCount per user in the users list - Add GET /clusters/:id/resources for node, CPU, memory, pod monitoring - Parse K8s node capacity/allocatable with CPU millicores and memory MiB helpers - Frontend: admin users page with search bar, create form, role dropdown, app count - Frontend: cluster resource panel with nodes table, CPU/memory bars, summary cards
This commit is contained in:
@@ -54,6 +54,13 @@ export class ClustersController {
|
||||
return this.clustersService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id/resources')
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiOperation({ summary: 'Get cluster resource usage — nodes, CPU, memory, pods (Admin only)' })
|
||||
async getClusterResources(@Param('id') id: string) {
|
||||
return this.clustersService.getClusterResources(id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Roles(UserRole.ADMIN)
|
||||
@ApiOperation({ summary: 'Get cluster details (Admin only)' })
|
||||
|
||||
@@ -348,4 +348,101 @@ export class ClustersService {
|
||||
this.logger.log(`Pool "${pool.name}" least-apps → cluster "${selected.name}" (${countMap.get(selected.id) || 0} apps)`);
|
||||
return selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resource usage for a specific cluster — nodes, total CPU/memory, pod counts.
|
||||
*/
|
||||
async getClusterResources(id: string): Promise<{
|
||||
nodes: { name: string; status: string; roles: string; cpuCapacity: string; memoryCapacity: string; cpuAllocatable: string; memoryAllocatable: string; }[];
|
||||
totalCpuCapacity: string;
|
||||
totalMemoryCapacity: string;
|
||||
totalCpuAllocatable: string;
|
||||
totalMemoryAllocatable: string;
|
||||
podCount: number;
|
||||
nodeCount: number;
|
||||
appCount: number;
|
||||
}> {
|
||||
const cluster = await this.findOne(id);
|
||||
|
||||
const kc = new k8s.KubeConfig();
|
||||
kc.loadFromString(cluster.kubeconfig);
|
||||
const coreApi = kc.makeApiClient(k8s.CoreV1Api);
|
||||
|
||||
try {
|
||||
// Get nodes
|
||||
const nodesRes = await coreApi.listNode();
|
||||
const nodes = nodesRes.body.items.map((node) => {
|
||||
const conditions = node.status?.conditions || [];
|
||||
const readyCondition = conditions.find((c) => c.type === 'Ready');
|
||||
const roles = Object.keys(node.metadata?.labels || {})
|
||||
.filter((l) => l.startsWith('node-role.kubernetes.io/'))
|
||||
.map((l) => l.replace('node-role.kubernetes.io/', ''))
|
||||
.join(', ') || 'worker';
|
||||
|
||||
return {
|
||||
name: node.metadata?.name || 'unknown',
|
||||
status: readyCondition?.status === 'True' ? 'Ready' : 'NotReady',
|
||||
roles,
|
||||
cpuCapacity: node.status?.capacity?.cpu || '0',
|
||||
memoryCapacity: node.status?.capacity?.memory || '0',
|
||||
cpuAllocatable: node.status?.allocatable?.cpu || '0',
|
||||
memoryAllocatable: node.status?.allocatable?.memory || '0',
|
||||
};
|
||||
});
|
||||
|
||||
// Total capacity
|
||||
let totalCpuCap = 0;
|
||||
let totalMemCap = 0;
|
||||
let totalCpuAlloc = 0;
|
||||
let totalMemAlloc = 0;
|
||||
for (const node of nodes) {
|
||||
totalCpuCap += this.parseCpuToMillicores(node.cpuCapacity);
|
||||
totalMemCap += this.parseMemoryToMi(node.memoryCapacity);
|
||||
totalCpuAlloc += this.parseCpuToMillicores(node.cpuAllocatable);
|
||||
totalMemAlloc += this.parseMemoryToMi(node.memoryAllocatable);
|
||||
}
|
||||
|
||||
// Get all pods count
|
||||
const podsRes = await coreApi.listPodForAllNamespaces();
|
||||
const podCount = podsRes.body.items.length;
|
||||
|
||||
// Get app count for this cluster
|
||||
const appCountResult = await this.dataSource.query(
|
||||
`SELECT COUNT(*) as count FROM applications WHERE "clusterId" = $1`,
|
||||
[id],
|
||||
);
|
||||
const appCount = parseInt(appCountResult[0]?.count || '0', 10);
|
||||
|
||||
return {
|
||||
nodes,
|
||||
totalCpuCapacity: `${totalCpuCap}m`,
|
||||
totalMemoryCapacity: `${totalMemCap.toFixed(0)}Mi`,
|
||||
totalCpuAllocatable: `${totalCpuAlloc}m`,
|
||||
totalMemoryAllocatable: `${totalMemAlloc.toFixed(0)}Mi`,
|
||||
podCount,
|
||||
nodeCount: nodes.length,
|
||||
appCount,
|
||||
};
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Failed to get cluster resources for "${cluster.name}": ${err.message}`);
|
||||
throw new BadRequestException(`Cannot fetch resources: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private parseCpuToMillicores(cpu: string): number {
|
||||
if (!cpu || cpu === '0') return 0;
|
||||
if (cpu.endsWith('n')) return parseFloat(cpu) / 1_000_000;
|
||||
if (cpu.endsWith('u')) return parseFloat(cpu) / 1_000;
|
||||
if (cpu.endsWith('m')) return parseFloat(cpu);
|
||||
return parseFloat(cpu) * 1000;
|
||||
}
|
||||
|
||||
private parseMemoryToMi(memory: string): number {
|
||||
if (!memory || memory === '0') return 0;
|
||||
if (memory.endsWith('Ki')) return parseFloat(memory) / 1024;
|
||||
if (memory.endsWith('Mi')) return parseFloat(memory);
|
||||
if (memory.endsWith('Gi')) return parseFloat(memory) * 1024;
|
||||
if (memory.endsWith('Ti')) return parseFloat(memory) * 1024 * 1024;
|
||||
return parseFloat(memory) / (1024 * 1024); // bytes
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user