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:
keyhan
2026-04-05 17:48:15 +03:30
parent 2621dc0cc6
commit e97af36740
8 changed files with 564 additions and 69 deletions
+97
View File
@@ -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
}
}