merge develop into release
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-13 16:55:44 +09:00
8 changed files with 184 additions and 60 deletions
-24
View File
@@ -60,10 +60,6 @@ import {
AgentInstallStatusPage as AgentInstallStatusRestoredPage, AgentInstallStatusPage as AgentInstallStatusRestoredPage,
AgentRegistrationPage as AgentRegistrationRestoredPage, AgentRegistrationPage as AgentRegistrationRestoredPage,
AgentSshKeysPage as AgentSshKeysRestoredPage, AgentSshKeysPage as AgentSshKeysRestoredPage,
AgentWorkspacePage as AgentWorkspaceRestoredPage,
GatewayAgentsPage as GatewayAgentsRestoredPage,
GatewayDevicesPage as GatewayDevicesRestoredPage,
GatewayStatusPage as GatewayStatusRestoredPage,
TailscaleAclPage as TailscaleAclRestoredPage, TailscaleAclPage as TailscaleAclRestoredPage,
TailscaleRoutesPage as TailscaleRoutesRestoredPage, TailscaleRoutesPage as TailscaleRoutesRestoredPage,
TailscaleStatusPage as TailscaleStatusRestoredPage TailscaleStatusPage as TailscaleStatusRestoredPage
@@ -90,9 +86,6 @@ type ViewKey =
| 'appReleases' | 'appReleases'
| 'appDeployments' | 'appDeployments'
| 'appMigrations' | 'appMigrations'
| 'gatewayStatus'
| 'gatewayAgents'
| 'gatewayDevices'
| 'tailscaleStatus' | 'tailscaleStatus'
| 'tailscaleRoutes' | 'tailscaleRoutes'
| 'tailscaleAcl' | 'tailscaleAcl'
@@ -104,7 +97,6 @@ type ViewKey =
| 'agentRegistration' | 'agentRegistration'
| 'agentInstallStatus' | 'agentInstallStatus'
| 'agentSshKeys' | 'agentSshKeys'
| 'agentWorkspace'
| 'agentCommands' | 'agentCommands'
| 'runtimeConfig' | 'runtimeConfig'
| 'runtimeLogs' | 'runtimeLogs'
@@ -129,9 +121,6 @@ const viewByLabel: Record<string, ViewKey> = {
'실행 설정': 'runtimeConfig', '실행 설정': 'runtimeConfig',
'배포 관리': 'appDeployments', '배포 관리': 'appDeployments',
마이그레이션: 'appMigrations', 마이그레이션: 'appMigrations',
'연결 상태': 'gatewayStatus',
'Agent 관리': 'gatewayAgents',
'장비 동기화': 'gatewayDevices',
'노드 현황': 'tailscaleStatus', '노드 현황': 'tailscaleStatus',
라우트: 'tailscaleRoutes', 라우트: 'tailscaleRoutes',
ACL: 'tailscaleAcl', ACL: 'tailscaleAcl',
@@ -143,7 +132,6 @@ const viewByLabel: Record<string, ViewKey> = {
'장비 등록': 'agentRegistration', '장비 등록': 'agentRegistration',
'설치 상태': 'agentInstallStatus', '설치 상태': 'agentInstallStatus',
'SSH 키 배포': 'agentSshKeys', 'SSH 키 배포': 'agentSshKeys',
'Workspace 설정': 'agentWorkspace',
'원격 명령': 'agentCommands', '원격 명령': 'agentCommands',
'로그/이벤트': 'runtimeLogs', '로그/이벤트': 'runtimeLogs',
'장비 제어': 'runtimeDeviceControl', '장비 제어': 'runtimeDeviceControl',
@@ -174,9 +162,6 @@ const pathByView: Record<ViewKey, string> = {
appReleases: '/applications/releases', appReleases: '/applications/releases',
appDeployments: '/applications/deployments', appDeployments: '/applications/deployments',
appMigrations: '/applications/migrations', appMigrations: '/applications/migrations',
gatewayStatus: '/gateway/status',
gatewayAgents: '/gateway/agents',
gatewayDevices: '/gateway/devices',
tailscaleStatus: '/tailscale/status', tailscaleStatus: '/tailscale/status',
tailscaleRoutes: '/tailscale/routes', tailscaleRoutes: '/tailscale/routes',
tailscaleAcl: '/tailscale/acl', tailscaleAcl: '/tailscale/acl',
@@ -188,7 +173,6 @@ const pathByView: Record<ViewKey, string> = {
agentRegistration: '/agents/registration', agentRegistration: '/agents/registration',
agentInstallStatus: '/agents/install-status', agentInstallStatus: '/agents/install-status',
agentSshKeys: '/agents/ssh-keys', agentSshKeys: '/agents/ssh-keys',
agentWorkspace: '/agents/workspace',
agentCommands: '/agents/commands', agentCommands: '/agents/commands',
runtimeConfig: '/applications/runtime-settings', runtimeConfig: '/applications/runtime-settings',
runtimeLogs: '/runtime/logs', runtimeLogs: '/runtime/logs',
@@ -2325,12 +2309,6 @@ function App() {
return <DeploymentManagementPage />; return <DeploymentManagementPage />;
case 'appMigrations': case 'appMigrations':
return <MigrationManagementPage />; return <MigrationManagementPage />;
case 'gatewayStatus':
return <GatewayStatusRestoredPage />;
case 'gatewayAgents':
return <GatewayAgentsRestoredPage />;
case 'gatewayDevices':
return <GatewayDevicesRestoredPage />;
case 'tailscaleStatus': case 'tailscaleStatus':
return <TailscaleStatusRestoredPage />; return <TailscaleStatusRestoredPage />;
case 'tailscaleRoutes': case 'tailscaleRoutes':
@@ -2353,8 +2331,6 @@ function App() {
return <AgentInstallStatusRestoredPage />; return <AgentInstallStatusRestoredPage />;
case 'agentSshKeys': case 'agentSshKeys':
return <AgentSshKeysRestoredPage />; return <AgentSshKeysRestoredPage />;
case 'agentWorkspace':
return <AgentWorkspaceRestoredPage />;
case 'agentCommands': case 'agentCommands':
return <AgentCommandsRestoredPage />; return <AgentCommandsRestoredPage />;
case 'runtimeConfig': case 'runtimeConfig':
-10
View File
@@ -75,19 +75,9 @@ export const navGroups = [
{ label: '장비 제어' }, { label: '장비 제어' },
{ label: '설치 상태' }, { label: '설치 상태' },
{ label: 'SSH 키 배포' }, { label: 'SSH 키 배포' },
{ label: 'Workspace 설정' },
{ label: '원격 명령' } { label: '원격 명령' }
] ]
}, },
{
label: '플랫폼 게이트웨이',
icon: ServerCog,
children: [
{ label: '연결 상태' },
{ label: 'Agent 관리' },
{ label: '장비 동기화' }
]
},
{ {
label: '인프라', label: '인프라',
icon: HardDrive, icon: HardDrive,
-11
View File
@@ -7,7 +7,6 @@ import {
Megaphone, Megaphone,
Network, Network,
Rocket, Rocket,
ServerCog,
ShieldCheck, ShieldCheck,
Users Users
} from 'lucide-react'; } from 'lucide-react';
@@ -80,19 +79,9 @@ export const navGroups: NavGroup[] = [
{ label: '장비 제어', path: '/runtime/devices' }, { label: '장비 제어', path: '/runtime/devices' },
{ label: '설치 상태', path: '/agents/install-status' }, { label: '설치 상태', path: '/agents/install-status' },
{ label: 'SSH 키 배포', path: '/agents/ssh-keys' }, { label: 'SSH 키 배포', path: '/agents/ssh-keys' },
{ label: 'Workspace 설정', path: '/agents/workspace' },
{ label: '원격 명령', path: '/agents/commands' } { label: '원격 명령', path: '/agents/commands' }
] ]
}, },
{
label: '플랫폼 게이트웨이',
icon: ServerCog,
children: [
{ label: '연결 상태', path: '/gateway/status' },
{ label: 'Agent 관리', path: '/gateway/agents' },
{ label: '장비 동기화', path: '/gateway/devices' }
]
},
{ {
label: '인프라', label: '인프라',
icon: HardDrive, icon: HardDrive,
-9
View File
@@ -879,15 +879,6 @@ export function AgentSshKeysPage() {
); );
} }
export function AgentWorkspacePage() {
return (
<>
<PageTitle eyebrow="Agent" title="Workspace 설정" description="장비별 작업 디렉터리와 동기화 정책을 관리합니다." />
<AgentPlaceholder title="Workspace 설정" description="계정별 workspace 경로와 동기화 상태입니다." />
</>
);
}
export function AgentCommandsPage() { export function AgentCommandsPage() {
const columns = useMemo<Array<DataTableColumn<(typeof runtimeCommands)[number]>>>( const columns = useMemo<Array<DataTableColumn<(typeof runtimeCommands)[number]>>>(
() => [ () => [
@@ -77,6 +77,11 @@ export function DeploymentManagementPage() {
const [releases, setReleases] = useState<AppRelease[]>([]); const [releases, setReleases] = useState<AppRelease[]>([]);
const [releaseId, setReleaseId] = useState(''); const [releaseId, setReleaseId] = useState('');
const [namespace, setNamespace] = useState(''); const [namespace, setNamespace] = useState('');
const [replicas, setReplicas] = useState('1');
const [cpuRequest, setCpuRequest] = useState('100m');
const [cpuLimit, setCpuLimit] = useState('500m');
const [memoryRequest, setMemoryRequest] = useState('128Mi');
const [memoryLimit, setMemoryLimit] = useState('256Mi');
const [updateLauncherAppVersion, setUpdateLauncherAppVersion] = useState(true); const [updateLauncherAppVersion, setUpdateLauncherAppVersion] = useState(true);
const [deployModalOpen, setDeployModalOpen] = useState(false); const [deployModalOpen, setDeployModalOpen] = useState(false);
const [selectedDeployment, setSelectedDeployment] = useState<DeploymentJob | null>(null); const [selectedDeployment, setSelectedDeployment] = useState<DeploymentJob | null>(null);
@@ -158,6 +163,12 @@ export function DeploymentManagementPage() {
setReleaseId(release.id); setReleaseId(release.id);
setNamespace(release.app.namespace || ''); setNamespace(release.app.namespace || '');
} }
if (!release && !releaseId && !releases[0]?.app?.namespace) setNamespace('');
setReplicas('1');
setCpuRequest('100m');
setCpuLimit('500m');
setMemoryRequest('128Mi');
setMemoryLimit('256Mi');
setUpdateLauncherAppVersion(true); setUpdateLauncherAppVersion(true);
setDeployModalOpen(true); setDeployModalOpen(true);
}; };
@@ -165,8 +176,22 @@ export function DeploymentManagementPage() {
const deploy = async () => { const deploy = async () => {
if (deploying) return; if (deploying) return;
setDeploying(true); setDeploying(true);
const requestedReplicas = Number.parseInt(replicas, 10);
const resources = {
...(Number.isFinite(requestedReplicas) && requestedReplicas > 0 ? { replicas: requestedReplicas } : {}),
...(cpuRequest.trim() ? { cpuRequest: cpuRequest.trim() } : {}),
...(cpuLimit.trim() ? { cpuLimit: cpuLimit.trim() } : {}),
...(memoryRequest.trim() ? { memoryRequest: memoryRequest.trim() } : {}),
...(memoryLimit.trim() ? { memoryLimit: memoryLimit.trim() } : {})
};
try { try {
const result = await createDeployment({ releaseId, environment: 'prod', namespace, updateLauncherAppVersion }); const result = await createDeployment({
releaseId,
environment: 'prod',
namespace,
updateLauncherAppVersion,
resources: Object.keys(resources).length ? resources : undefined
});
toast({ type: 'success', message: '배포 작업이 실행되었습니다.' }); toast({ type: 'success', message: '배포 작업이 실행되었습니다.' });
setSelectedDeployment(result.deploymentJob); setSelectedDeployment(result.deploymentJob);
setDeployModalOpen(false); setDeployModalOpen(false);
@@ -357,6 +382,60 @@ export function DeploymentManagementPage() {
{deployableReleases.map((release) => <option key={release.id} value={release.id}>{release.app.name} {release.version}</option>)} {deployableReleases.map((release) => <option key={release.id} value={release.id}>{release.app.name} {release.version}</option>)}
</select> </select>
<input className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={namespace} disabled={deploying} onChange={(event) => setNamespace(event.target.value)} placeholder="stdo-lms" /> <input className="h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none" value={namespace} disabled={deploying} onChange={(event) => setNamespace(event.target.value)} placeholder="stdo-lms" />
<div className="grid gap-3 md:grid-cols-2">
<label className="block">
<span className="text-xs font-medium text-muted-foreground">Replicas</span>
<input
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none"
value={replicas}
disabled={deploying}
onChange={(event) => setReplicas(event.target.value)}
placeholder="1"
/>
</label>
<label className="block">
<span className="text-xs font-medium text-muted-foreground">CPU 요청</span>
<input
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none"
value={cpuRequest}
disabled={deploying}
onChange={(event) => setCpuRequest(event.target.value)}
placeholder="예: 100m"
/>
</label>
</div>
<div className="grid gap-3 md:grid-cols-2">
<label className="block">
<span className="text-xs font-medium text-muted-foreground">CPU 상한</span>
<input
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none"
value={cpuLimit}
disabled={deploying}
onChange={(event) => setCpuLimit(event.target.value)}
placeholder="예: 500m"
/>
</label>
<label className="block">
<span className="text-xs font-medium text-muted-foreground">메모리 요청</span>
<input
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none"
value={memoryRequest}
disabled={deploying}
onChange={(event) => setMemoryRequest(event.target.value)}
placeholder="예: 256Mi"
/>
</label>
</div>
<label className="block">
<span className="text-xs font-medium text-muted-foreground">메모리 상한</span>
<input
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none"
value={memoryLimit}
disabled={deploying}
onChange={(event) => setMemoryLimit(event.target.value)}
placeholder="예: 512Mi"
/>
</label>
<label className="flex items-center gap-2 rounded-md border border-border bg-muted/20 px-3 py-2 text-sm text-foreground"> <label className="flex items-center gap-2 rounded-md border border-border bg-muted/20 px-3 py-2 text-sm text-foreground">
<input <input
type="checkbox" type="checkbox"
+15 -1
View File
@@ -12,6 +12,14 @@ import type {
} from '../../types/application'; } from '../../types/application';
import type { ApiAppComponent } from '../../types/runtime'; import type { ApiAppComponent } from '../../types/runtime';
export type DeploymentResourceLimits = {
replicas?: number;
cpuRequest?: string;
cpuLimit?: string;
memoryRequest?: string;
memoryLimit?: string;
};
export type GiteaCommit = { export type GiteaCommit = {
sha: string; sha: string;
commit?: { commit?: {
@@ -230,7 +238,13 @@ export function listDeploymentsPage(options: { appKey?: string; page: number; li
return apiRequest<{ deployments: DeploymentJob[]; total: number; page: number; limit: number; deployedReleaseIds?: string[] }>(`/apps/deployments?${params.toString()}`); return apiRequest<{ deployments: DeploymentJob[]; total: number; page: number; limit: number; deployedReleaseIds?: string[] }>(`/apps/deployments?${params.toString()}`);
} }
export function createDeployment(input: { releaseId: string; environment: string; namespace: string; updateLauncherAppVersion?: boolean }) { export function createDeployment(input: {
releaseId: string;
environment: string;
namespace: string;
updateLauncherAppVersion?: boolean;
resources?: DeploymentResourceLimits;
}) {
return apiRequest<{ deploymentJob: DeploymentJob }>('/apps/deployments', { return apiRequest<{ deploymentJob: DeploymentJob }>('/apps/deployments', {
method: 'POST', method: 'POST',
body: JSON.stringify(input) body: JSON.stringify(input)
+79 -2
View File
@@ -36,6 +36,11 @@ export function LauncherVersionsPage() {
const [deployOpen, setDeployOpen] = useState(false); const [deployOpen, setDeployOpen] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [deploying, setDeploying] = useState(false); const [deploying, setDeploying] = useState(false);
const [deployReplicas, setDeployReplicas] = useState('1');
const [deployCpuRequest, setDeployCpuRequest] = useState('100m');
const [deployCpuLimit, setDeployCpuLimit] = useState('500m');
const [deployMemoryRequest, setDeployMemoryRequest] = useState('128Mi');
const [deployMemoryLimit, setDeployMemoryLimit] = useState('256Mi');
const [releaseForm, setReleaseForm] = useState({ const [releaseForm, setReleaseForm] = useState({
buildJobId: '', buildJobId: '',
version: '', version: '',
@@ -128,7 +133,16 @@ export function LauncherVersionsPage() {
const deployBackend = () => { const deployBackend = () => {
if (!latestBackendBuild?.downloadUrl) return; if (!latestBackendBuild?.downloadUrl) return;
setDeploying(true); setDeploying(true);
deployLauncherBackend(latestBackendBuild.downloadUrl) const requestedReplicas = Number.parseInt(deployReplicas, 10);
const resources = {
...(Number.isFinite(requestedReplicas) && requestedReplicas > 0 ? { replicas: requestedReplicas } : {}),
...(deployCpuRequest.trim() ? { cpuRequest: deployCpuRequest.trim() } : {}),
...(deployCpuLimit.trim() ? { cpuLimit: deployCpuLimit.trim() } : {}),
...(deployMemoryRequest.trim() ? { memoryRequest: deployMemoryRequest.trim() } : {}),
...(deployMemoryLimit.trim() ? { memoryLimit: deployMemoryLimit.trim() } : {})
};
deployLauncherBackend(latestBackendBuild.downloadUrl, Object.keys(resources).length ? resources : undefined)
.then((result) => { .then((result) => {
setDeployResult(result); setDeployResult(result);
toast({ type: 'success', message: '런처 백엔드를 배포했습니다.' }); toast({ type: 'success', message: '런처 백엔드를 배포했습니다.' });
@@ -141,6 +155,15 @@ export function LauncherVersionsPage() {
.finally(() => setDeploying(false)); .finally(() => setDeploying(false));
}; };
const openBackendDeploy = () => {
setDeployReplicas('1');
setDeployCpuRequest('100m');
setDeployCpuLimit('500m');
setDeployMemoryRequest('128Mi');
setDeployMemoryLimit('256Mi');
setDeployOpen(true);
};
return ( return (
<> <>
<PageTitle <PageTitle
@@ -159,7 +182,7 @@ export function LauncherVersionsPage() {
릴리즈 등록 릴리즈 등록
</Button> </Button>
) : ( ) : (
<Button className="h-8 px-3 text-[13px]" onClick={() => setDeployOpen(true)} disabled={!latestBackendBuild?.downloadUrl}> <Button className="h-8 px-3 text-[13px]" onClick={openBackendDeploy} disabled={!latestBackendBuild?.downloadUrl}>
<Play className="size-4" /> <Play className="size-4" />
백엔드 배포 백엔드 배포
</Button> </Button>
@@ -345,6 +368,60 @@ export function LauncherVersionsPage() {
<p className="font-medium">최신 성공 빌드 이미지로 런처 백엔드를 배포합니다.</p> <p className="font-medium">최신 성공 빌드 이미지로 런처 백엔드를 배포합니다.</p>
<p className="mt-1 text-xs leading-5 text-muted-foreground">Deployment 이미지가 교체되고 rollout 완료까지 대기합니다.</p> <p className="mt-1 text-xs leading-5 text-muted-foreground">Deployment 이미지가 교체되고 rollout 완료까지 대기합니다.</p>
</div> </div>
<div className="grid gap-3 md:grid-cols-2">
<label className="block">
<span className="text-xs font-medium text-muted-foreground">Replicas</span>
<input
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none"
value={deployReplicas}
disabled={deploying}
onChange={(event) => setDeployReplicas(event.target.value)}
placeholder="1"
/>
</label>
<label className="block">
<span className="text-xs font-medium text-muted-foreground">CPU 요청</span>
<input
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none"
value={deployCpuRequest}
disabled={deploying}
onChange={(event) => setDeployCpuRequest(event.target.value)}
placeholder="예: 100m"
/>
</label>
</div>
<div className="grid gap-3 md:grid-cols-2">
<label className="block">
<span className="text-xs font-medium text-muted-foreground">CPU 상한</span>
<input
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none"
value={deployCpuLimit}
disabled={deploying}
onChange={(event) => setDeployCpuLimit(event.target.value)}
placeholder="예: 500m"
/>
</label>
<label className="block">
<span className="text-xs font-medium text-muted-foreground">메모리 요청</span>
<input
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none"
value={deployMemoryRequest}
disabled={deploying}
onChange={(event) => setDeployMemoryRequest(event.target.value)}
placeholder="예: 256Mi"
/>
</label>
</div>
<label className="block">
<span className="text-xs font-medium text-muted-foreground">메모리 상한</span>
<input
className="mt-2 h-9 w-full rounded-md border border-input bg-background px-3 text-sm outline-none"
value={deployMemoryLimit}
disabled={deploying}
onChange={(event) => setDeployMemoryLimit(event.target.value)}
placeholder="예: 512Mi"
/>
</label>
<div className="rounded-md border border-border bg-background p-4"> <div className="rounded-md border border-border bg-background p-4">
<p className="text-xs text-muted-foreground">배포 이미지</p> <p className="text-xs text-muted-foreground">배포 이미지</p>
<p className="mt-2 break-all font-mono text-xs font-medium">{latestBackendBuild?.downloadUrl || '-'}</p> <p className="mt-2 break-all font-mono text-xs font-medium">{latestBackendBuild?.downloadUrl || '-'}</p>
+10 -2
View File
@@ -63,6 +63,14 @@ export type LauncherBackendDeployResult = {
logs: string; logs: string;
}; };
export type DeploymentResourceLimits = {
replicas?: number;
cpuRequest?: string;
cpuLimit?: string;
memoryRequest?: string;
memoryLimit?: string;
};
export type LauncherBuildEnvVariable = { export type LauncherBuildEnvVariable = {
key: string; key: string;
value: string; value: string;
@@ -281,10 +289,10 @@ export function getLauncherBackendDeployment() {
return apiRequest<LauncherBackendDeployment>('/launcher/backend-deployment'); return apiRequest<LauncherBackendDeployment>('/launcher/backend-deployment');
} }
export function deployLauncherBackend(image: string) { export function deployLauncherBackend(image: string, resources?: DeploymentResourceLimits) {
return apiRequest<LauncherBackendDeployResult>('/launcher/backend-deployment', { return apiRequest<LauncherBackendDeployResult>('/launcher/backend-deployment', {
method: 'POST', method: 'POST',
body: JSON.stringify({ image }) body: JSON.stringify({ image, resources: resources && Object.keys(resources).length ? resources : undefined })
}); });
} }