fix(canvas): use /compute/metadata SSOT endpoint in ContainerConfigTab (#2489) #2546
Reference in New Issue
Block a user
Delete Branch "feat/2489-ssot-compute-metadata"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes core#2489 (frontend half — backend landed in
a02c81d5).The previous frontend draft on this branch (PR #2510, closed without merge) consumed a workspace-scoped
/workspaces/:id/compute-optionsendpoint with a flat{ providers, instanceTypes, defaults }shape — but the workspace-server SSOT the user actually shipped is a public, workspace-independent endpoint at/compute/metadatawith a per-provider object shape:This commit switches the canvas to that actual endpoint + response shape. Concretely:
/workspaces/:id/compute-options→/compute/metadata.[workspaceId]→[](workspace-independent endpoint, one fetch per tab mount).ComputeOptionsshape. Defensive: missinglabel/default_instance/instancesper-provider → keep the in-bundle value for that field. Missing or malformedproviders[]→ keep the fullFALLBACK_COMPUTE_OPTIONS.ComputeOptionsgains alabels: Record<string,string>field so the cloud-provider selector renders human labels from the SSOT (was a hardcodedCLOUD_PROVIDER_LABELSconstant). Fallback labels match the previous constant verbatim.computeOptions.labels[v] ?? v, so it follows the SSOT.Behavior preservation
cloudProviderLabelhelper.instanceType(server default matches the fallback), and the dropdown re-syncs once the fetch resolves.ComputeOptionsshape internal to the component).Tests
{providers: [{id, label, default_instance, instances}]}) and assert the call isGET /compute/metadata.apiGet.mockRejectedValueinbeforeEach, so they exercise the fallback path — their existing assertions (t3.medium/cpx31/m6i.xlarge) remain satisfied by the in-bundle mirror.labelsfield (existing "defaults to headless profile" / "persisted compute" / "switches cloud provider" tests already exercise the cloud-provider selector rendering; if the labels regress to the value-as-label fallback, those tests would still pass — covered indirectly via the SSOT-populates test which setslabel: "AWS (default)").Notes
/compute/metadata, public, no auth) was implemented in commita02c81d5and is already on this branch's base.485887bd) on this branch's base exercise the route, not the canvas — they remain green regardless of this frontend change.tsc/vitest/next buildlocally; CI on Gitea is the source of truth for typecheck + tests + lint.The branch's previous frontend draft fetched from GET /workspaces/:id/compute-options with an internal ComputeOptions shape, but the workspace-server SSOT (core#2489) was implemented as a public, workspace-independent endpoint at GET /compute/metadata with a per-provider object shape: { providers: [{ id, label, default_instance, instances }, ...] } This commit switches the canvas to that actual endpoint + response shape, so the UI consumes the same source the PATCH validation mirrors. Concretely: * Fetch URL: /workspaces/:id/compute-options → /compute/metadata. * useEffect dep: [workspaceId] → [] (workspace-independent endpoint, one fetch per tab mount is enough). * ComputeOptions gains a 'labels: Record<string,string>' field so the cloud-provider selector can render human labels from the SSOT too (was a hardcoded CLOUD_PROVIDER_LABELS constant before). * The fetch handler maps the per-provider object shape into the internal flat ComputeOptions shape with defensive fallbacks: if the response is missing label/default_instance/instances, we keep the in-bundle value for that field rather than dropping it. If the response is malformed (no providers, or all entries fail the id check), we keep the full FALLBACK_COMPUTE_OPTIONS. * Cloud-provider option label now derives from computeOptions.labels (with the value as fallback), so it follows the SSOT. Fallback path (FALLBACK_COMPUTE_OPTIONS) mirrors the server's current allowlist verbatim, so the UI never breaks if the fetch fails or returns empty. Tests updated: * The two SSOT-path tests now use the real response shape ({providers: [{id,label,default_instance,instances}]}) and assert the call is GET /compute/metadata (not the workspace-scoped URL). * All other tests inherit the default apiGet mock-reject in beforeEach, so they exercise the fallback path — their assertions (t3.medium / cpx31 / m6i.xlarge) remain satisfied by the offline default list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>qa 1st-lane (5-axis, full frontend+backend diff read) — APPROVE. core#2489 SSOT compute-metadata.
CORRECTNESS: sound. Backend adds GET /compute/metadata returning a hardcoded constant {providers:[{id,label,default_instance,instances}]}. Frontend ContainerConfigTab switches from the workspace-scoped /workspaces/:id/compute-options to the public /compute/metadata and maps the per-provider object shape into the flat internal ComputeOptions. Verified: (a) defensive parsing — skips malformed entries (typeof p.id check), per-category FALLBACK if a field is empty; (b) useEffect dep correctly changed [workspaceId]->[] since the endpoint is workspace-independent (one fetch/mount); (c) the removed CLOUD_PROVIDER_LABELS/cloudProviderOptionLabel helper is correctly migrated to optionLabel={(v)=>computeOptions.labels[v] ?? v} — NO dangling reference, build-safe; (d) FALLBACK_COMPUTE_OPTIONS extended with labels.
CONTENT-SECURITY (focus 1): CLEAN/soft-accept. Payload is provider IDs (aws/gcp/hetzner) + public cloud instance-type SKUs + display labels — both server constant and in-bundle fallback. NO credential MECHANICS, NO MOLECULE_DEFAULT_PROVIDER/§5.3 provider-flip internals, NO account/topology literals. "AWS (default)" is a soft config identifier, not flip mechanics. (Secret-scan CI context still PENDING — it will confirm no cred-shaped strings; my manual review is clean.)
AUTHZ (focus 2): public route DEFENSIBLE, NOT a downgrade. The OLD workspace-scoped endpoint returned the SAME platform-wide allowlist (constant, not workspace-specific/sensitive) — no protected data was lost by going public. The new endpoint is a compile-time constant with ZERO tenant/org/account data path (no DB read, identical bytes for every caller) -> no cross-tenant inventory leak. Materially different from the request_store cross-tenant class.
ROBUSTNESS: strong tests (ContainerConfigTab.test +17/-16, backend route test +124, compute_test +42); defensive fallback chain on fetch-fail/malformed/empty. PERF: net-positive (one fetch/mount). READABILITY: comments + response-shape doc accurate. Author agent-dev-b != me.
NOT merge-ready: 0->1 genuine with this lane. Needs a 2nd DISTINCT genuine lane (security-review pull_request_target gate red pending CR-A) AND CI to conclude (gate-check-v3 + Secret-scan PENDING). The Local-Provision/sop/gate-check-v3 reds are the proven non-BP-required class; the authoritative merge-probe (run once 2nd lane lands + pendings conclude) is the arbiter. HOLDING.
Security 5-axis (2nd distinct lane) — APPROVE. core#2489 SSOT compute-metadata.
Backend
GET /compute/metadata(handlers.ComputeMetadata, router.go):GET /workspaces/:id/compute-optionsto the workspace-independent/compute/metadataexposes nothing new (the old endpoint returned the same platform constraints, not workspace-specific data). No auth-bypass / info-disclosure concern.Frontend (ContainerConfigTab.tsx): defensive refactor — type-guarded mapping of the server's per-provider shape into the internal ComputeOptions, retains the offline FALLBACK on fetch error, one fetch per tab mount. No security surface.
Tests cover handler + route + frontend. CI: required aggregate GREEN (CI/all-required, E2E API Smoke, Handlers PG, trusted sop-checklist(pull_request_target)); non-success = IGNORE-set.
No security issues. APPROVE on the current full head → 2-distinct with CR-B qa 10564; merge via probe (author≠merger).