From 834e17e7cf1724f69372a93ad9d3895328e3cfbc Mon Sep 17 00:00:00 2001 From: Vantz Stockwell Date: Sat, 21 Feb 2026 14:45:06 -0500 Subject: [PATCH] feat: Add backend support for one-click Rust server deployment Add deploy endpoint, DTO, NATS command publisher, and WebSocket bridge subscription to support the one-click server deployment feature via the companion agent. Co-Authored-By: Claude Opus 4.6 --- .../modules/servers/dto/deploy-server.dto.ts | 41 +++++++++++++++++++ .../src/modules/servers/servers.controller.ts | 11 +++++ .../src/modules/servers/servers.service.ts | 9 ++++ .../src/services/nats-bridge.service.ts | 5 +++ backend-nest/src/services/nats.service.ts | 9 ++++ 5 files changed, 75 insertions(+) create mode 100644 backend-nest/src/modules/servers/dto/deploy-server.dto.ts diff --git a/backend-nest/src/modules/servers/dto/deploy-server.dto.ts b/backend-nest/src/modules/servers/dto/deploy-server.dto.ts new file mode 100644 index 0000000..3b857ee --- /dev/null +++ b/backend-nest/src/modules/servers/dto/deploy-server.dto.ts @@ -0,0 +1,41 @@ +import { IsString, IsInt, Min, Max, MinLength } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class DeployServerDto { + @ApiProperty({ example: 'My Rust Server', description: 'Server hostname' }) + @IsString() + server_name: string; + + @ApiProperty({ example: 100, description: 'Maximum player slots' }) + @IsInt() + @Min(1) + @Max(500) + max_players: number; + + @ApiProperty({ example: 4000, description: 'World size (1000-8000)' }) + @IsInt() + @Min(1000) + @Max(8000) + world_size: number; + + @ApiProperty({ example: 12345, description: 'Map seed' }) + @IsInt() + seed: number; + + @ApiProperty({ example: 28015, description: 'Server game port' }) + @IsInt() + @Min(1024) + @Max(65535) + server_port: number; + + @ApiProperty({ example: 28016, description: 'RCON port' }) + @IsInt() + @Min(1024) + @Max(65535) + rcon_port: number; + + @ApiProperty({ example: 'changeme', description: 'RCON password (min 6 chars)' }) + @IsString() + @MinLength(6) + rcon_password: string; +} diff --git a/backend-nest/src/modules/servers/servers.controller.ts b/backend-nest/src/modules/servers/servers.controller.ts index 0c1e0b3..0100d77 100644 --- a/backend-nest/src/modules/servers/servers.controller.ts +++ b/backend-nest/src/modules/servers/servers.controller.ts @@ -3,6 +3,7 @@ import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; import { ServersService } from './servers.service'; import { UpdateServerConfigDto } from './dto/update-config.dto'; import { SendCommandDto } from './dto/send-command.dto'; +import { DeployServerDto } from './dto/deploy-server.dto'; import { CurrentTenant } from '../../common/decorators/current-tenant.decorator'; import { RequirePermission } from '../../common/decorators/require-permission.decorator'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; @@ -62,4 +63,14 @@ export class ServersController { async restartServer(@CurrentTenant() licenseId: string) { return await this.serversService.restartServer(licenseId); } + + @Post('deploy') + @RequirePermission('server.manage') + @ApiOperation({ summary: 'Deploy Rust server via companion agent' }) + async deployServer( + @CurrentTenant() licenseId: string, + @Body() dto: DeployServerDto, + ) { + return await this.serversService.deployServer(licenseId, dto); + } } diff --git a/backend-nest/src/modules/servers/servers.service.ts b/backend-nest/src/modules/servers/servers.service.ts index a5afef4..6c1417e 100644 --- a/backend-nest/src/modules/servers/servers.service.ts +++ b/backend-nest/src/modules/servers/servers.service.ts @@ -5,6 +5,7 @@ import { ServerConnection } from '../../entities/server-connection.entity'; import { ServerConfig } from '../../entities/server-config.entity'; import { NatsService } from '../../services/nats.service'; import { UpdateServerConfigDto } from './dto/update-config.dto'; +import { DeployServerDto } from './dto/deploy-server.dto'; @Injectable() export class ServersService { @@ -86,4 +87,12 @@ export class ServersService { await this.natsService.sendServerCommand(licenseId, 'restart'); return { message: 'Restart command sent' }; } + + /** + * Deploy Rust server via companion agent + */ + async deployServer(licenseId: string, dto: DeployServerDto) { + await this.natsService.sendDeployCommand(licenseId, { ...dto }); + return { message: 'Deployment started' }; + } } diff --git a/backend-nest/src/services/nats-bridge.service.ts b/backend-nest/src/services/nats-bridge.service.ts index b683b08..babfe78 100644 --- a/backend-nest/src/services/nats-bridge.service.ts +++ b/backend-nest/src/services/nats-bridge.service.ts @@ -34,6 +34,11 @@ export class NatsBridgeService implements OnModuleInit { this.emit(licenseId, 'server_status', data); }); + this.nats.subscribe('corrosion.*.deploy.status', (data, subject) => { + const licenseId = subject.split('.')[1]; + this.emit(licenseId, 'deploy_status', data); + }); + this.logger.log('NATS bridge subscriptions initialized'); } diff --git a/backend-nest/src/services/nats.service.ts b/backend-nest/src/services/nats.service.ts index 075cdf4..5c1db54 100644 --- a/backend-nest/src/services/nats.service.ts +++ b/backend-nest/src/services/nats.service.ts @@ -70,4 +70,13 @@ export class NatsService implements OnModuleInit, OnModuleDestroy { timestamp: new Date().toISOString(), }); } + + /** Publish a deploy command to a specific license's companion agent */ + async sendDeployCommand(licenseId: string, config: Record): Promise { + await this.publish(`corrosion.${licenseId}.cmd.deploy`, { + action: 'deploy', + config, + timestamp: new Date().toISOString(), + }); + } }