feat(warehouse): component warehouse in CubeOps with on-demand node download - #1353
feat(warehouse): component warehouse in CubeOps with on-demand node download#1353fslongjin wants to merge 1 commit into
Conversation
AI-Generated Review — PR #1353
OverviewThis is a large, well-structured feature: CubeOps becomes a cluster-level component warehouse for the four versioned components (cube-shim, cube-image, cube-agent, cube-kernel-scf), with on-demand download by Cubelet when a pinned version is missing during create/restore. Highlights:
The test surface is genuinely good (store tests across both dialects, handler tests, importer tests, cubelet install/fetch tests). Findings1. (Medium) Compat matrix and WebUI are blind to cube-shim drift
2. (Medium) STALE status is now unreachable; dead code left in server and WebUI
3. (Medium) Node warehouse endpoints are unauthenticated behind a spoofable header
4. (Medium) Upload endpoint has no server-side size cap
5. (Low)
|
91b9d1f to
32f14e1
Compare
32f14e1 to
325a613
Compare
|
Replied on each inline thread. Summary: Fixed: crash-window catalog repair (R1), upload GC + 2h TTL (R2), cancel False positives / not changing: unauthenticated |
…ownload Fixes create/restore failing outright when a node lacks a pinned component version, which previously required manual recovery on the node: CubeOps now serves as the cluster-level component warehouse, nodes download missing versions on demand, and creation only fails if the download cannot complete. - CubeOps: unpack one-click packages into the warehouse (image / Agent / Shim / kernel; kernel versions use content-derived short names); import from upload, GitHub Release, or CNB with host whitelisting; imports run as async jobs claimed by replicas; new admin APIs (catalog / delete / coverage / pre-install) and node-facing internal APIs (download / job ack / inventory report) - Cubelet: fetch missing versions during create/restore with concurrent downloads merged; atomic install after validating in a temp dir; failures distinguish "not in warehouse" from "download unsuccessful"; periodic inventory reporting and pre-install job claiming; unchanged behavior when no warehouse address configured - CubeMaster: compatibility evaluation is now "all versions pinned => compatible", removing per-dimension comparison so multi-version templates are not marked stale and rebuilt; create timeout 300s -> 600s - web: new warehouse overview / component detail / jobs pages and the import form, with full EN/ZH translations - deploy: CubeOps mounts a 50G shared persistent volume; deployment fails fast with multiple replicas and no shared storage; compute nodes get the warehouse address automatically, with an in-place upgrade path for existing nodes - CubeDB: 4 new tables (warehouse / import jobs / pre-install jobs / node installs) with mysql and postgres migrations Signed-off-by: jinlong <jinlong@tencent.com>
325a613 to
4701297
Compare
|
Replied on each second-round inline thread. Fixed: skip hashing on duplicate import when dest dir and catalog row already exist; Not changing: unauthenticated |
|
|
||
| function replicaLiveDiffers(node: TemplateNodeCompat): boolean { | ||
| return ( | ||
| versionsDiffer(node.boundGuestImageVersion, node.currentGuestImageVersion) || |
There was a problem hiding this comment.
Drift detection misses cube-shim. Under the new multi-version semantics a replica is OK only when guest-image, cube-agent, kernel, and cube-shim are all pinned (see evaluateCompat in CubeMaster/pkg/templatecenter/store.go), and this replicaLiveDiffers helper drives the new drift note / driftReplicas KPI. It compares only guest/agent/kernel — TemplateNodeCompat has no shim bound/current fields and node-side GetNodeComponentVersions still reports only guest-image/cube-agent/kernel. A node whose shim was upgraded (the most runtime-sensitive of the four, it must match the containerd runtime) will show no drift indicator even though create uses the pinned shim. Consider adding a shim dimension to TemplateNodeCompat and to the node-side version report. The identical pattern exists in TemplateDetail.tsx replicaLiveDiffers.
| shim := normalizeComponentVersion(replica.ShimVersion) | ||
| return guest != "" && agent != "" && kernel != "" && shim != "" | ||
| } | ||
|
|
There was a problem hiding this comment.
evaluateCompat can now return only OK or UNKNOWN, so CompatStatusStale becomes unreachable: GetCompatMatrix still counts stale_templates/stale_replicas/affected_nodes by stored status, but no newly-written row can be STALE (historical rows get recomputed by ScanNodeCompat). The STALE branches that remain — the web KPI cards gated on matrix.summary.staleTemplates > 0, compat.status.STALE / compat.kpi.staleTemplates locale keys, the compatTone STALE case — are effectively dead code. Worth a cleanup pass, and a release-note callout that legacy pre-multi-version replicas (guest+agent pinned, no kernel/shim) move from OK/STALE to UNKNOWN and will now surface the rebuild warning even though they were previously considered compatible.
| httputil.WriteJSON(c, http.StatusOK, gin.H{"ok": true}) | ||
| } | ||
|
|
||
| func requireNodeID(c *gin.Context) (string, error) { |
There was a problem hiding this comment.
Node identity for every /internal/warehouse/* endpoint is a caller-supplied header (X-Cube-Node-ID, or a node_id query param) with no shared secret. Any process that can reach CubeOps :3010 — in Helm, any pod via the ClusterIP service; in one-click, anything on the host network — can impersonate any node: PUT /internal/warehouse/inventory overwrites another node's install-coverage records, POST /internal/warehouse/jobs/:id/ack can mark another node's preinstall jobs succeeded/failed, and GET /internal/warehouse/blob can pull every artifact. This matches CubeMaster's existing /internal/meta trust model (also header-only), so it's a documented cluster-trust boundary rather than a regression — but the README note ("do not publish this prefix") only protects the nginx edge; the ClusterIP service and one-click host exposure still leave it reachable from anything on the cluster/host. Consider a per-node registration secret or network policy, at least for the mutating inventory/ack routes.
| httputil.WriteError(c, http.StatusInternalServerError, err.Error()) | ||
| return | ||
| } | ||
| if _, err := io.Copy(out, src); err != nil { |
There was a problem hiding this comment.
io.Copy streams the uploaded .tar.gz with no byte limit. The only caps are edge-level: nginx client_max_body_size 8g (chart + one-click) and MaxMultipartMemory (which only controls when the multipart body spools to disk, not the final size). A request that reaches CubeOps :3010 directly — no nginx in front, which is how one-click deploys talk to it on localhost and how any cluster pod can reach it — can write an arbitrarily large file into _uploads/, and the 2-hour sweep TTL means a burst of large uploads can fill the warehouse volume before cleanup. Worth enforcing the same 8 GiB cap in Upload (and/or a total-quota check against the layout root) so the limit does not depend on the proxy in front of the server.
| const fd = new FormData(); | ||
| fd.append('file', file); | ||
| const token = localStorage.getItem('cube.accessToken') ?? ''; | ||
| const resp = await fetch('/opsapi/v1/warehouse/uploads', { |
There was a problem hiding this comment.
Unlike every other WebUI API call, warehouseApi.upload uses a raw fetch with the access token read straight from localStorage and no 401 refresh/retry. After the access token expires (~15–30 min lifetime, with refresh-token rotation), the upload fails with 401 and the user must reload the page; ops()/api() in lib/api.ts share a single-flight refreshAccessToken() that this path bypasses. Consider routing the upload through ops() (passing the FormData as body), or adding the same refresh-and-retry on 401 here. The manual JSON.parse(text) also throws on non-JSON error bodies, whereas the shared helpers use safeJson.
| ResyncInterval: 10 * time.Hour, | ||
| DisableCreateNode: false, | ||
| NodeStatusUpdateFrequency: tomlext.FromStdTime(10 * time.Second), | ||
| CubeOpsTimeout: tomlext.FromStdTime(10 * time.Minute), |
There was a problem hiding this comment.
Default cubeops_timeout (10 min) is shorter than CubeOps's own fetch budget: warehouse.fetch_timeout defaults to 30 min and the importer claim-stale threshold is 30 min. On a cold node whose component is not yet in the warehouse, CubeOps may need to download from GitHub (up to 30 min) before it can stream to the node; the cubelet's client times out at 10 min. The server-side singleflight fetch continues (context.WithoutCancel), so a retry succeeds once the artifact lands — but the first sandbox create on a cold node can fail spuriously at exactly the 10-minute mark. Consider raising the cubelet default (or tying it to the 600 s create timeout) so the "up to 10 minutes" WebUI copy matches the server's actual budget.
Background
Since CubeSandbox introduced component multi-versioning, sandbox create/restore requests pin the exact component versions from the template. Previously, if a node did not have a required version locally (typical cases: a new node joining the cluster, or an existing node missing an older version), creation failed immediately and operators had to SSH into the node to copy directories or reinstall by hand.
This PR automates that away: CubeOps acts as the cluster-level component warehouse. Operators import once; nodes download on demand when a version is missing; creation proceeds once the download succeeds, and only fails if it cannot. The overall architecture is described below, with the notable implementation trade-offs listed under "Key Implementation Decisions".
Architecture
Each component keeps a clear responsibility. CubeMaster's boundary is unchanged — it remains unaware of per-node inventory and never handles component files itself:
flowchart TB subgraph control [Control plane] UI[CubeOps console] Ops[CubeOps component warehouse<br/>version catalog + artifact storage] UI -->|import / browse / pre-install| Ops end subgraph sources [Import sources] Up[Admin upload] GH[GitHub Release] CNB[CNB] Up --> Ops GH --> Ops CNB --> Ops end subgraph node [Compute node] Cubelet[Cubelet] --> Inv[Local multi-version inventory] Cubelet -->|on-demand download when missing| Ops endCubeOps: the warehouse and its import entrance
/data/cubeops/warehouse), mounted as a shared persistent volume under Kubernetes. Content is written to a temporary directory first and moved into place only when complete, so a partial directory is never mistaken for an installable version; on shared storage that does not support atomic rename (e.g. NFS), it falls back to copying while preserving executable permissions.Cubelet: on-demand download when a version is missing
CubeMaster: scheduling unchanged, compatibility evaluation corrected
Master keeps dispatching create/restore exactly as before, but the compatibility-matrix semantics had to be corrected in step: since nodes now self-heal missing versions from the warehouse, "node has not reported versions yet" or "node lacks a version" no longer implies template incompatibility. The new rule: a template with all four component versions pinned is immediately compatible; only legacy records with incomplete version information evaluate to unknown. The old per-dimension comparison logic was removed — otherwise multi-version templates would be wrongly marked stale and trigger rebuilds. Additionally, the create timeout is relaxed from 300s to 600s to accommodate the download wait.
Console
Three new pages: warehouse overview (per-component version counts, sizes, missing-node counts, active jobs), component detail (per-version source and checksum, installed/missing node lists, with pre-install from selected missing nodes and version deletion), and a jobs center (import and pre-install jobs, paginated). The import form supports all three sources and multiple architectures. Full English and Chinese UI translations are included.
Key implementation decisions
A few points discussed during design review landed as follows:
Explicitly out of scope for this phase: Master rescheduling based on node inventory, nodes downloading directly from GitHub/CNB, falling back to a different version when one is missing, and fleet-wide forced pre-install.
Code size
97 files, +9,083 / −329. Breakdown by component (lines counted from the staged diff):
Notes: "test code" for web is mock data (for offline development of the three new pages); backend tests cover importing, unpacking, disk layout, catalog aggregation, handlers and the store layer (including database integration tests), plus Cubelet download, installation, concurrency merging, and inventory reporting.
Deployment & upgrade notes