Compare commits

...

5 Commits

Author SHA1 Message Date
devops-engineer 331f502636 Merge branch 'main' into feat/workspace-provider-field
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
CI / Python Lint & Test (pull_request) Successful in 4s
E2E API Smoke Test / detect-changes (pull_request) Successful in 8s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 6s
Harness Replays / detect-changes (pull_request) Successful in 6s
CI / Detect changes (pull_request) Successful in 12s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 10s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 10s
E2E Chat / detect-changes (pull_request) Successful in 13s
Harness Replays / Harness Replays (pull_request) Successful in 2s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 10s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 7s
qa-review / approved (pull_request_target) Failing after 6s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 5s
gate-check-v3 / gate-check (pull_request_target) Successful in 11s
security-review / approved (pull_request_target) Failing after 6s
E2E Chat / E2E Chat (pull_request) Successful in 6s
sop-checklist / review-refire (pull_request_target) Has been skipped
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 15s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, local-postgres-e2
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request_target) Successful in 6s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 25s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 25s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 43s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m1s
sop-tier-check / tier-check (pull_request_target) Failing after 1m7s
CI / Platform (Go) (pull_request) Failing after 1m50s
CI / Canvas (Next.js) (pull_request) Failing after 8m8s
CI / all-required (pull_request) Has been skipped
CI / Canvas Deploy Status (pull_request) Has been skipped
2026-06-06 17:55:56 +00:00
devops-engineer d357f17be5 Merge branch 'main' into feat/workspace-provider-field
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
CI / Detect changes (pull_request) Successful in 8s
E2E API Smoke Test / detect-changes (pull_request) Successful in 7s
CI / Python Lint & Test (pull_request) Successful in 7s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 9s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 5s
Harness Replays / detect-changes (pull_request) Successful in 6s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 9s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 11s
E2E Chat / detect-changes (pull_request) Successful in 15s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 5s
sop-checklist / review-refire (pull_request_target) Has been skipped
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 4s
security-review / approved (pull_request_target) Failing after 6s
qa-review / approved (pull_request_target) Failing after 7s
gate-check-v3 / gate-check (pull_request_target) Successful in 7s
E2E Chat / E2E Chat (pull_request) Successful in 2s
Harness Replays / Harness Replays (pull_request) Successful in 5s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 14s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, local-postgres-e2
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request_target) Successful in 15s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 13s
sop-tier-check / tier-check (pull_request_target) Failing after 12s
CI / Platform (Go) (pull_request) Failing after 36s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 58s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 46s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 1m11s
CI / Canvas (Next.js) (pull_request) Failing after 6m52s
CI / all-required (pull_request) Has been skipped
CI / Canvas Deploy Status (pull_request) Has been skipped
2026-06-06 15:10:40 +00:00
devops-engineer a76386efc2 Merge branch 'main' into feat/workspace-provider-field
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
CI / Detect changes (pull_request) Successful in 6s
CI / Python Lint & Test (pull_request) Successful in 8s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 5s
E2E API Smoke Test / detect-changes (pull_request) Successful in 11s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 5s
Harness Replays / detect-changes (pull_request) Successful in 5s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 4s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 11s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 16s
E2E Chat / detect-changes (pull_request) Successful in 17s
sop-checklist / review-refire (pull_request_target) Has been skipped
qa-review / approved (pull_request_target) Failing after 6s
gate-check-v3 / gate-check (pull_request_target) Successful in 7s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, local-postgres-e2
sop-checklist / na-declarations (pull_request) N/A: (none)
E2E Chat / E2E Chat (pull_request) Successful in 2s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 15s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 2s
sop-checklist / all-items-acked (pull_request_target) Successful in 5s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 13s
Harness Replays / Harness Replays (pull_request) Successful in 10s
security-review / approved (pull_request_target) Failing after 13s
sop-tier-check / tier-check (pull_request_target) Failing after 10s
CI / Platform (Go) (pull_request) Failing after 39s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Failing after 48s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 59s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m14s
CI / Canvas (Next.js) (pull_request) Failing after 6m18s
CI / all-required (pull_request) Has been skipped
CI / Canvas Deploy Status (pull_request) Has been skipped
2026-06-06 12:30:39 +00:00
core-devops 26ea3f8322 fix(workspace,canvas): Hetzner real shapes cpx/cax (verified vs live API)
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 3s
CI / Python Lint & Test (pull_request) Successful in 3s
Harness Replays / detect-changes (pull_request) Successful in 5s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 3s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 4s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 2s
E2E API Smoke Test / detect-changes (pull_request) Successful in 9s
E2E Chat / detect-changes (pull_request) Successful in 8s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
sop-checklist / review-refire (pull_request_target) Has been skipped
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, local-postgres-e2
sop-checklist / na-declarations (pull_request) N/A: (none)
gate-check-v3 / gate-check (pull_request_target) Successful in 7s
qa-review / approved (pull_request_target) Failing after 6s
security-review / approved (pull_request_target) Failing after 5s
sop-checklist / all-items-acked (pull_request_target) Successful in 4s
Harness Replays / Harness Replays (pull_request) Successful in 1s
E2E Chat / E2E Chat (pull_request) Successful in 2s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 15s
CI / Detect changes (pull_request) Successful in 16s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 14s
sop-tier-check / tier-check (pull_request_target) Failing after 5s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 1s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 3s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m0s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 57s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 2m44s
CI / Platform (Go) (pull_request) Successful in 3m54s
CI / Canvas (Next.js) (pull_request) Failing after 6m31s
CI / all-required (pull_request) Has been skipped
CI / Canvas Deploy Status (pull_request) Has been skipped
The hardcoded Hetzner instance lists used cx/ccx, but the live Hetzner project
(verified via HCLOUD_TOKEN against api.hetzner.cloud) offers cpx (AMD) + cax
(ARM). Aligned both the workspace-server allowlist and the canvas Container
Config selector to the real shapes, in sync with controlplane's RateCatalog:

  cpx11/cpx21/cpx31/cpx41/cpx51 (AMD) + cax21/cax31/cax41 (ARM)

