This commit is contained in:
@@ -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[];
|
||||||
|
}
|
||||||
@@ -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 { ScaleWorkloadDto } from './dto/scale-workload.dto';
|
||||||
|
import { UpdateWorkloadEnvDto } from './dto/update-workload-env.dto';
|
||||||
import { RuntimeService } from './runtime.service';
|
import { RuntimeService } from './runtime.service';
|
||||||
|
|
||||||
@Controller('runtime')
|
@Controller('runtime')
|
||||||
@@ -22,6 +23,31 @@ export class RuntimeController {
|
|||||||
return this.runtimeService.listPodMetrics(namespace);
|
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')
|
@Post('workloads/:namespace/:kind/:name/restart')
|
||||||
restartWorkload(
|
restartWorkload(
|
||||||
@Param('namespace') namespace: string,
|
@Param('namespace') namespace: string,
|
||||||
|
|||||||
@@ -70,6 +70,65 @@ export class RuntimeService {
|
|||||||
return parsePodMetrics(output, Boolean(namespace), namespace);
|
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) {
|
async restartWorkload(namespace: string, kind: string, name: string) {
|
||||||
await this.kubectl(['rollout', 'restart', `${kind}/${name}`, '-n', namespace]);
|
await this.kubectl(['rollout', 'restart', `${kind}/${name}`, '-n', namespace]);
|
||||||
return {
|
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[]) {
|
private async kubectlJson(args: string[]) {
|
||||||
const output = await this.kubectl(args);
|
const output = await this.kubectl(args);
|
||||||
return JSON.parse(output) as KubectlJson;
|
return JSON.parse(output) as KubectlJson;
|
||||||
|
|||||||
Reference in New Issue
Block a user