add runtime workload env management api
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-06 10:25:12 +09:00
parent 979ccac26d
commit a06d200eac
3 changed files with 133 additions and 1 deletions
@@ -0,0 +1,17 @@
import { Type } from 'class-transformer';
import { IsArray, IsString, ValidateNested } from 'class-validator';
class WorkloadEnvVariableDto {
@IsString()
key!: string;
@IsString()
value!: string;
}
export class UpdateWorkloadEnvDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => WorkloadEnvVariableDto)
variables!: WorkloadEnvVariableDto[];
}
+27 -1
View File
@@ -1,6 +1,7 @@
import { Body, Controller, Delete, Get, Param, Post, Query } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { ScaleWorkloadDto } from './dto/scale-workload.dto';
import { UpdateWorkloadEnvDto } from './dto/update-workload-env.dto';
import { RuntimeService } from './runtime.service';
@Controller('runtime')
@@ -22,6 +23,31 @@ export class RuntimeController {
return this.runtimeService.listPodMetrics(namespace);
}
@Get('workloads/:namespace/:kind/:name/env')
getWorkloadEnv(
@Param('namespace') namespace: string,
@Param('kind') kind: string,
@Param('name') name: string,
) {
return this.runtimeService.getWorkloadEnv(namespace, kind, name);
}
@Patch('workloads/:namespace/:kind/:name/env')
updateWorkloadEnv(
@Param('namespace') namespace: string,
@Param('kind') kind: string,
@Param('name') name: string,
@Body() body: UpdateWorkloadEnvDto,
) {
return this.runtimeService.updateWorkloadEnv(
namespace,
kind,
name,
body.variables,
);
}
@Post('workloads/:namespace/:kind/:name/restart')
restartWorkload(
@Param('namespace') namespace: string,
+89
View File
@@ -70,6 +70,65 @@ export class RuntimeService {
return parsePodMetrics(output, Boolean(namespace), namespace);
}
async getWorkloadEnv(namespace: string, kind: string, name: string) {
const normalizedKind = this.normalizeEnvWorkloadKind(kind);
const workload = await this.kubectlJson([
'get',
`${normalizedKind}/${name}`,
'-n',
namespace,
'-o',
'json',
]);
const container = workload.spec?.template?.spec?.containers?.[0];
const env = container?.env ?? [];
return {
namespace,
target: `${normalizedKind}/${name}`,
container: container?.name ?? null,
variables: env
.map((item: KubectlJson) => ({
key: item.name,
value: typeof item.value === 'string' ? item.value : '',
editable: typeof item.value === 'string' || item.value === undefined,
source: item.valueFrom ? 'valueFrom' : 'literal',
}))
.filter((item: { key?: string }) => item.key),
};
}
async updateWorkloadEnv(
namespace: string,
kind: string,
name: string,
variables: Array<{ key?: string; value?: string }>,
) {
const normalizedKind = this.normalizeEnvWorkloadKind(kind);
const current = await this.getWorkloadEnv(namespace, normalizedKind, name);
const nextVariables = this.normalizeEnvVariables(variables);
const nextKeys = new Set(nextVariables.map((variable) => variable.key));
const deleteArgs = current.variables
.filter((variable: { key: string; editable: boolean }) => variable.editable && !nextKeys.has(variable.key))
.map((variable: { key: string }) => `${variable.key}-`);
const setArgs = nextVariables.map((variable) => `${variable.key}=${variable.value}`);
if (deleteArgs.length || setArgs.length) {
await this.kubectl([
'set',
'env',
`${normalizedKind}/${name}`,
...deleteArgs,
...setArgs,
'-n',
namespace,
]);
}
return this.getWorkloadEnv(namespace, normalizedKind, name);
}
async restartWorkload(namespace: string, kind: string, name: string) {
await this.kubectl(['rollout', 'restart', `${kind}/${name}`, '-n', namespace]);
return {
@@ -323,6 +382,36 @@ export class RuntimeService {
}));
}
private normalizeEnvWorkloadKind(kind: string) {
const normalized = kind.toLowerCase();
if (!['deployment', 'statefulset', 'daemonset'].includes(normalized)) {
throw new BadRequestException({
error: 'unsupported_workload_kind',
message: '환경변수 관리는 Deployment, StatefulSet, DaemonSet만 지원합니다.',
});
}
return normalized;
}
private normalizeEnvVariables(variables: Array<{ key?: string; value?: string }>) {
const byKey = new Map<string, { key: string; value: string }>();
for (const variable of variables ?? []) {
const key = (variable.key ?? '').trim();
if (!key) continue;
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
throw new BadRequestException({
error: 'invalid_env_key',
message: `환경변수 키 형식이 올바르지 않습니다: ${key}`,
});
}
byKey.set(key, { key, value: variable.value ?? '' });
}
return [...byKey.values()].sort((left, right) => left.key.localeCompare(right.key));
}
private async kubectlJson(args: string[]) {
const output = await this.kubectl(args);
return JSON.parse(output) as KubectlJson;