From 048f39246eb771b081da0e7eab276254d234753d Mon Sep 17 00:00:00 2001 From: chyoo Date: Mon, 6 Jul 2026 10:25:02 +0900 Subject: [PATCH] manage launcher backend runtime env --- src/pages/launcher/LauncherComponentsPage.tsx | 150 +++++++++++++++++- src/pages/launcher/launcher-api.ts | 35 ++++ 2 files changed, 184 insertions(+), 1 deletion(-) diff --git a/src/pages/launcher/LauncherComponentsPage.tsx b/src/pages/launcher/LauncherComponentsPage.tsx index 80d8132..cd4757b 100644 --- a/src/pages/launcher/LauncherComponentsPage.tsx +++ b/src/pages/launcher/LauncherComponentsPage.tsx @@ -1,4 +1,4 @@ -import { Database, FileArchive, RotateCcw, Server, TerminalSquare } from 'lucide-react'; +import { Database, FileArchive, Plus, RotateCcw, Server, Settings, TerminalSquare, Trash2 } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { Badge, Button, Card, CardHeader, Modal } from '../../components/ui'; import { useToast } from '../../components/toast'; @@ -11,7 +11,9 @@ import { listLauncherVersions, loadLauncherComponentLogs, loadLauncherRuntimeResources, + loadRuntimeWorkloadEnv, restartLauncherComponent, + saveRuntimeWorkloadEnv, type LauncherBuildJob, type LauncherVersion } from './launcher-api'; @@ -119,6 +121,10 @@ export function LauncherComponentsPage() { const [logTarget, setLogTarget] = useState(null); const [logs, setLogs] = useState(''); const [logLoading, setLogLoading] = useState(false); + const [envTarget, setEnvTarget] = useState(null); + const [envVariables, setEnvVariables] = useState>([]); + const [envLoading, setEnvLoading] = useState(false); + const [envSaving, setEnvSaving] = useState(false); const loadingVisible = useMinimumLoading(loading || logLoading); const activeVersion = useMemo(() => versions.find((version) => version.isActive) ?? versions[0], [versions]); @@ -171,6 +177,54 @@ export function LauncherComponentsPage() { .finally(() => setActionKey('')); }; + const openEnv = (component: LauncherComponentDefinition) => { + if (!component.kind || !component.workloadName) return; + setEnvTarget(component); + setEnvVariables([]); + setEnvLoading(true); + loadRuntimeWorkloadEnv(LAUNCHER_NAMESPACE, apiKind(component.kind), component.workloadName) + .then((result) => { + setEnvVariables(result.variables ?? []); + }) + .catch((error: Error) => { + toast({ type: 'error', title: '환경변수 조회 실패', message: error.message }); + }) + .finally(() => setEnvLoading(false)); + }; + + const updateEnvVariable = (index: number, patch: Partial<{ key: string; value: string }>) => { + setEnvVariables((prev) => prev.map((variable, variableIndex) => ( + variableIndex === index ? { ...variable, ...patch } : variable + ))); + }; + + const removeEnvVariable = (index: number) => { + setEnvVariables((prev) => prev.filter((_, variableIndex) => variableIndex !== index)); + }; + + const saveEnv = () => { + if (!envTarget?.kind || !envTarget.workloadName) return; + setEnvSaving(true); + saveRuntimeWorkloadEnv( + LAUNCHER_NAMESPACE, + apiKind(envTarget.kind), + envTarget.workloadName, + envVariables + .filter((variable) => variable.editable) + .map((variable) => ({ key: variable.key.trim(), value: variable.value })) + .filter((variable) => variable.key) + ) + .then((result) => { + setEnvVariables(result.variables ?? []); + toast({ type: 'success', title: '환경변수 저장', message: `${envTarget.name} 환경변수를 반영했습니다.` }); + refresh(); + }) + .catch((error: Error) => { + toast({ type: 'error', title: '환경변수 저장 실패', message: error.message }); + }) + .finally(() => setEnvSaving(false)); + }; + return ( <> 재시작 + {component.type === 'backend' ? ( + + ) : null} ) : null} @@ -334,6 +394,94 @@ export function LauncherComponentsPage() { + setEnvTarget(null)} + className="!max-w-4xl" + footer={ + <> + + + + } + > +
+

+ 저장하면 Kubernetes Deployment 환경변수가 변경되고 새 Pod로 롤아웃됩니다. Secret/ConfigMap에서 주입된 값은 여기서 직접 수정하지 않습니다. +

+
+ + + + + + + + + + + {envLoading ? ( + + + + ) : null} + {!envLoading && envVariables.length === 0 ? ( + + + + ) : null} + {!envLoading && envVariables.map((variable, index) => ( + + + + + + + ))} + +
KeyValueSource
환경변수를 불러오는 중입니다.
등록된 환경변수가 없습니다.
+ updateEnvVariable(index, { key: event.target.value })} + /> + + updateEnvVariable(index, { value: event.target.value })} + /> + + {variable.source} + + +
+
+ +
+
+ ); diff --git a/src/pages/launcher/launcher-api.ts b/src/pages/launcher/launcher-api.ts index 988293d..8c39b87 100644 --- a/src/pages/launcher/launcher-api.ts +++ b/src/pages/launcher/launcher-api.ts @@ -314,10 +314,45 @@ export function deleteLauncherCommonSetting(id: string) { return apiRequest<{ deleted: boolean }>(`/launcher/common-settings/${id}`, { method: 'DELETE' }); } + +export type RuntimeWorkloadEnvVariable = { + key: string; + value: string; + editable: boolean; + source: string; +}; + export function loadLauncherRuntimeResources(namespace = 'studio-system') { return apiRequest(`/apps/runtime-namespaces/${encodeURIComponent(namespace)}/resources`); } + +export function loadRuntimeWorkloadEnv(namespace: string, kind: string, name: string) { + return apiRequest<{ + namespace: string; + target: string; + container: string | null; + variables: RuntimeWorkloadEnvVariable[]; + }>(`/runtime/workloads/${encodeURIComponent(namespace)}/${encodeURIComponent(kind.toLowerCase())}/${encodeURIComponent(name)}/env`); +} + +export function saveRuntimeWorkloadEnv( + namespace: string, + kind: string, + name: string, + variables: Array<{ key: string; value: string }> +) { + return apiRequest<{ + namespace: string; + target: string; + container: string | null; + variables: RuntimeWorkloadEnvVariable[]; + }>(`/runtime/workloads/${encodeURIComponent(namespace)}/${encodeURIComponent(kind.toLowerCase())}/${encodeURIComponent(name)}/env`, { + method: 'PATCH', + body: JSON.stringify({ variables }) + }); +} + export function loadLauncherComponentLogs(namespace: string, kind: string, name: string, tail = 120) { return apiRequest( `/runtime/workloads/${encodeURIComponent(namespace)}/${encodeURIComponent(kind.toLowerCase())}/${encodeURIComponent(name)}/logs?tail=${tail}`