add launcher component secret setting support
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-08 15:24:08 +09:00
parent 403d3ac0ed
commit c89210ac6a
3 changed files with 32 additions and 21 deletions
+27 -14
View File
@@ -122,7 +122,7 @@ export function LauncherComponentsPage() {
const [logs, setLogs] = useState(''); const [logs, setLogs] = useState('');
const [logLoading, setLogLoading] = useState(false); const [logLoading, setLogLoading] = useState(false);
const [envTarget, setEnvTarget] = useState<LauncherComponentDefinition | null>(null); const [envTarget, setEnvTarget] = useState<LauncherComponentDefinition | null>(null);
const [envVariables, setEnvVariables] = useState<Array<{ key: string; value: string; editable: boolean; source: string }>>([]); const [envVariables, setEnvVariables] = useState<Array<{ key: string; value: string; isSecret: boolean; editable: boolean; source: string }>>([]);
const [envLoading, setEnvLoading] = useState(false); const [envLoading, setEnvLoading] = useState(false);
const [envSaving, setEnvSaving] = useState(false); const [envSaving, setEnvSaving] = useState(false);
const loadingVisible = useMinimumLoading(loading || logLoading); const loadingVisible = useMinimumLoading(loading || logLoading);
@@ -187,12 +187,12 @@ export function LauncherComponentsPage() {
setEnvVariables(result.variables ?? []); setEnvVariables(result.variables ?? []);
}) })
.catch((error: Error) => { .catch((error: Error) => {
toast({ type: 'error', title: '환경변수 조회 실패', message: error.message }); toast({ type: 'error', title: '설정값 조회 실패', message: error.message });
}) })
.finally(() => setEnvLoading(false)); .finally(() => setEnvLoading(false));
}; };
const updateEnvVariable = (index: number, patch: Partial<{ key: string; value: string }>) => { const updateEnvVariable = (index: number, patch: Partial<{ key: string; value: string; isSecret: boolean }>) => {
setEnvVariables((prev) => prev.map((variable, variableIndex) => ( setEnvVariables((prev) => prev.map((variable, variableIndex) => (
variableIndex === index ? { ...variable, ...patch } : variable variableIndex === index ? { ...variable, ...patch } : variable
))); )));
@@ -211,16 +211,16 @@ export function LauncherComponentsPage() {
envTarget.workloadName, envTarget.workloadName,
envVariables envVariables
.filter((variable) => variable.editable) .filter((variable) => variable.editable)
.map((variable) => ({ key: variable.key.trim(), value: variable.value })) .map((variable) => ({ key: variable.key.trim(), value: variable.value, isSecret: variable.isSecret }))
.filter((variable) => variable.key) .filter((variable) => variable.key)
) )
.then((result) => { .then((result) => {
setEnvVariables(result.variables ?? []); setEnvVariables(result.variables ?? []);
toast({ type: 'success', title: '환경변수 저장', message: `${envTarget.name} 환경변수를 반영했습니다.` }); toast({ type: 'success', title: '설정값 저장', message: `${envTarget.name} 설정값을 반영했습니다.` });
refresh(); refresh();
}) })
.catch((error: Error) => { .catch((error: Error) => {
toast({ type: 'error', title: '환경변수 저장 실패', message: error.message }); toast({ type: 'error', title: '설정값 저장 실패', message: error.message });
}) })
.finally(() => setEnvSaving(false)); .finally(() => setEnvSaving(false));
}; };
@@ -329,7 +329,7 @@ export function LauncherComponentsPage() {
{component.type === 'backend' ? ( {component.type === 'backend' ? (
<Button variant="outline" onClick={() => openEnv(component)} disabled={!workload}> <Button variant="outline" onClick={() => openEnv(component)} disabled={!workload}>
<Settings className="size-4" /> <Settings className="size-4" />
환경변수 설정값
</Button> </Button>
) : null} ) : null}
</div> </div>
@@ -396,7 +396,7 @@ export function LauncherComponentsPage() {
<Modal <Modal
open={Boolean(envTarget)} open={Boolean(envTarget)}
title={envTarget ? `${envTarget.name} 환경변수` : '환경변수'} title={envTarget ? `${envTarget.name} 설정값` : '설정값'}
onClose={() => setEnvTarget(null)} onClose={() => setEnvTarget(null)}
className="!max-w-4xl" className="!max-w-4xl"
footer={ footer={
@@ -410,14 +410,15 @@ export function LauncherComponentsPage() {
> >
<div className="space-y-4"> <div className="space-y-4">
<p className="text-xs leading-5 text-muted-foreground"> <p className="text-xs leading-5 text-muted-foreground">
저장하면 Kubernetes Deployment 환경변수가 변경되고 새 Pod로 롤아웃됩니다. Secret/ConfigMap에서 주입된 값은 여기서 직접 수정하지 않습니다. Secret을 체크하면 Kubernetes Secret에 저장하고 secretKeyRef로 연결합니다. 체크하지 않은 값은 Deployment 환경변수로 저장합니다.
</p> </p>
<div className="overflow-x-auto rounded-md border border-border"> <div className="overflow-x-auto rounded-md border border-border">
<table className="w-full min-w-[720px] text-left text-sm"> <table className="w-full min-w-[820px] text-left text-sm">
<thead className="border-b border-border bg-muted/30 text-xs text-muted-foreground"> <thead className="border-b border-border bg-muted/30 text-xs text-muted-foreground">
<tr> <tr>
<th className="px-4 py-3 font-medium">Key</th> <th className="px-4 py-3 font-medium">Key</th>
<th className="px-4 py-3 font-medium">Value</th> <th className="px-4 py-3 font-medium">Value</th>
<th className="w-24 px-4 py-3 font-medium">Secret</th>
<th className="w-24 px-4 py-3 font-medium">Source</th> <th className="w-24 px-4 py-3 font-medium">Source</th>
<th className="w-16 px-4 py-3 font-medium"></th> <th className="w-16 px-4 py-3 font-medium"></th>
</tr> </tr>
@@ -425,12 +426,12 @@ export function LauncherComponentsPage() {
<tbody className="divide-y divide-border"> <tbody className="divide-y divide-border">
{envLoading ? ( {envLoading ? (
<tr> <tr>
<td colSpan={4} className="px-4 py-8 text-center text-muted-foreground">환경변수를 불러오는 중입니다.</td> <td colSpan={5} className="px-4 py-8 text-center text-muted-foreground">설정값을 불러오는 중입니다.</td>
</tr> </tr>
) : null} ) : null}
{!envLoading && envVariables.length === 0 ? ( {!envLoading && envVariables.length === 0 ? (
<tr> <tr>
<td colSpan={4} className="px-4 py-8 text-center text-muted-foreground">등록된 환경변수가 없습니다.</td> <td colSpan={5} className="px-4 py-8 text-center text-muted-foreground">등록된 설정값이 없습니다.</td>
</tr> </tr>
) : null} ) : null}
{!envLoading && envVariables.map((variable, index) => ( {!envLoading && envVariables.map((variable, index) => (
@@ -451,6 +452,18 @@ export function LauncherComponentsPage() {
onChange={(event) => updateEnvVariable(index, { value: event.target.value })} onChange={(event) => updateEnvVariable(index, { value: event.target.value })}
/> />
</td> </td>
<td className="px-4 py-3">
<label className="inline-flex items-center gap-2 text-xs text-muted-foreground">
<input
type="checkbox"
className="size-4 accent-primary"
checked={variable.isSecret}
disabled={!variable.editable}
onChange={(event) => updateEnvVariable(index, { isSecret: event.target.checked })}
/>
Secret
</label>
</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<Badge tone={variable.editable ? 'blue' : 'neutral'}>{variable.source}</Badge> <Badge tone={variable.editable ? 'blue' : 'neutral'}>{variable.source}</Badge>
</td> </td>
@@ -473,11 +486,11 @@ export function LauncherComponentsPage() {
</div> </div>
<Button <Button
variant="outline" variant="outline"
onClick={() => setEnvVariables((prev) => [...prev, { key: '', value: '', editable: true, source: 'literal' }])} onClick={() => setEnvVariables((prev) => [...prev, { key: '', value: '', isSecret: false, editable: true, source: 'literal' }])}
disabled={envLoading} disabled={envLoading}
> >
<Plus className="size-4" /> <Plus className="size-4" />
환경변수 추가 설정값 추가
</Button> </Button>
</div> </div>
</Modal> </Modal>
+2 -1
View File
@@ -425,6 +425,7 @@ export function deleteLauncherCommonSetting(id: string) {
export type RuntimeWorkloadEnvVariable = { export type RuntimeWorkloadEnvVariable = {
key: string; key: string;
value: string; value: string;
isSecret: boolean;
editable: boolean; editable: boolean;
source: string; source: string;
}; };
@@ -447,7 +448,7 @@ export function saveRuntimeWorkloadEnv(
namespace: string, namespace: string,
kind: string, kind: string,
name: string, name: string,
variables: Array<{ key: string; value: string }> variables: Array<{ key: string; value: string; isSecret: boolean }>
) { ) {
return apiRequest<{ return apiRequest<{
namespace: string; namespace: string;
+3 -6
View File
@@ -466,9 +466,8 @@ export function RuntimeConfigPage() {
<table className="w-full table-fixed text-left text-sm"> <table className="w-full table-fixed text-left text-sm">
<colgroup> <colgroup>
<col className="w-[22%]" /> <col className="w-[22%]" />
<col className="w-[29%]" /> <col className="w-[30%]" />
<col className="w-[19%]" /> <col />
<col className="w-[24%]" />
<col className="w-20" /> <col className="w-20" />
</colgroup> </colgroup>
<thead className="sticky top-0 z-10 border-b border-border bg-card text-xs text-muted-foreground"> <thead className="sticky top-0 z-10 border-b border-border bg-card text-xs text-muted-foreground">
@@ -476,13 +475,12 @@ export function RuntimeConfigPage() {
<th className="px-5 py-3 font-medium">구성요소</th> <th className="px-5 py-3 font-medium">구성요소</th>
<th className="px-5 py-3 font-medium">Key</th> <th className="px-5 py-3 font-medium">Key</th>
<th className="px-5 py-3 font-medium">Value</th> <th className="px-5 py-3 font-medium">Value</th>
<th className="px-5 py-3 font-medium">등록일</th>
<th className="w-16 px-5 py-3 text-right font-medium">관리</th> <th className="w-16 px-5 py-3 text-right font-medium">관리</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-border"> <tbody className="divide-y divide-border">
{variables.length === 0 ? ( {variables.length === 0 ? (
<tr><td colSpan={5} className="px-5 py-14 text-center text-muted-foreground">등록된 설정값이 없습니다.</td></tr> <tr><td colSpan={4} className="px-5 py-14 text-center text-muted-foreground">등록된 설정값이 없습니다.</td></tr>
) : null} ) : null}
{variables.map((variable) => ( {variables.map((variable) => (
<tr key={variable.id} className="hover:bg-muted/20"> <tr key={variable.id} className="hover:bg-muted/20">
@@ -494,7 +492,6 @@ export function RuntimeConfigPage() {
</td> </td>
<td className="break-all px-4 py-3 font-mono text-xs font-medium text-foreground">{variable.key}</td> <td className="break-all px-4 py-3 font-mono text-xs font-medium text-foreground">{variable.key}</td>
<td className="break-all px-4 py-3 font-mono text-xs text-muted-foreground">{variable.isSecret ? '********' : variable.value}</td> <td className="break-all px-4 py-3 font-mono text-xs text-muted-foreground">{variable.isSecret ? '********' : variable.value}</td>
<td className="px-4 py-3 text-xs text-muted-foreground">{formatDate(variable.createdAt)}</td>
<td className="px-3 py-3 text-right"> <td className="px-3 py-3 text-right">
<Button variant="ghost" className="h-8 w-8 p-0 text-rose-300 hover:text-rose-200" title="삭제" aria-label={`${variable.key} 삭제`} onClick={() => removeVariable(variable.id)} disabled={Boolean(actionKey)}> <Button variant="ghost" className="h-8 w-8 p-0 text-rose-300 hover:text-rose-200" title="삭제" aria-label={`${variable.key} 삭제`} onClick={() => removeVariable(variable.id)} disabled={Boolean(actionKey)}>
<Trash2 className="size-4" /> <Trash2 className="size-4" />