Without this the allowlist would reject the real default (cpx31) and the UI
would offer shapes that don't exist.

Verified: workspace-server go build + compute tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 16:33:57 -07:00
core-devops 95631c0bb2 feat(workspace,canvas): provider field + Container Config provider selector
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 3s
CI / Python Lint & Test (pull_request) Successful in 7s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 6s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 7s
Harness Replays / detect-changes (pull_request) Successful in 7s
sop-checklist / review-refire (pull_request_target) Has been skipped
gate-check-v3 / gate-check (pull_request_target) Successful in 8s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, local-postgres-e2
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request_target) Successful in 4s
security-review / approved (pull_request_target) Failing after 5s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 13s
E2E Chat / detect-changes (pull_request) Successful in 13s
sop-tier-check / tier-check (pull_request_target) Failing after 4s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 12s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 12s
Harness Replays / Harness Replays (pull_request) Successful in 1s
E2E Chat / E2E Chat (pull_request) Successful in 2s
CI / Detect changes (pull_request) Successful in 21s
E2E API Smoke Test / detect-changes (pull_request) Successful in 21s
qa-review / approved (pull_request_target) Failing after 13s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 7s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 3s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 1m4s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m30s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2m22s
CI / Platform (Go) (pull_request) Successful in 10m48s
CI / Canvas (Next.js) (pull_request) Failing after 10m53s
CI / all-required (pull_request) Has been skipped
CI / Canvas Deploy Status (pull_request) Has been skipped
Phase 4 (molecule-core side) of the multi-provider abstraction
(controlplane RFC docs/design/rfc-multi-provider-provisioner.md).

workspace-server:
- WorkspaceCompute.Provider ("aws"|"hetzner"|"gcp"; empty→aws for back-compat).
- Per-provider instance allowlist (validateWorkspaceCompute) — AWS shapes
  unchanged, Hetzner cx/ccx + GCP e2/n2 added; unknown provider or
  wrong-provider shape rejected. provider round-trips into the persisted
  compute JSON. Tests added.

canvas:
- WorkspaceCompute.provider type.
- Container Config tab: a Cloud-provider selector ABOVE Instance type that
  drives the instance-type list (selecting a provider resets to a valid shape).
  Default AWS until the CTO-gated flip.

