feat(compute#2489-A): add display_defaults field to /compute/metadata SSOT #2879

Merged
devops-engineer merged 1 commits from fix/2489-ssot-display-default into main 2026-06-14 21:32:04 +00:00
2 changed files with 184 additions and 14 deletions
@@ -82,6 +82,27 @@ var workspaceComputeDefaultInstanceByProvider = map[string]string{
"gcp": "e2-standard-2",
}
// workspaceComputeDisplayDefaultByProvider is the per-provider default machine
// size the canvas pre-selects for DISPLAY-mode create flows. Distinct from
// workspaceComputeDefaultInstanceByProvider because display-mode boxes need
// a larger default (t3.xlarge vs t3.medium on AWS) — the create flow's
// display-mode branch codifies the prior hardcoded
// `DEFAULT_DISPLAY_INSTANCE_TYPE = "t3.xlarge"` constant in
// canvas/src/components/CreateWorkspaceDialog.tsx so the SSOT can drive it
// instead of a parallel canvas-side mirror (core#2489 phase-2 enabler).
//
// MUST stay in lock-step with workspaceComputeProvidersOrdered — the SSOT-
// consistency check in init() panics if a provider is added here without
// a default in the parent map (or vice versa). Same bidirectional invariant
// as workspaceComputeProviderLabels / workspaceComputeDefaultInstanceByProvider.
// Pinned by TestComputeMetadata_SSOTInternalConsistency (extended for this
// map in the #2489-A follow-up).
var workspaceComputeDisplayDefaultByProvider = map[string]string{
"aws": "t3.xlarge",
"hetzner": "cpx41",
"gcp": "e2-standard-4",
}
// workspaceComputeInstanceAllowlist is the O(1) validation set, keyed by cloud
// provider. DERIVED from workspaceComputeInstanceTypesOrdered in init() so the
// ordered list (what the canvas renders) and the set (what the backend validates)
@@ -177,15 +198,19 @@ var workspaceComputeMetadataRenderOrder = []string{"aws", "gcp", "hetzner"}
// - render-order slice is a permutation of providers slice (same
// set, no duplicates)
// - every rendered provider has a default + non-empty instance-types
// - every rendered provider has a display-default (added in #2489-A
// follow-up so the canvas's CreateWorkspaceDialog display-mode
// hardcoded t3.xlarge can be REPLACED, not paralleled)
//
// A mismatch in ANY direction panics. The check is extracted as a
// pure function (no side effects, no init() dependency) so the test
// suite can invoke it against MUTATED SSOT data (negative cases:
// missing label, missing render entry, duplicate render entry,
// missing default, empty instance-types) and assert the panic
// behavior. A future regression in the production init() — e.g.
// someone removing the panic for "weird but tolerable" cases —
// would be caught by the negative tests calling this function.
// missing default, missing display-default, empty instance-types)
// and assert the panic behavior. A future regression in the
// production init() — e.g. someone removing the panic for "weird
// but tolerable" cases — would be caught by the negative tests
// calling this function.
//
// Every direction is enforced:
// - label without a provider: dead data (a future
@@ -198,6 +223,12 @@ var workspaceComputeMetadataRenderOrder = []string{"aws", "gcp", "hetzner"}
// - render-order entry without a default: silent empty
// default (the canvas would have to fall back to a hardcoded
// "t3.medium" or fail)
// - render-order entry without a display-default: silent empty
// display-default (the canvas's CreateWorkspaceDialog would
// fall back to a hardcoded "t3.xlarge" — which is the EXACT
// drift bug #2489 was opened to fix; this panic prevents a
// future regression where someone adds a new provider but
// forgets the display-default)
// - render-order entry with empty instance-types: silent
// empty dropdown
// - duplicate render-order entry: render would silently drop
@@ -210,6 +241,7 @@ func checkComputeSSOTConsistency(
labels map[string]string,
renderOrder []string,
defaults map[string]string,
displayDefaults map[string]string,
instanceTypes map[string][]string,
) {
ssotSet := make(map[string]struct{}, len(providers))
@@ -248,15 +280,20 @@ func checkComputeSSOTConsistency(
panic(fmt.Sprintf("workspaceComputeProvidersOrdered has entry %q missing from workspaceComputeMetadataRenderOrder", p))
}
}
// 3. every rendered provider has a default + non-empty
// instance-types (the canvas relies on both; an empty
// default falls back to "t3.medium" via the consumer
// helper, but a missing default is a UX dead-end we want
// to catch at boot, not in the field).
// 3. every rendered provider has a default + a display-default
// + non-empty instance-types (the canvas relies on all three;
// an empty default falls back to "t3.medium" via the consumer
// helper, an empty display-default falls back to "t3.xlarge"
// via the CreateWorkspaceDialog hardcoded constant, and a
// missing instance-types is a UX dead-end — we want all
// three caught at boot, not in the field).
for _, p := range renderOrder {
if _, ok := defaults[p]; !ok {
panic(fmt.Sprintf("workspaceComputeMetadataRenderOrder has entry %q with no default in workspaceComputeDefaultInstanceByProvider", p))
}
if _, ok := displayDefaults[p]; !ok {
panic(fmt.Sprintf("workspaceComputeMetadataRenderOrder has entry %q with no display-default in workspaceComputeDisplayDefaultByProvider (core#2489 phase-2 enabler) — this would silently re-introduce the CreateWorkspaceDialog hardcoded `t3.xlarge` drift bug", p))
}
if len(instanceTypes[p]) == 0 {
panic(fmt.Sprintf("workspaceComputeMetadataRenderOrder has entry %q with empty instance-types list", p))
}
@@ -279,6 +316,7 @@ func init() {
workspaceComputeProviderLabels,
workspaceComputeMetadataRenderOrder,
workspaceComputeDefaultInstanceByProvider,
workspaceComputeDisplayDefaultByProvider,
workspaceComputeInstanceTypesOrdered,
)
}
@@ -515,8 +553,19 @@ type workspaceComputeOptionsResponse struct {
// InstanceTypes per provider, in canonical render order.
InstanceTypes map[string][]string `json:"instanceTypes"`
// Defaults maps each provider → its default instance type (the canvas
// pre-selects this when switching providers).
// pre-selects this when switching providers; headless create flow).
Defaults map[string]string `json:"defaults"`
// DisplayDefaults maps each provider → its default instance type for
// DISPLAY-mode create flows. Distinct from Defaults because display
// boxes need a larger default (t3.xlarge vs t3.medium on AWS). The
// canvas's CreateWorkspaceDialog currently hardcodes the display
// default as t3.xlarge (parallel to this map's value); the canvas
// migration to consume this field is a follow-up PR (core#2489
// phase-2). Codified here as the SSOT so the canvas-side constant
// can be REPLACED (not paralleled) once the canvas PR lands.
// Same bidirectional SSOT-consistency invariant as Defaults: keys
// must match the Providers slice (panicked in init() otherwise).
DisplayDefaults map[string]string `json:"display_defaults"`
}
// buildComputeOptions assembles the SSOT response from the allowlist + defaults.
@@ -538,10 +587,16 @@ func buildComputeOptions() workspaceComputeOptionsResponse {
defaults[k] = v
}
displayDefaults := make(map[string]string, len(workspaceComputeDisplayDefaultByProvider))
for k, v := range workspaceComputeDisplayDefaultByProvider {
displayDefaults[k] = v
}
return workspaceComputeOptionsResponse{
Providers: providers,
InstanceTypes: instanceTypes,
Defaults: defaults,
Providers: providers,
InstanceTypes: instanceTypes,
Defaults: defaults,
DisplayDefaults: displayDefaults,
}
}
@@ -427,6 +427,35 @@ func TestComputeOptions_DefaultsAreValidForTheirProvider(t *testing.T) {
}
}
// TestComputeOptions_DisplayDefaultsAreValidForTheirProvider is
// the core#2489-A phase-2 enabler: the new
// workspaceComputeDisplayDefaultByProvider map (the per-provider
// display-mode default — distinct from the headless default
// because display boxes need a larger default like t3.xlarge)
// MUST satisfy the same allowlist + coverage invariant as the
// existing defaults map. Codifying the prior hardcoded
// `DEFAULT_DISPLAY_INSTANCE_TYPE = "t3.xlarge"` canvas
// constant into the SSOT means the SSOT now drives the
// canvas's display-mode default — the canvas PR that
// REPLACES the hardcoded constant can rely on this test as
// the contract pin.
func TestComputeOptions_DisplayDefaultsAreValidForTheirProvider(t *testing.T) {
for provider, def := range workspaceComputeDisplayDefaultByProvider {
if !instanceTypeAllowedForProvider(provider, def) {
t.Errorf("display-default instance %q for provider %q is not in that provider's allowlist (a bad display-default would silently break the canvas's create flow)", def, provider)
}
}
// Every provider must have a display-default (so the create
// flow's display-mode branch never lands on "" — would
// silently fall back to the canvas's hardcoded t3.xlarge,
// defeating the SSOT).
for _, p := range workspaceComputeProvidersOrdered {
if workspaceComputeDisplayDefaultByProvider[p] == "" {
t.Errorf("provider %q has no display-default instance type (the canvas's CreateWorkspaceDialog would silently fall back to the hardcoded t3.xlarge — exactly the #2489 drift we're consolidating)", p)
}
}
}
// core#2489: the GET /compute-options endpoint returns exactly the SSOT data the
// canvas renders dropdowns from. Every (provider, instance-type) it advertises
// MUST pass validateWorkspaceCompute — the whole point of the consolidation.
@@ -818,7 +847,7 @@ func TestComputeMetadata_ReturnsProviderAllowlist(t *testing.T) {
}
want := []struct {
id, label, defaultInstance string
instanceCount int
instanceCount int
}{
{"aws", "AWS (default)", "t3.medium", 7},
{"gcp", "GCP", "e2-standard-2", 5},
@@ -841,6 +870,43 @@ func TestComputeMetadata_ReturnsProviderAllowlist(t *testing.T) {
}
}
// TestComputeOptions_ResponseIncludesDisplayDefaults pins the
// core#2489-A phase-2 enabler: the /compute/metadata response
// (or buildComputeOptions() directly) must include the new
// `display_defaults` field so the canvas's CreateWorkspaceDialog
// (follow-up PR) can REPLACE the hardcoded
// `DEFAULT_DISPLAY_INSTANCE_TYPE = "t3.xlarge"` constant.
// Codifies the SSOT contract for the display-mode create flow.
func TestComputeOptions_ResponseIncludesDisplayDefaults(t *testing.T) {
resp := buildComputeOptions()
if len(resp.DisplayDefaults) == 0 {
t.Fatal("buildComputeOptions() returned empty DisplayDefaults; the core#2489 phase-2 enabler is missing")
}
wantDefaults := map[string]string{
"aws": "t3.xlarge",
"hetzner": "cpx41",
"gcp": "e2-standard-4",
}
for provider, want := range wantDefaults {
got, ok := resp.DisplayDefaults[provider]
if !ok {
t.Errorf("buildComputeOptions().DisplayDefaults missing provider %q (was: %v)", provider, resp.DisplayDefaults)
continue
}
if got != want {
t.Errorf("buildComputeOptions().DisplayDefaults[%q] = %q, want %q (matches the canvas's prior hardcoded t3.xlarge constant)", provider, got, want)
}
}
// Every Providers entry must have a DisplayDefaults entry
// (the SSOT-consistency check enforces this at boot, but a
// test pin makes the contract greppable).
for _, p := range resp.Providers {
if _, ok := resp.DisplayDefaults[p]; !ok {
t.Errorf("buildComputeOptions() has provider %q with no DisplayDefaults entry", p)
}
}
}
// TestComputeMetadata_SSOTInternalConsistency pins that the SSOT
// additions (workspaceComputeProviderLabels, workspaceComputeMetadataRenderOrder)
// are kept in lock-step with the existing SSOT maps
@@ -932,6 +998,7 @@ func TestComputeMetadata_InitPanicsOnLabelMissingFromProviders(t *testing.T) {
mutatedLabels,
workspaceComputeMetadataRenderOrder,
workspaceComputeDefaultInstanceByProvider,
workspaceComputeDisplayDefaultByProvider,
workspaceComputeInstanceTypesOrdered,
)
}
@@ -962,6 +1029,7 @@ func TestComputeMetadata_InitPanicsOnProviderMissingLabel(t *testing.T) {
workspaceComputeProviderLabels,
workspaceComputeMetadataRenderOrder,
workspaceComputeDefaultInstanceByProvider,
workspaceComputeDisplayDefaultByProvider,
workspaceComputeInstanceTypesOrdered,
)
}
@@ -993,6 +1061,7 @@ func TestComputeMetadata_InitPanicsOnRenderOrderEntryMissingProvider(t *testing.
workspaceComputeProviderLabels,
mutatedRender,
workspaceComputeDefaultInstanceByProvider,
workspaceComputeDisplayDefaultByProvider,
workspaceComputeInstanceTypesOrdered,
)
}
@@ -1029,6 +1098,7 @@ func TestComputeMetadata_InitPanicsOnProviderMissingFromRenderOrder(t *testing.T
mutatedLabels,
workspaceComputeMetadataRenderOrder,
workspaceComputeDefaultInstanceByProvider,
workspaceComputeDisplayDefaultByProvider,
workspaceComputeInstanceTypesOrdered,
)
}
@@ -1058,6 +1128,7 @@ func TestComputeMetadata_InitPanicsOnDuplicateRenderOrderEntry(t *testing.T) {
workspaceComputeProviderLabels,
mutatedRender,
workspaceComputeDefaultInstanceByProvider,
workspaceComputeDisplayDefaultByProvider,
workspaceComputeInstanceTypesOrdered,
)
}
@@ -1093,6 +1164,48 @@ func TestComputeMetadata_InitPanicsOnRenderOrderEntryMissingDefault(t *testing.T
workspaceComputeProviderLabels,
workspaceComputeMetadataRenderOrder,
mutatedDefaults,
workspaceComputeDisplayDefaultByProvider,
workspaceComputeInstanceTypesOrdered,
)
}
// TestComputeMetadata_InitPanicsOnRenderOrderEntryMissingDisplayDefault
// is the core#2489-A negative case for direction 3.c (render-
// order entry without a display-default). A render entry whose
// provider has no display-default would silently fall back to
// the canvas's hardcoded "t3.xlarge" in CreateWorkspaceDialog
// — silently re-introducing the EXACT drift bug #2489 was
// opened to fix. The production check must panic.
func TestComputeMetadata_InitPanicsOnRenderOrderEntryMissingDisplayDefault(t *testing.T) {
// Mutate: remove the "gcp" display-default.
mutatedDisplayDefaults := make(map[string]string, len(workspaceComputeDisplayDefaultByProvider))
for k, v := range workspaceComputeDisplayDefaultByProvider {
if k == "gcp" {
continue
}
mutatedDisplayDefaults[k] = v
}
defer func() {
r := recover()
if r == nil {
t.Fatal("expected panic on render-order-entry-without-display-default, got nil (production checkComputeSSOTConsistency is too lenient)")
}
msg, _ := r.(string)
if !strings.Contains(msg, "gcp") {
t.Errorf("panic message should mention the offending provider 'gcp', got: %q", msg)
}
if !strings.Contains(msg, "display-default") {
t.Errorf("panic message should mention 'display-default' (so a future maintainer can grep the message), got: %q", msg)
}
}()
checkComputeSSOTConsistency(
workspaceComputeProvidersOrdered,
workspaceComputeProviderLabels,
workspaceComputeMetadataRenderOrder,
workspaceComputeDefaultInstanceByProvider,
mutatedDisplayDefaults,
workspaceComputeInstanceTypesOrdered,
)
}
@@ -1129,6 +1242,7 @@ func TestComputeMetadata_InitPanicsOnRenderOrderEntryEmptyInstanceTypes(t *testi
workspaceComputeProviderLabels,
workspaceComputeMetadataRenderOrder,
workspaceComputeDefaultInstanceByProvider,
workspaceComputeDisplayDefaultByProvider,
mutatedInstances,
)
}
@@ -1150,6 +1264,7 @@ func TestComputeMetadata_InitAcceptsLiveSSOT(t *testing.T) {
workspaceComputeProviderLabels,
workspaceComputeMetadataRenderOrder,
workspaceComputeDefaultInstanceByProvider,
workspaceComputeDisplayDefaultByProvider,
workspaceComputeInstanceTypesOrdered,
)
}