feat: multi-cluster management with pool-based load balancing

- Add ClusterPool entity for grouping clusters into named pools
- Support 3 deployment modes: manual cluster, pool load-balanced, default fallback
- Pool strategies: least-apps (fewest deployed apps) and round-robin
- Add pool CRUD API endpoints (admin) and public pool listing
- Frontend deploy page: 3-mode cluster selector (Default/Manual/Pool)
- Frontend app detail: shows assigned cluster and pool info
- Admin pools management page with cluster selection and strategy picker
- Application entity extended with poolId field
This commit is contained in:
keyhan
2026-04-05 17:37:05 +03:30
parent 1b1ccfc18f
commit 2621dc0cc6
14 changed files with 976 additions and 14 deletions
@@ -20,22 +20,46 @@ export class ApplicationsService {
) {}
async create(userId: string, dto: CreateApplicationDto): Promise<Application> {
// Auto-assign default cluster if not specified
// Cluster assignment: 3 modes
// 1. Manual: specific clusterId provided
// 2. Pool-based LB: poolId provided → pick from pool using pool's strategy
// 3. Default fallback: no clusterId/poolId → use default cluster
let clusterId = dto.clusterId;
let poolId = dto.poolId;
if (!clusterId) {
try {
const defaultCluster = await this.clustersService.getDefault();
clusterId = defaultCluster.id;
this.logger.log(`Auto-assigned default cluster "${defaultCluster.name}" to app "${dto.name}"`);
} catch {
this.logger.warn('No default cluster found — app will be created without cluster assignment');
if (poolId) {
// Mode 2: Pool-based load balancing
try {
const optimal = await this.clustersService.getOptimalClusterFromPool(poolId);
clusterId = optimal.id;
this.logger.log(`Pool-assigned cluster "${optimal.name}" to app "${dto.name}" (pool LB)`);
} catch (err: any) {
this.logger.warn(`Pool assignment failed for app "${dto.name}": ${err.message}`);
poolId = undefined; // Clear invalid pool
}
}
// Mode 3: Default fallback (no manual cluster, no pool, or pool failed)
if (!clusterId) {
try {
const defaultCluster = await this.clustersService.getDefault();
clusterId = defaultCluster.id;
this.logger.log(`Default-assigned cluster "${defaultCluster.name}" to app "${dto.name}"`);
} catch {
this.logger.warn('No cluster available — app will be created without cluster assignment');
}
}
} else {
this.logger.log(`Manual cluster assignment for app "${dto.name}" → cluster ${clusterId}`);
}
const app = this.appsRepository.create({
...dto,
userId,
clusterId,
poolId,
subdomain: `${dto.name}-${userId.split('-')[0]}`,
});
return this.appsRepository.save(app);
@@ -88,6 +88,11 @@ export class CreateApplicationDto {
@IsOptional()
@IsString()
clusterId?: string;
@ApiPropertyOptional({ description: 'Cluster pool ID for load-balanced deployment' })
@IsOptional()
@IsString()
poolId?: string;
}
export class UpdateApplicationDto {
@@ -74,6 +74,9 @@ export class Application {
@Column({ nullable: true })
clusterId: string;
@Column({ nullable: true })
poolId: string; // Cluster pool used for load-balanced assignment
@OneToMany(() => Deployment, (deployment: Deployment) => deployment.application)
deployments: Deployment[];