This commit is contained in:
-24
@@ -60,10 +60,6 @@ import {
|
||||
AgentInstallStatusPage as AgentInstallStatusRestoredPage,
|
||||
AgentRegistrationPage as AgentRegistrationRestoredPage,
|
||||
AgentSshKeysPage as AgentSshKeysRestoredPage,
|
||||
AgentWorkspacePage as AgentWorkspaceRestoredPage,
|
||||
GatewayAgentsPage as GatewayAgentsRestoredPage,
|
||||
GatewayDevicesPage as GatewayDevicesRestoredPage,
|
||||
GatewayStatusPage as GatewayStatusRestoredPage,
|
||||
TailscaleAclPage as TailscaleAclRestoredPage,
|
||||
TailscaleRoutesPage as TailscaleRoutesRestoredPage,
|
||||
TailscaleStatusPage as TailscaleStatusRestoredPage
|
||||
@@ -90,9 +86,6 @@ type ViewKey =
|
||||
| 'appReleases'
|
||||
| 'appDeployments'
|
||||
| 'appMigrations'
|
||||
| 'gatewayStatus'
|
||||
| 'gatewayAgents'
|
||||
| 'gatewayDevices'
|
||||
| 'tailscaleStatus'
|
||||
| 'tailscaleRoutes'
|
||||
| 'tailscaleAcl'
|
||||
@@ -104,7 +97,6 @@ type ViewKey =
|
||||
| 'agentRegistration'
|
||||
| 'agentInstallStatus'
|
||||
| 'agentSshKeys'
|
||||
| 'agentWorkspace'
|
||||
| 'agentCommands'
|
||||
| 'runtimeConfig'
|
||||
| 'runtimeLogs'
|
||||
@@ -129,9 +121,6 @@ const viewByLabel: Record<string, ViewKey> = {
|
||||
'실행 설정': 'runtimeConfig',
|
||||
'배포 관리': 'appDeployments',
|
||||
마이그레이션: 'appMigrations',
|
||||
'연결 상태': 'gatewayStatus',
|
||||
'Agent 관리': 'gatewayAgents',
|
||||
'장비 동기화': 'gatewayDevices',
|
||||
'노드 현황': 'tailscaleStatus',
|
||||
라우트: 'tailscaleRoutes',
|
||||
ACL: 'tailscaleAcl',
|
||||
@@ -143,7 +132,6 @@ const viewByLabel: Record<string, ViewKey> = {
|
||||
'장비 등록': 'agentRegistration',
|
||||
'설치 상태': 'agentInstallStatus',
|
||||
'SSH 키 배포': 'agentSshKeys',
|
||||
'Workspace 설정': 'agentWorkspace',
|
||||
'원격 명령': 'agentCommands',
|
||||
'로그/이벤트': 'runtimeLogs',
|
||||
'장비 제어': 'runtimeDeviceControl',
|
||||
@@ -174,9 +162,6 @@ const pathByView: Record<ViewKey, string> = {
|
||||
appReleases: '/applications/releases',
|
||||
appDeployments: '/applications/deployments',
|
||||
appMigrations: '/applications/migrations',
|
||||
gatewayStatus: '/gateway/status',
|
||||
gatewayAgents: '/gateway/agents',
|
||||
gatewayDevices: '/gateway/devices',
|
||||
tailscaleStatus: '/tailscale/status',
|
||||
tailscaleRoutes: '/tailscale/routes',
|
||||
tailscaleAcl: '/tailscale/acl',
|
||||
@@ -188,7 +173,6 @@ const pathByView: Record<ViewKey, string> = {
|
||||
agentRegistration: '/agents/registration',
|
||||
agentInstallStatus: '/agents/install-status',
|
||||
agentSshKeys: '/agents/ssh-keys',
|
||||
agentWorkspace: '/agents/workspace',
|
||||
agentCommands: '/agents/commands',
|
||||
runtimeConfig: '/applications/runtime-settings',
|
||||
runtimeLogs: '/runtime/logs',
|
||||
@@ -2325,12 +2309,6 @@ function App() {
|
||||
return <DeploymentManagementPage />;
|
||||
case 'appMigrations':
|
||||
return <MigrationManagementPage />;
|
||||
case 'gatewayStatus':
|
||||
return <GatewayStatusRestoredPage />;
|
||||
case 'gatewayAgents':
|
||||
return <GatewayAgentsRestoredPage />;
|
||||
case 'gatewayDevices':
|
||||
return <GatewayDevicesRestoredPage />;
|
||||
case 'tailscaleStatus':
|
||||
return <TailscaleStatusRestoredPage />;
|
||||
case 'tailscaleRoutes':
|
||||
@@ -2353,8 +2331,6 @@ function App() {
|
||||
return <AgentInstallStatusRestoredPage />;
|
||||
case 'agentSshKeys':
|
||||
return <AgentSshKeysRestoredPage />;
|
||||
case 'agentWorkspace':
|
||||
return <AgentWorkspaceRestoredPage />;
|
||||
case 'agentCommands':
|
||||
return <AgentCommandsRestoredPage />;
|
||||
case 'runtimeConfig':
|
||||
|
||||
@@ -75,19 +75,9 @@ export const navGroups = [
|
||||
{ label: '장비 제어' },
|
||||
{ label: '설치 상태' },
|
||||
{ label: 'SSH 키 배포' },
|
||||
{ label: 'Workspace 설정' },
|
||||
{ label: '원격 명령' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '플랫폼 게이트웨이',
|
||||
icon: ServerCog,
|
||||
children: [
|
||||
{ label: '연결 상태' },
|
||||
{ label: 'Agent 관리' },
|
||||
{ label: '장비 동기화' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '인프라',
|
||||
icon: HardDrive,
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Megaphone,
|
||||
Network,
|
||||
Rocket,
|
||||
ServerCog,
|
||||
ShieldCheck,
|
||||
Users
|
||||
} from 'lucide-react';
|
||||
@@ -80,19 +79,9 @@ export const navGroups: NavGroup[] = [
|
||||
{ label: '장비 제어', path: '/runtime/devices' },
|
||||
{ label: '설치 상태', path: '/agents/install-status' },
|
||||
{ label: 'SSH 키 배포', path: '/agents/ssh-keys' },
|
||||
{ label: 'Workspace 설정', path: '/agents/workspace' },
|
||||
{ label: '원격 명령', path: '/agents/commands' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '플랫폼 게이트웨이',
|
||||
icon: ServerCog,
|
||||
children: [
|
||||
{ label: '연결 상태', path: '/gateway/status' },
|
||||
{ label: 'Agent 관리', path: '/gateway/agents' },
|
||||
{ label: '장비 동기화', path: '/gateway/devices' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '인프라',
|
||||
icon: HardDrive,
|
||||
|
||||
@@ -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() {
|
||||
const columns = useMemo<Array<DataTableColumn<(typeof runtimeCommands)[number]>>>(
|
||||
() => [
|
||||
|
||||
@@ -77,6 +77,11 @@ export function DeploymentManagementPage() {
|
||||
const [releases, setReleases] = useState<AppRelease[]>([]);
|
||||
const [releaseId, setReleaseId] = 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 [deployModalOpen, setDeployModalOpen] = useState(false);
|
||||
const [selectedDeployment, setSelectedDeployment] = useState<DeploymentJob | null>(null);
|
||||
@@ -158,6 +163,12 @@ export function DeploymentManagementPage() {
|
||||
setReleaseId(release.id);
|
||||
setNamespace(release.app.namespace || '');
|
||||
}
|
||||
if (!release && !releaseId && !releases[0]?.app?.namespace) setNamespace('');
|
||||
setReplicas('1');
|
||||
setCpuRequest('100m');
|
||||
setCpuLimit('500m');
|
||||
setMemoryRequest('128Mi');
|
||||
setMemoryLimit('256Mi');
|
||||
setUpdateLauncherAppVersion(true);
|
||||
setDeployModalOpen(true);
|
||||
};
|
||||
@@ -165,8 +176,22 @@ export function DeploymentManagementPage() {
|
||||
const deploy = async () => {
|
||||
if (deploying) return;
|
||||
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 {
|
||||
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: '배포 작업이 실행되었습니다.' });
|
||||
setSelectedDeployment(result.deploymentJob);
|
||||
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>)}
|
||||
</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" />
|
||||
<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">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
@@ -12,6 +12,14 @@ import type {
|
||||
} from '../../types/application';
|
||||
import type { ApiAppComponent } from '../../types/runtime';
|
||||
|
||||
export type DeploymentResourceLimits = {
|
||||
replicas?: number;
|
||||
cpuRequest?: string;
|
||||
cpuLimit?: string;
|
||||
memoryRequest?: string;
|
||||
memoryLimit?: string;
|
||||
};
|
||||
|
||||
export type GiteaCommit = {
|
||||
sha: string;
|
||||
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()}`);
|
||||
}
|
||||
|
||||
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', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input)
|
||||
|
||||
@@ -36,6 +36,11 @@ export function LauncherVersionsPage() {
|
||||
const [deployOpen, setDeployOpen] = useState(false);
|
||||
const [saving, setSaving] = 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({
|
||||
buildJobId: '',
|
||||
version: '',
|
||||
@@ -128,7 +133,16 @@ export function LauncherVersionsPage() {
|
||||
const deployBackend = () => {
|
||||
if (!latestBackendBuild?.downloadUrl) return;
|
||||
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) => {
|
||||
setDeployResult(result);
|
||||
toast({ type: 'success', message: '런처 백엔드를 배포했습니다.' });
|
||||
@@ -141,6 +155,15 @@ export function LauncherVersionsPage() {
|
||||
.finally(() => setDeploying(false));
|
||||
};
|
||||
|
||||
const openBackendDeploy = () => {
|
||||
setDeployReplicas('1');
|
||||
setDeployCpuRequest('100m');
|
||||
setDeployCpuLimit('500m');
|
||||
setDeployMemoryRequest('128Mi');
|
||||
setDeployMemoryLimit('256Mi');
|
||||
setDeployOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
@@ -159,7 +182,7 @@ export function LauncherVersionsPage() {
|
||||
릴리즈 등록
|
||||
</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" />
|
||||
백엔드 배포
|
||||
</Button>
|
||||
@@ -345,6 +368,60 @@ export function LauncherVersionsPage() {
|
||||
<p className="font-medium">최신 성공 빌드 이미지로 런처 백엔드를 배포합니다.</p>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">Deployment 이미지가 교체되고 rollout 완료까지 대기합니다.</p>
|
||||
</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">
|
||||
<p className="text-xs text-muted-foreground">배포 이미지</p>
|
||||
<p className="mt-2 break-all font-mono text-xs font-medium">{latestBackendBuild?.downloadUrl || '-'}</p>
|
||||
|
||||
@@ -63,6 +63,14 @@ export type LauncherBackendDeployResult = {
|
||||
logs: string;
|
||||
};
|
||||
|
||||
export type DeploymentResourceLimits = {
|
||||
replicas?: number;
|
||||
cpuRequest?: string;
|
||||
cpuLimit?: string;
|
||||
memoryRequest?: string;
|
||||
memoryLimit?: string;
|
||||
};
|
||||
|
||||
export type LauncherBuildEnvVariable = {
|
||||
key: string;
|
||||
value: string;
|
||||
@@ -281,10 +289,10 @@ export function getLauncherBackendDeployment() {
|
||||
return apiRequest<LauncherBackendDeployment>('/launcher/backend-deployment');
|
||||
}
|
||||
|
||||
export function deployLauncherBackend(image: string) {
|
||||
export function deployLauncherBackend(image: string, resources?: DeploymentResourceLimits) {
|
||||
return apiRequest<LauncherBackendDeployResult>('/launcher/backend-deployment', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ image })
|
||||
body: JSON.stringify({ image, resources: resources && Object.keys(resources).length ? resources : undefined })
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user