Workload
Overview
This document covers the Workload screen (gpuWorkload / route name Workload) — a filterable table of all Volcano PodGroup/workload objects across clusters, with drill-down to Workload Detail (topology canvas) and Workload Log (log viewer), plus a “Deploy Workload” creation modal (guide-only, links out to Model Manager). It is one of the three screens under the “Workload Management” LNB menu group (gpuWorkloadGrp — routes.manifest.json:305-312), alongside Scheduling (see 1-scheduling.mdx) and Time Table (see 2-timetable.mdx).
Workload lives under src/modules/scheduler/dashboard/ (the “scheduler(volcano) 병합 모듈” — a merged Volcano-scheduler feature set), and is wired into the host router via src/pages/index.ts + src/router/route-defs.ts + routes.manifest.json. Both a system (/system/gpu/...) and a project (/proj/{project}/gpu/...) variant exist, sharing the same Vue components.
Route Design
Route names/constants: src/constants/routeNames/index.ts, paths: src/constants/routes/index.ts.
| Screen | Route name (system) | Route name (project) | Path | Component (lazy import) |
|---|---|---|---|---|
| Workload (list) | Workload (SCHEDULER_WORKLOAD) | projectGpuSchedulerWorkload (PROJECT_SCHEDULER_WORKLOAD) | /workload → /system/gpu/workload / /proj/{project}/gpu/workload | @/modules/scheduler/dashboard/pages/Workload/index.vue — src/pages/index.ts:51-52 |
| Workload Detail (hidden drill-down) | WorkloadDetail (SCHEDULER_WORKLOAD_DETAIL) | same | /workload/detail/:cluster/:namespace/:name | @/modules/scheduler/dashboard/pages/WorkloadDetail/index.vue — src/pages/index.ts:55-56 |
| Workload Log (hidden drill-down) | WorkloadLog (SCHEDULER_WORKLOAD_LOG) | same | /workload/logs/:poolName/:workloadName | @/modules/scheduler/dashboard/pages/WorkloadLog/index.vue — src/pages/index.ts:49-50 |
Route registration/mapping: src/router/index.ts:36-52 (system componentMap) and :71-86 (project componentMap). Route metadata (title/icon/parent/apis) is declared in src/router/route-defs.ts:620-643 (Workload def) and mirrored/generated into routes.manifest.json:753-859 (gpuWorkload menu entry) plus :1323-1434 for the project-mode projectGpuSchedulerWorkload entry. An old alias redirect exists for the legacy path (/scheduler/workload → SCHEDULER_WORKLOAD) — src/constants/routes/index.ts:78.
ROUTE_NAMES.SCHEDULER_WORKLOAD_DETAIL/_LOG are declared once and reused by both system and project route tables (route-defs.ts:660-668, 683-692 and :851-869), so the same page component serves both contexts; project scoping is done inside the page via getHostProjectId() rather than by separate components.
UI Structure
Workload Management (LNB group, order=3)
├── Scheduling (order 1) → see 1-scheduling.mdx
├── Time Table (order 2) → see 2-timetable.mdx
└── Workload (order 3) → Workload/index.vue ← THIS DOC
├── Workload Detail (hidden) → WorkloadDetail/index.vue (VueFlow topology canvas)
└── Workload Log (hidden) → WorkloadLog/index.vue (log viewer, split layout)
Workload page (Workload/index.vue)
- Header with title + “Deploy Workload” button opening a 2-step creation modal (:2-16, modal :284-447).
- Filter bar: Cluster / Project / GPU Pool / Workload Type / GPU Type / State selects, plus a “search in
” + text search row, and active-filter tag chips (:18-122).
- Loading / error / empty states (:124-147).
- Results shown as
<Table>(from @cloudz-mp/zmp-common-ui) with accordion sub-rows revealing active_pods (:143-282). - Columns: Workload(name), GPU Health, Type, Category, Priority, Preemptible, Status, GPU Pool, GPU Allocation, CPU/Memory, Created At, Project, Cluster, Actions (tableColumns, :861-889).
- Row actions via WorkloadActionMenu (stop/delete/detail) — :229-239.
- GPU health badge (“Action needed”) that deep-links into GPU Fault screen — :186-206, openFaultInInventory :850-859.
Workload Detail page (WorkloadDetail/index.vue)
- Breadcrumb + live status header (heart/warn icon, “Priority Run” button gated by ownership + edit permission) — :1-59.
- VueFlow canvas rendering a Workload node and Pod nodes with edges — :76-150+.
- PriorityRunDialog reused for ad-hoc priority elevation.
Workload Log page (WorkloadLog/index.vue)
- Breadcrumb (GPU Overview → Pool → Workload name), title bar with streaming tag, download-logs and back-to-pool icon buttons — :1-89.
- Split layout: left info sidebar (Status / Spec / Resource-per-pod sections) + (below the read excerpt) a main log-stream panel — :91-150+.
Component Design
Key components used by these three screens (all under src/modules/scheduler/dashboard/components/ unless noted):
| Component | File | Used by | Props/Emits (from usage) |
|---|---|---|---|
| ClusterSelectGuard | components/ClusterSelectGuard.vue | Workload/index.vue, WorkloadDetail/index.vue, WorkloadLog/index.vue | none — self-contained guard that blocks pages until a cluster is chosen when the realm is multi-cluster |
| WorkloadActionMenu | components/WorkloadActionMenu/index.vue | Workload/index.vue (row actions) | props: name, cluster, project, namespace, owner-kind; emits: done, detail. Logic in sibling action.ts (resolveStopAction) |
| PriorityRunDialog | components/PriorityRunDialog/index.vue | WorkloadDetail/index.vue | props: open, workload-name, subtitle, status-badge, footnote, target-rect, gang-info; emits: close, pick |
| BackChip | src/modules/scheduler/common/BackChip.vue | WorkloadDetail/index.vue | :fallback |
Composables consumed:
| Composable | File | Used by | Purpose |
|---|---|---|---|
| useFaultState | src/modules/scheduler/fault/useFaultState.ts | Workload/index.vue (:469,831) | Shared GPU-fault/alert lookup so the Workload table’s “Action needed” badge matches the Inventory tree’s badge logic |
| useProfileStore (Pinia) | src/modules/scheduler/_host/useProfileStore.ts | Workload/index.vue | Realm/cluster/namespace/project selection state shared across the whole scheduler module |
| getHostProjectId | src/modules/scheduler/_host/utils/hostContext.ts | All three pages | Detects project-mode (/proj/{id}/...) to lock filters/scope |
| workloadHelpers | pages/Workload/workloadHelpers.ts | Workload/index.vue | workloadTypeOf/Label, gpuTypeOf/Label, gpuResourceText, formatDateTime, relativeAge — pure derivation helpers for table cells |
API Design
All Volcano-scheduler API calls are centralized in src/modules/scheduler/dashboard/apis/index.ts (base path /core/v1/scheduler, axios instance = services.axiosInstance from _host/baseUi.ts). GPU-pool “core” endpoints (a separate /core/v1/realms/{realm}/gpu/pools) live in src/modules/scheduler/dashboard/apis/coreGpu.ts (fetchCoreGpuPools, used by these pages to scope by project pool ownership).
| Function | Method & Path | Used by (file:line) | Notes |
|---|---|---|---|
| fetchWorkloads(namespace, gpuPool?, cluster?, project?) | GET {realm-cluster}/workloads | Workload/index.vue:459,591,604 | Normalizes 2 possible BE response shapes via normalizeWorkload() (apis/index.ts:184-276). Auto-injects project from getHostProjectId() if caller omits it (apis/index.ts:328). |
| elevateWorkloadPriority(namespace, podName, priority, cluster?) | POST \{realm-cluster\}/actions/direct-run | Priority-run dialogs (WorkloadDetail) | priority 100–900 (P1–P9) |
| evictWorkload(namespace, podName, cluster?) | POST /core/v1/scheduler/clusters/\{cluster\}/actions/evict | WorkloadActionMenu/action.ts | Server dispatches to stop-isvc (KServe) or delete depending on owner kind |
| fetchGpuPoolList(projectId?) | derives from fetchVolcanoQueues() → GET {realm-cluster}/volcano-queues | Workload/index.vue:462,666 | Maps queue name to {label, value} |
| fetchProjects() | GET /core/v1/realms/{realm}/projects | Workload/index.vue:461,654 | Filter-bar project options |
| fetchCoreGpuPools() | GET /core/v1/realms/{realm}/gpu/pools | apis/coreGpu.ts; all three pages | Pool ownership ledger, used to scope “my project’s pools” |
| fetchPodLogs(cluster, namespace, appId, podName, tailLines) | GET {realm-cluster}/applications/{appId}/logs | WorkloadLog page (log tail) | |
| fetchWorkloadManifest(cluster, namespace, kind, name) | GET {realm-cluster}/workloads/manifest | WorkloadDetail (YAML drawer) | |
| resolveCluster() / resolveRealm() | n/a (client helper) | All three pages | See Business Logic — cluster resolution/guard rules |
Route-level API permission bindings for this screen (used by the host’s manifest-based RBAC, DERIVED_ONLY mode) are declared in the schedulerWorkloadApis constant — src/router/route-defs.ts:240-260 — and mirrored in routes.manifest.json:812-858 (gpuWorkload). Key mutating endpoints requiring edit verb: POST .../actions/direct-run-internal, POST .../actions/evict.
State Management
- Pinia store: useProfileStore (src/modules/scheduler/_host/useProfileStore.ts, store id from STORES.PROFILE, defined with the Composition API defineStore setup syntax). Holds realm/cluster list (clusters, schedulerClusters), selectedCluster, selectedNamespace, selectedProject, projects, namespaces, and ensureProfile() to lazy-load the profile. The Workload page reads/writes this store for scope filters (Workload/index.vue:494,510,574-576,624-629).
- Other Pinia stores in the module (not primary to this screen but part of the same scheduler dashboard state layer): useQueueStore (stores/useQueueStore/index.ts, built from generic createQueueStore(STORES.QUEUE_DASHBOARD) factory in stores/createQueueStore.ts), useSystemQueueStore, useGpuPoolAdminStore.
- Local component state (no store): the Workload page otherwise uses plain ref/reactive/computed local to the SFC — no dedicated Pinia store for the page. It keeps workloads, filters (reactive), projectOptions, poolOptions, createOpen/form (deploy modal) as page-local state (Workload/index.vue:486-569,552-553,1009-1023).
- Cross-page cache: none observed — each page fetches independently on mount/poll; no shared workload cache/store between Workload list and Time Table (confirmed by both calling fetchWorkloads separately with their own params).
Business Logic
- Cluster resolution / guard rule — resolveCluster() (apis/index.ts:67-83): explicit selection (?cluster= query or store.selectedCluster) wins; if realm has exactly 1 cluster it auto-selects; if it has >1 clusters and none explicitly chosen it throws (“클러스터를 먼저 선택해 주세요.”) rather than silently defaulting to clusters[0] — enforced via ClusterSelectGuard component on Workload/WorkloadDetail/ WorkloadLog pages. The Workload list additionally supports fanning out across all clusters via an explicit “All” selection rather than requiring single-cluster selection up front (Workload/index.vue:573-580).
- Project isolation (“설계 ④”) — When in project mode (getHostProjectId() truthy):
- The Workload list uses a 2-pronged “is my workload” check — pool ledger membership OR project/namespace label match — to avoid empty-list failures from FE/BE label mismatches (isMyWorkload, Workload/index.vue:540-545,927-929); project filter cannot be cleared via “Clear all” when locked (:697-698).
- fetchWorkloads auto-injects the host project into project param server-side via injectProjectParam/getHostProjectId() fallback if caller doesn’t pass one explicitly (apis/index.ts:325-329).
- Priority class mapping — priorityClassToValue() (apis/index.ts:174-182): p1–p9 → 100–900; default-batch → 100; unknown → null. Priority badge display divides by 100 and prefixes P (Workload/index.vue:177). Missing priority defaults to display P1 in the Workload table (Workload/index.vue:173-178) even though the underlying value is null (scheduler treats unspecified as P1).
- GPU health badge / Inventory parity — the Workload table’s fault badge must use the exact same useFaultState composable as GpuInventory’s tree, “so users don’t have to decide which screen to trust” (Workload/index.vue:828-831 comment + code).
- Ownership + permission gate for Priority Run — WorkloadDetail only shows the “Priority Run” button if isMyWorkload && canEditWorkload (both conditions AND’d, referencing ticket “P2-F-B4”) — WorkloadDetail/index.vue:44.
- Status label translation — backend status Reserving is always displayed as “Waiting” to the user (statusLabel(), Workload/index.vue:993-996; also used in the State filter, :938-939).
- RBAC via route manifest (DERIVED_ONLY) — API-level permission is derived solely from the apis array attached to this route’s menu entry; any endpoint not listed there returns 403 for non-super-admin roles even if reachable in code (route-defs.ts:133-144 comment).
Data Flow
Workload list load cycle:onMounted → loadMyPoolNames() [project mode only] + loadWorkloads() + loadFilters()
loadWorkloads():
if filters.cluster === 'all': fan-out fetchWorkloads() per allClusterTargets(), tag `_cluster`
else: fetchWorkloads() for the single selected cluster
loadFilters(): fetchProjects() + fetchGpuPoolList() (independent try/catch, non-blocking)
→ rows computed (derives type/category/priority/preemptible/gpuPool/resources/status/age)
→ filteredRows computed (applies project/pool/wlType/gpuType/state/text filters + isMyWorkload gate)
→ <Table :data="filteredRows"> renders, accordion expands to active_pods sub-rows
watch(filters.cluster) → loadWorkloads()
watch([schedulerClusters, clusters]) → re-fan-out if still on 'all' and clusters list changed
User clicks WorkloadActionMenu (⋮) on a row
→ action.ts resolveStopAction(ownerKind) picks stop-isvc vs delete semantics
→ evictWorkload(namespace, podName, cluster) [POST .../actions/evict]
→ emits 'done' → Workload/index.vue.onWlDone() → loadWorkloads() (full refresh)
OR
→ emits 'detail' → openDetail(row) → router.push(SCHEDULER_WORKLOAD_DETAIL, \{cluster, namespace, name\})
WorkloadDetail mounted with route params \{cluster, namespace, name\}
→ fetches workload + topology data (nodes/pods) → renders VueFlow graph
→ if isMyWorkload && canEditWorkload: "Priority Run" button visible
→ openPriorityRun() → PriorityRunDialog opens
→ onPriorityPick(priority) → elevateWorkloadPriority(namespace, podName, priority, cluster)
[POST .../actions/direct-run]
→ dialog closes, page state refreshed to reflect new priority
Dependency Graph
src/pages/index.ts
├─(dyn import)→ src/modules/scheduler/dashboard/pages/Workload/index.vue
├─(dyn import)→ src/modules/scheduler/dashboard/pages/WorkloadDetail/index.vue
└─(dyn import)→ src/modules/scheduler/dashboard/pages/WorkloadLog/index.vue
src/router/index.ts ──uses names from── src/constants/routeNames/index.ts
src/router/route-defs.ts ──uses paths from── src/constants/routes/index.ts
routes.manifest.json ──generated/mirrors── src/router/route-defs.ts (per build-manifest script, referenced in route-defs.ts:1-9)
Workload/index.vue
├─→ @/modules/scheduler/dashboard/apis (fetchWorkloads, fetchProjects, fetchGpuPoolList, resolveCluster)
├─→ @/modules/scheduler/dashboard/apis/coreGpu (fetchCoreGpuPools)
├─→ @/modules/scheduler/dashboard/components/ClusterSelectGuard.vue
├─→ @/modules/scheduler/dashboard/components/WorkloadActionMenu/index.vue
├─→ @/modules/scheduler/fault/useFaultState
├─→ @/modules/scheduler/_host/useProfileStore
├─→ @/modules/scheduler/_host/utils/hostContext (getHostProjectId)
├─→ ./workloadHelpers.ts (workloadTypeOf/Label, gpuTypeOf/Label, gpuResourceText, formatDateTime, relativeAge)
├─→ @cloudz-mp/zmp-common-ui (Table, FormItem, Input, Select, Icon, Tag)
└─→ @/constants (ROUTE_NAMES)
WorkloadDetail/index.vue
├─→ @/modules/scheduler/dashboard/components/ClusterSelectGuard.vue
├─→ @/modules/scheduler/dashboard/components/PriorityRunDialog/index.vue
├─→ @/modules/scheduler/common/BackChip.vue
└─→ @vue-flow/core (VueFlow, Background, Controls, Panel, Handle, Position)
WorkloadLog/index.vue
├─→ @/modules/scheduler/dashboard/components/ClusterSelectGuard.vue
└─→ @/modules/scheduler/dashboard/apis (fetchPodLogs, presumed from apis module)
@/modules/scheduler/dashboard/apis/index.ts
├─→ @/modules/scheduler/_host/baseUi (services.axiosInstance, getRealm)
├─→ @/modules/scheduler/_host/utils/hostContext (getHostProjectId)
├─→ @/modules/scheduler/_host/utils/projectScope (injectProjectParam)
├─→ @/modules/scheduler/_host/useProfileStore
└─→ @/modules/scheduler/common/types/server, common/types/client (type defs)
Sequence Flow
A. Enter Workload list (project mode) and stop a workload:- Router resolves projectGpuSchedulerWorkload at /proj/{project}/gpu/workload → loads Workload/index.vue.
- getHostProjectId() returns the project id → lockedProjectId/projectLocked = true; filters.project is pre-set and the Project select is disabled.
- onMounted → loadMyPoolNames() (pool ownership ledger) + loadWorkloads() + loadFilters() run concurrently.
- loadWorkloads() fans out fetchWorkloads('', '', cluster, lockedProjectId) across allClusterTargets() (since filters.cluster defaults to 'all').
- rows/filteredRows computed apply isMyWorkload() gating (pool-ledger OR project/namespace match) in addition to other filters.
- User opens WorkloadActionMenu on a row → chooses “Stop” → menu’s action.ts resolves stop semantics by ownerKind → calls evictWorkload(namespace, name, cluster) (POST /actions/evict).
- Menu emits done → onWlDone() → loadWorkloads() re-fetches and re-renders the table with the workload’s updated/removed state.
- User clicks a row (or its detail emit) → openDetail(row) → router.push(SCHEDULER_WORKLOAD_DETAIL, {cluster, namespace, name}).
- WorkloadDetail mounts with route params → fetches workload + topology data (nodes/pods) → renders VueFlow graph.
- If isMyWorkload && canEditWorkload, the “Priority Run” button is shown.
- openPriorityRun() opens PriorityRunDialog; user picks a priority.
- onPriorityPick(priority) → elevateWorkloadPriority(namespace, podName, priority, cluster) (POST .../actions/direct-run).
- Dialog closes; page state refreshes to reflect the new priority.
Key Findings
- The Workload page is purely a read + narrow-write UI over the Volcano scheduler API surface centralized in src/modules/scheduler/dashboard/apis/index.ts.
- It implements client-side multi-cluster fan-out (“All” cluster mode) because the backend /workloads endpoint is single-cluster only.
- No dedicated Pinia store exists for Workload page state — only the shared useProfileStore (realm/cluster/project/namespace scope) is used; all page-specific data (workload list, filters, modal state) lives in local ref/reactive inside the SFC <script setup>.
- The page routes project isolation through getHostProjectId() + a “GPU pool ownership ledger” (fetchCoreGpuPools) as an additional safety net beyond label matching — this pattern is explicitly called out in code comments as security-relevant (“설계 ④”, “IDOR fail-open 차단”).
- The “Deploy Workload” modal does not call a create-workload API — it is a guided YAML-example generator that copies to clipboard or redirects to the host’s Model Manager (goModelDeploy()); there is no in-app workload deployment endpoint used here.
Risks / Technical Debt
- Workload/index.vue is a large monolithic SFC (2000+ lines including styles) with substantial inline business logic (filter derivation, priority math) — no unit test files were found colocated for its main logic (only WorkloadActionMenu/tests exists under dashboard/).
- Heavy reliance on loosely-typed
any/Record<string, unknown>for raw workload shapes — normalization logic (normalizeWorkload) attempts to reconcile two backend response shapes, which is inherently fragile to backend contract drift. - Project-mode workload filtering trusts 2 independent signals (pool ledger + labels) with a fallback “allow through” behavior during the loading window before myPoolNames populates, which could transiently render unfiltered cross-project data.
Assumptions
- WorkloadLog/index.vue’s log-fetching call to fetchPodLogs and its polling/streaming mechanism were Not Found in Code in the portion read (only the template’s left sidebar was inspected before the file was truncated); the “Streaming” tag suggests possible polling or SSE, but the mechanism was not confirmed.
- WorkloadDetail/index.vue‘s exact topology-fetching composable/API call was Not Found in Code in the portion read (only template markup through line 150 was inspected); the calls to fetchWorkloadManifest/topology composables are inferred from sibling pages’ patterns (useClusterTopology) and the imports list, not directly confirmed line-by-line.
Related Files
- routes.manifest.json
- src/router/route-defs.ts
- src/router/index.ts
- src/constants/routeNames/index.ts
- src/constants/routes/index.ts
- src/pages/index.ts
- src/modules/scheduler/dashboard/pages/Workload/index.vue
- src/modules/scheduler/dashboard/pages/Workload/workloadHelpers.ts
- src/modules/scheduler/dashboard/pages/WorkloadDetail/index.vue
- src/modules/scheduler/dashboard/pages/WorkloadLog/index.vue
- src/modules/scheduler/dashboard/pages/WorkloadLog/logState.ts
- src/modules/scheduler/dashboard/pages/GpuPoolDetail/index.vue (drill-down target)
- src/modules/scheduler/dashboard/apis/index.ts
- src/modules/scheduler/dashboard/apis/coreGpu.ts
- src/modules/scheduler/dashboard/components/ClusterSelectGuard.vue
- src/modules/scheduler/dashboard/components/WorkloadActionMenu/index.vue
- src/modules/scheduler/dashboard/components/WorkloadActionMenu/action.ts
- src/modules/scheduler/dashboard/components/PriorityRunDialog/index.vue
- src/modules/scheduler/fault/useFaultState.ts
- src/modules/scheduler/_host/useProfileStore.ts
- src/modules/scheduler/_host/baseUi.ts
- src/modules/scheduler/_host/utils/hostContext.ts
- src/modules/scheduler/_host/utils/projectScope.ts
- src/modules/scheduler/common/types/server/index.ts
- src/modules/scheduler/common/types/client/index.ts
- src/modules/scheduler/common/BackChip.vue
- src/modules/scheduler/dashboard/stores/useQueueStore/index.ts
- src/modules/scheduler/dashboard/stores/createQueueStore.ts
- src/i18n/locales/en/dash-workload.ts (and ja/ko equivalents)