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:
@@ -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[];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user