SSOT note: instance lists are still literals here (matching the tab's existing
hardcoded pattern) with a TODO to source from the CP provider catalog — kept in
ONE place per file and in sync with workspace-server's allowlist + controlplane's
RateCatalog until the catalog endpoint lands.

Verified: workspace-server go build + compute tests pass; canvas tsc --noEmit
clean for the changed files (pre-existing unrelated test errors only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:41:06 -07:00
5 changed files with 132 additions and 15 deletions
@@ -6,7 +6,21 @@ import { runtimeDisplayName } from "@/lib/runtime-names";
import { useCanvasStore, type WorkspaceNodeData } from "@/store/canvas";
import type { WorkspaceCompute } from "@/store/socket";
const INSTANCE_TYPES = ["t3.medium", "t3.large", "t3.xlarge", "t3.2xlarge", "m6i.large", "m6i.xlarge", "c6i.xlarge"];
// Multi-provider RFC: provider drives the instance-type list. AWS is the only
// default-visible option until the CTO-gated flip; Hetzner/GCP are shown when
// the backend reports them registered + the customer is entitled.
// TODO(SSOT): replace these literals with the CP provider catalog (the same
// SSOT that prices the shapes) — keep in sync with workspace-server's
// workspaceComputeInstanceAllowlistByProvider and controlplane's RateCatalog
// until the catalog endpoint lands.
const PROVIDERS = ["aws", "hetzner", "gcp"];
const PROVIDER_INSTANCE_TYPES: Record<string, string[]> = {
aws: ["t3.medium", "t3.large", "t3.xlarge", "t3.2xlarge", "m6i.large", "m6i.xlarge", "c6i.xlarge"],
hetzner: ["cpx11", "cpx21", "cpx31", "cpx41", "cpx51", "cax21", "cax31", "cax41"],
gcp: ["e2-small", "e2-standard-2", "e2-standard-4", "n2-standard-2", "n2-standard-4"],
};
const providerLabel = (p: string): string =>
p === "aws" ? "AWS" : p === "hetzner" ? "Hetzner (cost)" : p === "gcp" ? "Google Cloud" : p;
const RUNTIME_OPTIONS = ["claude-code", "codex", "hermes", "openclaw", "kimi", "kimi-cli", "external"];
const RESOLUTIONS = ["1280x720", "1440x900", "1920x1080", "2560x1440"];
const DEFAULT_HEADLESS_INSTANCE_TYPE = "t3.medium";
@@ -23,6 +37,7 @@ type Props = {
type FormState = {
runtime: string;
provider: string;
instanceType: string;
rootGB: string;
displayEnabled: boolean;
@@ -40,6 +55,7 @@ const dataPersistenceLabel = (v: string): string =>
export function ContainerConfigTab({ workspaceId, data }: Props) {
const runtime = data.runtime;
const provider = data.compute?.provider;
const instanceType = data.compute?.instance_type;
const rootGB = data.compute?.volume?.root_gb;
const displayMode = data.compute?.display?.mode;
@@ -48,8 +64,8 @@ export function ContainerConfigTab({ workspaceId, data }: Props) {
const displayHeight = data.compute?.display?.height;
const dataPersistence = data.compute?.data_persistence;
const initial = useMemo(
() => formFromData({ runtime, instanceType, rootGB, displayMode, displayProtocol, displayWidth, displayHeight, dataPersistence }),
[runtime, instanceType, rootGB, displayMode, displayProtocol, displayWidth, displayHeight, dataPersistence],
() => formFromData({ runtime, provider, instanceType, rootGB, displayMode, displayProtocol, displayWidth, displayHeight, dataPersistence }),
[runtime, provider, instanceType, rootGB, displayMode, displayProtocol, displayWidth, displayHeight, dataPersistence],
);
const [form, setForm] = useState<FormState>(initial);
const [saving, setSaving] = useState(false);
@@ -87,6 +103,7 @@ export function ContainerConfigTab({ workspaceId, data }: Props) {
const [width, height] = form.resolution.split("x").map((v) => parseInt(v, 10));
const compute: WorkspaceCompute = {
provider: form.provider,
instance_type: form.instanceType,
volume: { root_gb: rootGB },
display: form.displayEnabled
@@ -139,11 +156,27 @@ export function ContainerConfigTab({ workspaceId, data }: Props) {
optionLabel={runtimeDisplayName}
onChange={(runtime) => setForm((s) => ({ ...s, runtime }))}
/>
<SelectField
id="cloud-provider"
label="Cloud provider"
value={form.provider}
options={PROVIDERS}
optionLabel={providerLabel}
onChange={(provider) =>
setForm((s) => {
// Provider drives the instance-type set; reset to the new
// provider's first shape when the current one isn't valid there.
const shapes = PROVIDER_INSTANCE_TYPES[provider] ?? [];
const instanceType = shapes.includes(s.instanceType) ? s.instanceType : (shapes[0] ?? s.instanceType);
return { ...s, provider, instanceType };
})
}
/>
<SelectField
id="instance-type"
label="Instance type"
value={form.instanceType}
options={INSTANCE_TYPES}
options={PROVIDER_INSTANCE_TYPES[form.provider] ?? PROVIDER_INSTANCE_TYPES.aws}
onChange={(instanceType) => setForm((s) => ({ ...s, instanceType }))}
/>
<label className="grid gap-1" htmlFor="root-volume-gb">
@@ -247,6 +280,7 @@ export function ContainerConfigTab({ workspaceId, data }: Props) {
function formFromData(data: {
runtime?: string;
provider?: string;
instanceType?: string;
rootGB?: number;
displayMode?: string;
@@ -260,6 +294,7 @@ function formFromData(data: {
const resolution = `${width}x${height}`;
return {
runtime: data.runtime || "claude-code",
provider: data.provider || "aws",
instanceType: data.instanceType || DEFAULT_HEADLESS_INSTANCE_TYPE,
rootGB: String(data.rootGB || DEFAULT_HEADLESS_ROOT_GB),
displayEnabled: !!data.displayMode && data.displayMode !== "none",
+3
View File
@@ -358,6 +358,9 @@ export interface WorkspaceData {
}
export interface WorkspaceCompute {
// Cloud backend: "aws" | "hetzner" | "gcp". undefined = CP default (AWS until
// the CTO-gated flip; multi-provider RFC). Drives the valid instance_type set.
provider?: string;
instance_type?: string;
volume?: {
root_gb?: number;
@@ -31,20 +31,49 @@ type workspaceDisplayResponse struct {
Status string `json:"status,omitempty"`
}
var workspaceComputeInstanceAllowlist = map[string]struct{}{
"t3.medium": {},
"t3.large": {},
"t3.xlarge": {},
"t3.2xlarge": {},
"m6i.large": {},
"m6i.xlarge": {},
"c6i.xlarge": {},
// workspaceComputeInstanceAllowlistByProvider is the per-provider set of
// selectable shapes (multi-provider RFC). AWS keeps its existing list; Hetzner
// and GCP add theirs. Empty provider normalizes to "aws" so pre-multi-provider
// workspaces validate unchanged.
//
// TODO(SSOT): source this from the CP provider catalog (the same SSOT the
// Container Config selector reads) rather than a second hardcoded copy — keep
// in sync with controlplane internal/credits providerInstanceUSDPerHour until
// the catalog endpoint lands.
var workspaceComputeInstanceAllowlistByProvider = map[string]map[string]struct{}{
"aws": {
"t3.medium": {}, "t3.large": {}, "t3.xlarge": {}, "t3.2xlarge": {},
"m6i.large": {}, "m6i.xlarge": {}, "c6i.xlarge": {},
},
"hetzner": {
"cpx11": {}, "cpx21": {}, "cpx31": {}, "cpx41": {}, "cpx51": {},
"cax21": {}, "cax31": {}, "cax41": {},
},
"gcp": {
"e2-small": {}, "e2-standard-2": {}, "e2-standard-4": {},
"n2-standard-2": {}, "n2-standard-4": {},
},
}
func normalizeWorkspaceProvider(p string) string {
switch p {
case "aws", "hetzner", "gcp":
return p
case "":
return "aws" // back-compat: pre-multi-provider rows
default:
return "" // unknown → invalid (validated below)
}
}
func validateWorkspaceCompute(compute models.WorkspaceCompute) error {
provider := normalizeWorkspaceProvider(compute.Provider)
if provider == "" {
return fmt.Errorf("unsupported compute.provider (want aws|hetzner|gcp)")
}
if compute.InstanceType != "" {
if _, ok := workspaceComputeInstanceAllowlist[compute.InstanceType]; !ok {
return fmt.Errorf("unsupported compute.instance_type")
if _, ok := workspaceComputeInstanceAllowlistByProvider[provider][compute.InstanceType]; !ok {
return fmt.Errorf("unsupported compute.instance_type %q for provider %q", compute.InstanceType, provider)
}
}
if compute.Volume.RootGB != 0 {
@@ -107,7 +136,8 @@ func validateWorkspaceDisplayDimensions(width, height int) error {
}
func workspaceComputeIsZero(compute models.WorkspaceCompute) bool {
return compute.InstanceType == "" &&
return compute.Provider == "" &&
compute.InstanceType == "" &&
compute.Volume.RootGB == 0 &&
compute.Display.Mode == "" &&
compute.Display.Width == 0 &&
@@ -120,6 +150,9 @@ func workspaceComputeJSON(compute models.WorkspaceCompute) (string, error) {
return "{}", nil
}
out := map[string]interface{}{}
if compute.Provider != "" {
out["provider"] = compute.Provider
}
if compute.InstanceType != "" {
out["instance_type"] = compute.InstanceType
}
@@ -607,3 +607,44 @@ func TestWorkspaceDisplaySession_NonDisplayWorkspaceDoesNotProxy(t *testing.T) {
t.Errorf("unmet sqlmock expectations: %v", err)
}
}
// TestValidateWorkspaceCompute_Provider pins the multi-provider validation:
// per-provider instance allowlists, empty-provider back-compat (→ aws), and
// unknown providers rejected.
func TestValidateWorkspaceCompute_Provider(t *testing.T) {
ok := []models.WorkspaceCompute{
{InstanceType: "t3.large"}, // empty provider → aws
{Provider: "aws", InstanceType: "m6i.xlarge"}, // aws shape
{Provider: "hetzner", InstanceType: "cpx31"}, // hetzner shape
{Provider: "gcp", InstanceType: "e2-standard-2"}, // gcp shape
{Provider: "hetzner"}, // provider only, no shape
}
for _, c := range ok {
if err := validateWorkspaceCompute(c); err != nil {
t.Errorf("validateWorkspaceCompute(%+v) = %v, want nil", c, err)
}
}
bad := []models.WorkspaceCompute{
{Provider: "azure"}, // unknown provider
{Provider: "hetzner", InstanceType: "t3.large"}, // aws shape on hetzner
{Provider: "aws", InstanceType: "cpx31"}, // hetzner shape on aws
}
for _, c := range bad {
if err := validateWorkspaceCompute(c); err == nil {
t.Errorf("validateWorkspaceCompute(%+v) = nil, want error", c)
}
}
}
// TestWorkspaceComputeJSON_IncludesProvider ensures provider round-trips into
// the persisted compute JSON.
func TestWorkspaceComputeJSON_IncludesProvider(t *testing.T) {
js, err := workspaceComputeJSON(models.WorkspaceCompute{Provider: "hetzner", InstanceType: "cpx31"})
if err != nil {
t.Fatalf("workspaceComputeJSON: %v", err)
}
if !strings.Contains(js, `"provider":"hetzner"`) {
t.Fatalf("compute JSON missing provider: %s", js)
}
}
@@ -165,6 +165,11 @@ type WorkspaceComputeDisplay struct {
}
type WorkspaceCompute struct {
// Provider is the cloud backend this workspace runs on: "aws" | "hetzner" |
// "gcp". Empty = the CP's default provider (AWS until the CTO-gated flip;
// multi-provider RFC). Determines which instance_type values are valid and
// is forwarded to CP, which routes provisioning via its ProviderRegistry.
Provider string `json:"provider,omitempty"`
InstanceType string `json:"instance_type,omitempty"`
Volume WorkspaceComputeVolume `json:"volume,omitempty"`
Display WorkspaceComputeDisplay `json:"display,omitempty"`