From 9e7d9f0b826e6f67af7a2136f0be5c8025ff9e55 Mon Sep 17 00:00:00 2001 From: Zach Casper Date: Tue, 28 Apr 2026 13:24:51 -0500 Subject: [PATCH 1/5] First draft spec Signed-off-by: Zach Casper --- .../cli/2026-04-28-workspace-refactor.md | 264 ++++++++++++++++++ 1 file changed, 264 insertions(+) create mode 100644 eng/design-notes/cli/2026-04-28-workspace-refactor.md diff --git a/eng/design-notes/cli/2026-04-28-workspace-refactor.md b/eng/design-notes/cli/2026-04-28-workspace-refactor.md new file mode 100644 index 0000000000..1d53c0b8ac --- /dev/null +++ b/eng/design-notes/cli/2026-04-28-workspace-refactor.md @@ -0,0 +1,264 @@ +# Feature Specification: Replace Radius Workspaces with `rad configure --defaults` + +## Background + +### The workspace concept confuses users + +Radius today exposes a CLI-only concept called a **workspace**, configured through commands like `rad workspace create`, `rad workspace switch`, `rad workspace list`, and `rad workspace delete`. Despite the API-style verbs, a workspace is **not** a Radius API object — it is purely a client-side bundle stored in `~/.rad/config.yaml` that combines: + +- a Kubernetes connection (a kube context name), +- a default resource group (stored as a fully-qualified URI like `/planes/radius/local/resourceGroups/default`), and +- a default environment (also stored as a URI). + +This naming and command shape causes recurring user confusion: + +- **"Is a workspace an API resource?"** `rad workspace create` reads like `rad app create` or `rad env create`, which *do* create server-side objects. Users assume workspaces are similar and look for them in `rad resource list`, in the dashboard, or in their cluster's CRDs — where they do not exist. +- **"What is the difference between a workspace, a resource group, and an environment?"** New users see three concepts in the docs (`workspace`, `group`, `environment`) but only two of them (group, environment) actually correspond to anything on the cluster. The third is a CLI-local container whose only job is to remember which group and environment to use by default. +- **"Why do I need to switch workspaces to switch resource groups?"** Day-to-day, what users actually want is to change which resource group or environment their next `rad` commands target. Today that is spread across three commands (`rad workspace switch`, `rad group switch`, `rad env switch`) with subtly different semantics, plus per-command `--workspace`, `--group`, and `--environment` flags whose precedence is not obvious. +- **"Why does my workspace stop working when I switch clusters?"** A workspace pins a specific kube context. If the user switches clusters with `kubectl config use-context`, the workspace either silently uses the old (now-wrong) context or fails opaquely, depending on how the workspace was set up. + +The net effect is a surface area whose primary job is to remember two strings (a default group name and a default environment name) per cluster, but which appears to users as a fourth top-level concept on equal footing with apps, environments, and resource groups. + +### `az configure --defaults` as the model + +The `az` CLI solves the same problem with a much smaller surface: `az configure --defaults group=my-rg location=westus2`. There is no client-side "workspace" object — `az` simply remembers a few key/value defaults and applies them to subsequent commands. Users discover the feature once, learn one command, and never have to reason about a CLI-only entity that mirrors API objects. + +This refactor adopts the same model for Radius. Users set defaults with `rad configure --defaults group= environment=`, list them with `rad configure --list-defaults`, and clear individual keys by setting them to an empty value. The defaults are scoped to the active Kubernetes context (so switching clusters with `kubectl config use-context` automatically selects the right defaults), and Radius commands fall back through a clear, per-key precedence chain when a default is not set. New users never encounter the word "workspace"; existing users keep working without manual migration because the legacy `workspaces:` block is still read. + +The "workspace" concept is thereby downgraded from a top-level CLI noun to an internal storage detail and a one-shot `-w` flag for users who already rely on named workspace bundles. + +## Storage strategy + +The existing `workspaces:` block in `~/.rad/config.yaml` is **never written** by new commands and is **read only as a fallback** when no `defaults:` entry matches the active Kubernetes context. New commands write a single new top-level block: + +```yaml +defaults: + my-kubecontext: + group: my-group + environment: my-env + my-other-kubecontext: + group: prod-group + environment: prod-env +workspaces: # read-only fallback (unchanged on disk by new commands) + default: default + items: + default: + connection: { context: my-kubecontext, kind: kubernetes } + scope: /planes/radius/local/resourceGroups/default + environment: /planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/default +``` + +**Resolution order** for the group/environment used by a `rad` command (highest precedence first): + +1. **Per-command `-g/--group` / `-e/--environment` flag** on the individual command. +2. **Per-command `-w/--workspace ` flag**: if present, the named workspace from `workspaces.items.` supplies group (from `scope`), environment (from `environment`), and Kubernetes connection (from `connection.context`) for keys not satisfied by step 1. +3. **`defaults..`** in the new `defaults:` block. +4. **`workspaces.default`** — the entry named by `workspaces.default` supplies group/environment/connection for any key still unresolved. +5. **Built-in fallback**: the literal name `default` is used for `group` and/or `environment` if still unresolved. Because `rad init` creates a resource group named `default` and an environment named `default`, this gives a working zero-config experience without `rad init` needing to write to `~/.rad/config.yaml` at all. +6. Error only if even the built-in `default` fallback is unusable (e.g., the user has explicitly cleared a default with `rad configure --defaults group=` and the literal `default` group does not exist on the cluster). The remediation message names `--group`/`--environment`, `--workspace`, and `rad configure --defaults`. + +Resolution is **per key independently**: e.g., `--group` set on the command line, `environment` resolved from `defaults:`, and Kubernetes connection from `workspaces.default` is a valid combination. Steps stop at the first source that supplies each key. + +The Kubernetes connection has no `default` literal fallback — it must come from `-w`, `workspaces.default`, or the active kube context (the active context is implicit and used by all sources in steps 3–5 unless `-w` or `workspaces.default` overrides it). + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - First-time user runs `rad init` with no config file written for defaults (Priority: P1) + +A new user installs the CLI, points `kubectl` at their cluster, and runs `rad init`. The CLI installs the Radius control plane and creates a resource group named `default` and an environment named `default` on the cluster. **It does not need to write a `defaults:` block to `~/.rad/config.yaml`** — subsequent `rad` commands resolve to the literal name `default` via the built-in fallback (FR-012 step 5). No "workspace" is created, named, or mentioned anywhere in user-visible output. + +**Why this priority**: This is the entry point for every new user. The combination of "no workspace concept" and "no required config file write" is the defining shape of the new UX. + +**Independent Test**: Run `rad init` on a fresh machine, then run `rad env show` with no flags. Confirm that the environment named `default` from the resource group named `default` is shown \u2014 verifying that `rad init` created both, that the literal-`default` fallback resolved both keys without any `defaults:` block being written, and that no user-facing output mentioned \"workspace\". + +**Acceptance Scenarios**: + +1. **Given** a fresh machine with no Radius config and active kube context `my-kubecontext`, **When** the user runs `rad init` and accepts the defaults, **Then** a resource group named `default` and an environment named `default` are created on the cluster, no `defaults:` block is written to `~/.rad/config.yaml` solely for this purpose, and the success message contains no occurrence of "workspace". +2. **Given** a successful `rad init`, **When** the user runs `rad app list` immediately afterward without flags, **Then** the command succeeds by resolving to group `default` and environment `default` via the built-in literal fallback. +3. **Given** a successful `rad init`, **When** the user runs `rad workspace list` or `rad env switch` or `rad group switch`, **Then** the CLI fails with a "command removed" error that names the `rad configure --defaults` replacement. +4. **Given** a successful `rad init` followed by `rad configure --defaults group=my-rg`, **When** the user runs `rad app list`, **Then** the command targets `my-rg` (the configured default takes precedence over the literal `default` fallback). + +--- + +### User Story 2 - Set, list, and clear defaults with `rad configure` (Priority: P1) + +A user manages defaults via `rad configure --defaults =` (set), `rad configure --defaults =` (clear, empty value), and `rad configure --list-defaults` (inspect). + +**Why this priority**: This is the primary replacement surface for `rad workspace switch`, `rad group switch`, and `rad env switch`. Users must be able to perform every workspace-default operation through `rad configure`. + +**Independent Test**: From any valid Radius config, run `rad configure --defaults group=dev-rg`, `rad configure --defaults environment=dev-env`, then `rad configure --list-defaults`. Verify the values appear under the active kube context and that a scoped command uses them. Then run `rad configure --defaults group=` and verify the next scoped command falls through to the built-in literal `default` fallback (or fails with a clear remediation message if no resource group named `default` exists on the cluster). + +**Acceptance Scenarios**: + +1. **Given** an active kube context `my-kubecontext` and no existing defaults entry for it, **When** the user runs `rad configure --defaults group=my-rg`, **Then** the CLI validates that `my-rg` exists on the cluster connected via `my-kubecontext`, persists `defaults.my-kubecontext.group: my-rg` to `~/.rad/config.yaml`, and prints a confirmation including the kube context, key, and new value. +2. **Given** existing defaults for `my-kubecontext`, **When** the user runs `rad configure --defaults environment=my-env`, **Then** the CLI validates the environment within the configured default group and persists `defaults.my-kubecontext.environment: my-env`. +3. **Given** a config with defaults for one or more contexts, **When** the user runs `rad configure --list-defaults`, **Then** the CLI prints all `defaults:` entries grouped by kube context (with the active context indicated), and supports `--output json` returning a stable schema. +4. **Given** a default group is set for the active context, **When** the user runs `rad configure --defaults group=` (empty value), **Then** that single key is removed from `defaults.`. Subsequent commands fall through the resolution order; if no `workspaces.default` supplies the key, they fall through to the literal `default` fallback. If the literal `default` group does not exist on the cluster, the command fails with the standard remediation message. +5. **Given** removing the last key for a context leaves an empty map, **When** the operation completes, **Then** the empty `defaults.` entry is removed from the file (no empty `{}` left behind). +6. **Given** the user passes `rad configure --defaults group=` for a group that does not exist on the connected cluster, **When** the command runs, **Then** the CLI fails with a clear error and does not modify the config file. +7. **Given** the user passes multiple key/value pairs, e.g. `rad configure --defaults group=my-rg environment=my-env`, **When** the command runs, **Then** the CLI validates and persists all of them atomically (all-or-nothing) under the active kube context. +8. **Given** the user passes an unknown key, e.g. `rad configure --defaults foo=bar`, **When** the command runs, **Then** the CLI fails with a clear error listing the supported keys (`group`, `environment`) and does not modify the file. + +--- + +### User Story 3 - Per-context defaults make kube context switches automatic (Priority: P1) + +A user configures defaults for two kube contexts (`dev-cluster` and `prod-cluster`). They switch between them with `kubectl config use-context` and run `rad` commands. Each `rad` invocation automatically picks up the right defaults for the active context — no warnings, no prompts, no `rad config set context` step. + +**Why this priority**: This is the design's signature feature. The kube-context-keyed `defaults:` block replaces the static workspace-to-context binding and eliminates the mismatch class of problems entirely. + +**Independent Test**: Create defaults for two contexts, switch contexts via `kubectl`, run scoped commands without flags, and verify each one targets the matching cluster's defaults. + +**Acceptance Scenarios**: + +1. **Given** `defaults.dev-cluster.group: dev-rg` and `defaults.prod-cluster.group: prod-rg`, and active context `dev-cluster`, **When** the user runs `rad app list`, **Then** the command targets `dev-cluster` with group `dev-rg`. +2. **Given** the same config, **When** the user runs `kubectl config use-context prod-cluster && rad app list`, **Then** the command targets `prod-cluster` with group `prod-rg` automatically — no warning or prompt. +3. **Given** active kube context `unknown-cluster` and no `defaults.unknown-cluster` entry, no `workspaces.default` resolving the missing keys, and resource groups/environments named `default` exist on `unknown-cluster`, **When** the user runs `rad app list`, **Then** the command succeeds via the literal `default` fallback. If those literal-`default` resources do not exist, the command fails with a remediation message naming `--group`, `--workspace`, and `rad configure --defaults`. +4. **Given** the user has no active kube context (e.g., `KUBECONFIG` empty or current-context unset) and no `--workspace` flag is passed, **When** the user runs any scoped `rad` command, **Then** the CLI fails fast with a remediation message pointing at `kubectl config use-context` and `rad init`. (If `--workspace ` is passed, that workspace's `connection.context` is used and the command proceeds.) + +--- + +### User Story 4 - Per-command flags override; `--workspace` remains a one-shot selector (Priority: P1) + +Every scoped command supports `-g/--group`, `-e/--environment`, and `-w/--workspace` flags. Per-command `-g`/`-e` flags always win. `-w ` selects a named workspace from the legacy `workspaces.items` block as a single unit (its `connection.context`, `scope`, and `environment` are used for any keys not already supplied by `-g`/`-e`). None of these flags mutate the config file. + +**Why this priority**: Without consistent override semantics, the new model is worse than workspaces. `-w` is preserved so existing scripts and users who rely on named workspaces keep working without re-tooling. + +**Independent Test**: With defaults set, run scoped commands without flags and confirm they target the defaults. Repeat with `-g` and `-e` and confirm they override without changing the config. Repeat with `-w ` against a config that contains a non-default named workspace and confirm the command targets that workspace's group, environment, and connection — still without mutating the file. + +**Acceptance Scenarios**: + +1. **Given** `defaults.my-kubecontext.group: dev-rg, environment: dev-env` and active context `my-kubecontext`, **When** the user runs `rad deploy app.bicep`, **Then** the deployment targets `dev-rg`/`dev-env` and the config file is unchanged. +2. **Given** the same defaults, **When** the user runs `rad deploy app.bicep -g prod-rg -e prod-env`, **Then** the deployment targets `prod-rg`/`prod-env`, the config file is unchanged, and no prompt or warning about workspaces appears. +3. **Given** a config containing `workspaces.items.azure` with its own `connection.context`, `scope`, and `environment`, **When** the user runs `rad deploy app.bicep -w azure`, **Then** the deployment uses that workspace's connection, group, and environment as a unit; per-command `-g`/`-e` if also supplied still take precedence over the workspace's values. +4. **Given** no group default for the active context, no `--workspace`, no legacy `workspaces.default` resolving the key, **and** no resource group named `default` exists on the cluster, **When** the user runs `rad app list` without `--group`, **Then** the CLI fails with a remediation message that names `--group`, `--workspace`, and `rad configure --defaults`. (If a resource group named `default` does exist, the literal-`default` fallback satisfies the key and the command succeeds.) + +--- + +### User Story 5 - Read-only back-compat with the legacy `workspaces:` block (Priority: P2) + +An existing user upgrades the CLI without re-running `rad init`. Their `~/.rad/config.yaml` already contains a populated `workspaces:` block. Their workflows continue to work unchanged. The legacy block is read but never written by the new CLI; the management commands are gone but the data and the `--workspace` flag remain functional. + +**Why this priority**: Keeps existing users from being broken on upgrade, while still moving the codebase to the new model. + +**Independent Test**: Take an existing pre-refactor `~/.rad/config.yaml` (with `workspaces.default` and at least one item with `scope`, `environment`, `connection.context`). Upgrade the CLI. Run `rad app list` without modifying the file. Verify the command resolves group, environment, and Kubernetes connection from `workspaces.default`. + +**Acceptance Scenarios**: + +1. **Given** a pre-refactor config with `workspaces.default: default`, `workspaces.items.default.connection.context: my-kubecontext`, `workspaces.items.default.scope: /planes/radius/local/resourceGroups/foo`, and `workspaces.items.default.environment: /planes/radius/local/resourceGroups/foo/providers/Applications.Core/environments/bar`, **When** the user runs `rad app list`, **Then** the command targets group `foo`, environment `bar`, and the cluster reachable via kube context `my-kubecontext`, all resolved from `workspaces.default`. +2. **Given** the same config, **When** the user runs `rad configure --list-defaults`, **Then** the listing includes the values resolved from `workspaces.default`, clearly labels them as coming from the legacy block, and points the user at `rad configure --defaults group=...` if they want to migrate. +3. **Given** the same config, **When** the user runs `rad configure --defaults group=new-rg` while the active kube context is `my-kubecontext`, **Then** the CLI writes `defaults.my-kubecontext.group: new-rg`, **does not modify** the `workspaces:` block, and on subsequent commands `defaults.my-kubecontext.group` takes precedence over `workspaces.default.scope`. +4. **Given** no `defaults:` entry for the active kube context, no `workspaces.default`, and no resource group/environment named `default` on the cluster, **When** any scoped command runs without `-g`/`-e`/`-w`, **Then** it fails with the standard remediation message naming all three flags and `rad configure --defaults`. (If `default`/`default` resources exist, the literal fallback satisfies the keys and the command succeeds.) +5. **Given** the user invokes `rad workspace create/list/show/switch/delete`, `rad group switch`, or `rad env switch`, **When** they run, **Then** they fail with a "command removed" error naming the `rad configure --defaults` replacement and pointing at migration docs. The `--workspace ` flag, however, remains functional on scoped commands. + +--- + +### User Story 6 - JSON / scripting friendliness (Priority: P3) + +A user automating Radius operations in CI wants a stable, scriptable interface for reading and setting defaults. + +**Why this priority**: Useful but not blocking; humans can use the table output during the rollout. + +**Independent Test**: Run `rad configure --list-defaults --output json` and pipe to `jq`. Run `rad configure --defaults group=` in CI without TTY and confirm non-interactive success when the target exists. + +**Acceptance Scenarios**: + +1. **Given** a configured CLI, **When** the user runs `rad configure --list-defaults --output json`, **Then** the output is valid JSON keyed by kube context with stable field names (`group`, `environment`, plus a `source` field of `"defaults"` or `"workspaces"`). +2. **Given** a CI environment with no TTY, **When** the user runs `rad configure --defaults group=my-rg`, **Then** the command succeeds without prompts when the group exists, and fails non-zero with a stable error code when it does not. + +--- + +### Edge Cases + +- **Cluster unreachable during `rad configure --defaults`**: Fail without mutating the config; clearly distinguish "cluster unreachable" from "group not found". +- **Group default set, environment unset, no `default` environment on cluster**: Commands needing only a group succeed; commands needing an environment fall through to the literal `default` fallback and fail with a precise "no default environment" message only if a `default` environment does not exist. +- **Both unset, fresh install before `rad init`**: `rad configure --list-defaults` prints an empty result and a hint to run `rad init`. +- **Stale environment value**: Default environment was deleted out of band. Next scoped command fails with a remediation (run `rad env list`, then `rad configure --defaults environment=`). +- **Concurrent edits to `~/.rad/config.yaml`**: Two `rad configure --defaults …` invocations run in parallel; the file must not become corrupted (last-writer-wins with file locking is acceptable). +- **`rad init` re-run on an already-configured machine**: Existing defaults for the active context must not be silently overwritten without user confirmation. +- **Active kube context contains characters unusual in YAML keys** (dots, slashes, colons): These MUST be preserved verbatim as the YAML map key (quoted as needed). +- **Two contexts point at the same cluster**: Treated as independent entries in `defaults:`; the user can configure them identically or differently. No deduplication by Radius. +- **`KUBECONFIG` references multiple files / a non-default path**: The "active kube context" is resolved via the standard kubeconfig precedence rules used elsewhere in the CLI; no new resolution logic is introduced. +- **Legacy block contains a workspace whose `connection.context` matches the active kube context AND `defaults.` exists**: `defaults:` always wins over `workspaces.default`, key by key. Missing keys fall through to `workspaces.default` per the resolution order — the two sources are merged per-key. +- **`-w ` names a workspace that does not exist in `workspaces.items`**: Command fails with a clear error before contacting the cluster. +- **`-w ` plus `-g`/`-e` flags**: `-g`/`-e` win for those keys; the workspace supplies the remaining keys (notably `connection.context`). +- **`-w` is set but the workspace has no `scope` or `environment`** (e.g., the `azure` entry in the user's example config): the workspace supplies only `connection.context`; group/environment must come from `-g`/`-e`/`defaults:`/`workspaces.default` per the standard resolution order, otherwise a clear error. + +## Requirements *(mandatory)* + +### Functional Requirements + +#### Command surface + +- **FR-001**: The CLI MUST expose a `rad configure` command. It MUST support `--defaults = [=…]` (set), `--defaults =` with an empty value (clear that key), and `--list-defaults` (inspect). `--list-defaults` MUST support `--output json`. +- **FR-002**: Supported keys for `--defaults` MUST include `group` and `environment`. Unknown keys MUST fail the command with a message listing supported keys. +- **FR-003**: All `--defaults` operations MUST always target the entry for the **active Kubernetes context** in the new `defaults:` block. There is no flag to target a different context. +- **FR-004**: `rad configure --defaults group=` MUST validate that the named resource group exists on the cluster reachable via the active kube context before persisting. `rad configure --defaults environment=` MUST validate the environment exists within the configured (or already-set) default group. +- **FR-005**: When multiple keys are provided in one invocation (e.g., `group=… environment=…`), the operation MUST be atomic: validation of all keys MUST succeed before any write; on any failure, the file MUST be unchanged. +- **FR-006**: `rad configure --list-defaults` MUST display all entries from the `defaults:` block, plus any values resolved from the legacy `workspaces:` block (clearly labeled as such), with the active kube context highlighted. + +#### Storage and schema + +- **FR-007**: New commands MUST write defaults to a top-level `defaults:` block keyed by Kubernetes context name. Within each context entry, supported subkeys are `group` and `environment` (string values, names — not URIs). +- **FR-008**: New commands MUST NOT write to the legacy `workspaces:` block under any circumstance. +- **FR-009**: Setting a key to an empty value (clear) MUST remove that key from `defaults.`. If the resulting context entry has no remaining keys, the entry itself MUST be removed. If the resulting `defaults:` block is empty, the block MUST be removed. +- **FR-010**: `rad configure --defaults …` operations MUST be safe under concurrent execution (no file corruption); last-writer-wins is acceptable. The file MUST be written atomically (write-temp-then-rename or equivalent). +- **FR-011**: On any validation failure, the config file MUST remain byte-for-byte unchanged. + +#### Resolution and command behavior + +- **FR-012**: All scoped commands (including but not limited to `rad deploy`, `rad app list/show/delete/connections/status/graph`, `rad env list/show/create/delete`, `rad group list/show/create/delete`, `rad resource list/show/delete`, `rad recipe list/show/register/unregister`, `rad credential …`) MUST resolve group, environment, and Kubernetes connection using this exact order, applied **per key independently**: + 1. Per-command `-g/--group` / `-e/--environment` flag. + 2. Per-command `-w/--workspace ` flag: if present, the named entry in `workspaces.items.` supplies any keys (group from `scope`, environment from `environment`, connection from `connection.context`) not yet resolved by step 1. + 3. `defaults..` from the new `defaults:` block. + 4. `workspaces.default`: the workspace named by `workspaces.default` supplies any keys still unresolved (group from `scope`, environment from `environment`, connection from `connection.context`). + 5. **Built-in literal `default`**: for the `group` and `environment` keys only, the literal string `default` MUST be used if no earlier source supplied a value. This pairs with `rad init`'s creation of a resource group named `default` and an environment named `default` to deliver a working zero-config experience. + 6. Error — only reachable when the resolved Kubernetes connection cannot be determined, **or** when the user has explicitly cleared a default (e.g., via `rad configure --defaults group=`) and the literal `default` does not exist on the cluster. The remediation message MUST name `--group`, `--environment`, `--workspace`, and `rad configure --defaults`. +- **FR-013**: Per-key resolution MUST stop at the first source supplying a value for that key. Sources MAY supply different keys: e.g., `-g` from the command line, `environment` from `defaults:`, and `connection.context` from `workspaces.default` is a valid combined resolution. +- **FR-014**: When the Kubernetes connection cannot be determined from any source (no `-w`, no active kube context, no usable `workspaces.default`), any scoped command MUST fail fast with a remediation message and MUST NOT pick a default arbitrarily. +- **FR-015**: When the resolved Kubernetes connection refers to a cluster that is unreachable, command behavior MUST mirror today's behavior for unreachable clusters (this refactor does not change connection-failure semantics). + +#### `rad init` + +- **FR-016**: `rad init` MUST NOT create, name, or reference a "workspace" in any user-facing output. +- **FR-017**: `rad init` MUST create a resource group named `default` and an environment named `default` on the connected cluster (matching the literal-`default` fallback in FR-012 step 5). It MUST NOT write to `~/.rad/config.yaml` for the purpose of recording these defaults; the literal-`default` resolution rule provides the same effect with no file mutation. Users who want non-`default` names use `rad configure --defaults group= environment=` after `rad init`. +- **FR-017a**: `rad init` MAY still write `~/.rad/config.yaml` for non-default purposes (e.g., recording cloud-provider credentials or other configuration outside the `defaults:` and `workspaces:` blocks). Any such writes MUST NOT touch the `defaults:` or `workspaces:` blocks. + +#### Removal of legacy commands + +- **FR-018**: The following commands MUST be removed: `rad workspace create`, `rad workspace list`, `rad workspace show`, `rad workspace switch`, `rad workspace delete`, `rad group switch`, `rad env switch`. Invoking any of them MUST fail with a "command removed" error message that names the `rad configure --defaults` replacement and links to migration docs. +- **FR-019**: The `-w/--workspace ` **flag** on scoped commands MUST be **preserved**. It selects a named entry from `workspaces.items` for the duration of one command invocation per FR-012 step 2. Help text for the flag MUST describe it as a per-command override that reads the legacy `workspaces:` block. +- **FR-020**: The Go type `workspaces.Workspace` and related infrastructure MAY remain inside the codebase. It MUST NOT be exposed in any new public CLI surface other than the `--workspace` flag described in FR-019, and new help text/documentation MUST NOT introduce the term "workspace" outside the `--workspace` flag's own help and the migration guide. + +#### Documentation and discoverability + +- **FR-021**: All new help text, getting-started docs, and error messages MUST avoid introducing the term "workspace" to new users. Documentation MUST include a migration guide from `rad workspace`/`rad group switch`/`rad env switch` to `rad configure --defaults`, and from a populated `workspaces:` block to a `defaults:` block (showing equivalent commands). + +### Key Entities + +- **Defaults Entry**: A map keyed by Kubernetes context name. Each value contains the default `group` (resource group name) and `environment` (environment name) for `rad` commands run while that context is active. Stored under the new top-level `defaults:` key in `~/.rad/config.yaml`. Replaces the user-visible "workspace" concept. +- **Legacy Workspace Entry**: The pre-existing structure under `workspaces.items.` containing `connection`, `scope`, and `environment`. Read-only after this refactor. Used (a) per-key as a fallback when `defaults.` does not provide a value (the workspace named by `workspaces.default`), and (b) as a one-shot override when the user passes `-w ` on a command. +- **Active Kubernetes Context**: The current `current-context` resolved from the standard kubeconfig precedence chain. The lookup key for `defaults:`. The single source of truth for which cluster the CLI talks to. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: After `rad init` on a clean machine, a user can run `rad app list` and `rad deploy app.bicep` to completion **without ever seeing or typing the word "workspace"**. +- **SC-002**: Help text and getting-started docs contain **zero occurrences** of "workspace" outside the migration guide. +- **SC-003**: A user with an unmodified pre-refactor `~/.rad/config.yaml` can upgrade the CLI and run their previous scoped workflows with **no manual file edits** and **no command failures** caused by schema changes (assuming the active kube context matches the legacy default workspace's `connection.context`). +- **SC-004**: A user with `defaults:` entries for two kube contexts can switch between contexts via `kubectl config use-context` and run scoped `rad` commands against each cluster **with zero additional `rad` configuration steps** between switches. +- **SC-005**: `rad configure --defaults …` either succeeds and persists the value(s), or fails and leaves the config file byte-for-byte unchanged, in **100%** of test runs (including network failures, validation failures, and unknown keys). +- **SC-006**: Every operation previously achievable via `rad workspace switch`, `rad group switch`, or `rad env switch` is achievable via a single `rad configure --defaults …` invocation; **no operation requires more steps after the refactor than before**. +- **SC-007**: Time-to-first-deploy for a new user (time from `rad init` start to a successful `rad deploy app.bicep`) does not increase versus the pre-refactor baseline. +- **SC-008**: Inside the codebase, references to the legacy workspace types are confined to one well-defined compatibility shim package; no scoped command's resolution path reads workspace fields directly. + +## Assumptions + +- **Schema is additive, not replacing**: A new top-level `defaults:` key is added. The existing `workspaces:` key is read for back-compat but never written. This honors the "leave schema as-is if possible" priority while delivering a clean new UX. +- **Kube-context-keyed defaults remove the mismatch problem**: Because the lookup key is the active kube context, switching contexts in `kubectl` automatically selects the matching defaults. There is no longer a "stored context vs. active context" mismatch to detect, prompt about, or auto-update. The earlier proposed FR for context-mismatch UX is therefore moot and dropped from this revision. +- **Hard removal of legacy management commands; flag preserved**: `rad workspace*`, `rad group switch`, and `rad env switch` are removed. The `-w/--workspace ` flag on scoped commands is **preserved** as a one-shot selector that reads the legacy `workspaces.items` block. This keeps existing scripts working and gives users a per-command escape hatch without bringing back the workspace-management surface. +- **One defaults entry per active kube context**: Multiple named "profiles" per context are out of scope. Users wanting multiple environments on the same cluster can use `--group`/`--environment` flags or maintain separate kube contexts. +- **Kubernetes-only connections in scope**: This refactor targets the Kubernetes connection kind. Non-Kubernetes connection kinds (none currently end-user-facing) are out of scope. +- **No new auth model**: Authentication against Kubernetes and Radius control planes is unchanged. +- **Tests as parity oracle**: Existing functional tests for scoped commands provide the parity oracle: every test that passes pre-refactor must pass post-refactor, with `--workspace` removed and `--group`/`--environment` flags or `defaults:` entries used in its place. From 2f563476d2fa0ae0c2b155d4ed73f684d36dca2d Mon Sep 17 00:00:00 2001 From: Zach Casper Date: Wed, 6 May 2026 11:03:34 -0500 Subject: [PATCH 2/5] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Zach Casper --- eng/design-notes/cli/2026-04-28-workspace-refactor.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/design-notes/cli/2026-04-28-workspace-refactor.md b/eng/design-notes/cli/2026-04-28-workspace-refactor.md index 1d53c0b8ac..9f66425a70 100644 --- a/eng/design-notes/cli/2026-04-28-workspace-refactor.md +++ b/eng/design-notes/cli/2026-04-28-workspace-refactor.md @@ -69,7 +69,7 @@ A new user installs the CLI, points `kubectl` at their cluster, and runs `rad in **Why this priority**: This is the entry point for every new user. The combination of "no workspace concept" and "no required config file write" is the defining shape of the new UX. -**Independent Test**: Run `rad init` on a fresh machine, then run `rad env show` with no flags. Confirm that the environment named `default` from the resource group named `default` is shown \u2014 verifying that `rad init` created both, that the literal-`default` fallback resolved both keys without any `defaults:` block being written, and that no user-facing output mentioned \"workspace\". +**Independent Test**: Run `rad init` on a fresh machine, then run `rad env show` with no flags. Confirm that the environment named `default` from the resource group named `default` is shown — verifying that `rad init` created both, that the literal-`default` fallback resolved both keys without any `defaults:` block being written, and that no user-facing output mentioned "workspace". **Acceptance Scenarios**: From ad635e5e4c75185e28e695d6d98c099b96466d4d Mon Sep 17 00:00:00 2001 From: Zach Casper Date: Wed, 6 May 2026 11:04:55 -0500 Subject: [PATCH 3/5] Renamed to correct convention Signed-off-by: Zach Casper --- .../cli/2026-04-workspace-refactor.md | 266 ++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 eng/design-notes/cli/2026-04-workspace-refactor.md diff --git a/eng/design-notes/cli/2026-04-workspace-refactor.md b/eng/design-notes/cli/2026-04-workspace-refactor.md new file mode 100644 index 0000000000..01cca93fb2 --- /dev/null +++ b/eng/design-notes/cli/2026-04-workspace-refactor.md @@ -0,0 +1,266 @@ +# Feature Specification: Replace Radius Workspaces with `rad configure --defaults` + +Author: zachcasper + +## Background + +### The workspace concept confuses users + +Radius today exposes a CLI-only concept called a **workspace**, configured through commands like `rad workspace create`, `rad workspace switch`, `rad workspace list`, and `rad workspace delete`. Despite the API-style verbs, a workspace is **not** a Radius API object — it is purely a client-side bundle stored in `~/.rad/config.yaml` that combines: + +- a Kubernetes connection (a kube context name), +- a default resource group (stored as a fully-qualified URI like `/planes/radius/local/resourceGroups/default`), and +- a default environment (also stored as a URI). + +This naming and command shape causes recurring user confusion: + +- **"Is a workspace an API resource?"** `rad workspace create` reads like `rad app create` or `rad env create`, which *do* create server-side objects. Users assume workspaces are similar and look for them in `rad resource list`, in the dashboard, or in their cluster's CRDs — where they do not exist. +- **"What is the difference between a workspace, a resource group, and an environment?"** New users see three concepts in the docs (`workspace`, `group`, `environment`) but only two of them (group, environment) actually correspond to anything on the cluster. The third is a CLI-local container whose only job is to remember which group and environment to use by default. +- **"Why do I need to switch workspaces to switch resource groups?"** Day-to-day, what users actually want is to change which resource group or environment their next `rad` commands target. Today that is spread across three commands (`rad workspace switch`, `rad group switch`, `rad env switch`) with subtly different semantics, plus per-command `--workspace`, `--group`, and `--environment` flags whose precedence is not obvious. +- **"Why does my workspace stop working when I switch clusters?"** A workspace pins a specific kube context. If the user switches clusters with `kubectl config use-context`, the workspace either silently uses the old (now-wrong) context or fails opaquely, depending on how the workspace was set up. + +The net effect is a surface area whose primary job is to remember two strings (a default group name and a default environment name) per cluster, but which appears to users as a fourth top-level concept on equal footing with apps, environments, and resource groups. + +### `az configure --defaults` as the model + +The `az` CLI solves the same problem with a much smaller surface: `az configure --defaults group=my-rg location=westus2`. There is no client-side "workspace" object — `az` simply remembers a few key/value defaults and applies them to subsequent commands. Users discover the feature once, learn one command, and never have to reason about a CLI-only entity that mirrors API objects. + +This refactor adopts the same model for Radius. Users set defaults with `rad configure --defaults group= environment=`, list them with `rad configure --list-defaults`, and clear individual keys by setting them to an empty value. The defaults are scoped to the active Kubernetes context (so switching clusters with `kubectl config use-context` automatically selects the right defaults), and Radius commands fall back through a clear, per-key precedence chain when a default is not set. New users never encounter the word "workspace"; existing users keep working without manual migration because the legacy `workspaces:` block is still read. + +The "workspace" concept is thereby downgraded from a top-level CLI noun to an internal storage detail and a one-shot `-w` flag for users who already rely on named workspace bundles. + +## Storage strategy + +The existing `workspaces:` block in `~/.rad/config.yaml` is **never written** by new commands and is **read only as a fallback** when no `defaults:` entry matches the active Kubernetes context. New commands write a single new top-level block: + +```yaml +defaults: + my-kubecontext: + group: my-group + environment: my-env + my-other-kubecontext: + group: prod-group + environment: prod-env +workspaces: # read-only fallback (unchanged on disk by new commands) + default: default + items: + default: + connection: { context: my-kubecontext, kind: kubernetes } + scope: /planes/radius/local/resourceGroups/default + environment: /planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/default +``` + +**Resolution order** for the group/environment used by a `rad` command (highest precedence first): + +1. **Per-command `-g/--group` / `-e/--environment` flag** on the individual command. +2. **Per-command `-w/--workspace ` flag**: if present, the named workspace from `workspaces.items.` supplies group (from `scope`), environment (from `environment`), and Kubernetes connection (from `connection.context`) for keys not satisfied by step 1. +3. **`defaults..`** in the new `defaults:` block. +4. **`workspaces.default`** — the entry named by `workspaces.default` supplies group/environment/connection for any key still unresolved. +5. **Built-in fallback**: the literal name `default` is used for `group` and/or `environment` if still unresolved. Because `rad init` creates a resource group named `default` and an environment named `default`, this gives a working zero-config experience without `rad init` needing to write to `~/.rad/config.yaml` at all. +6. Error only if even the built-in `default` fallback is unusable (e.g., the user has explicitly cleared a default with `rad configure --defaults group=` and the literal `default` group does not exist on the cluster). The remediation message names `--group`/`--environment`, `--workspace`, and `rad configure --defaults`. + +Resolution is **per key independently**: e.g., `--group` set on the command line, `environment` resolved from `defaults:`, and Kubernetes connection from `workspaces.default` is a valid combination. Steps stop at the first source that supplies each key. + +The Kubernetes connection has no `default` literal fallback — it must come from `-w`, `workspaces.default`, or the active kube context (the active context is implicit and used by all sources in steps 3–5 unless `-w` or `workspaces.default` overrides it). + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - First-time user runs `rad init` with no config file written for defaults (Priority: P1) + +A new user installs the CLI, points `kubectl` at their cluster, and runs `rad init`. The CLI installs the Radius control plane and creates a resource group named `default` and an environment named `default` on the cluster. **It does not need to write a `defaults:` block to `~/.rad/config.yaml`** — subsequent `rad` commands resolve to the literal name `default` via the built-in fallback (FR-012 step 5). No "workspace" is created, named, or mentioned anywhere in user-visible output. + +**Why this priority**: This is the entry point for every new user. The combination of "no workspace concept" and "no required config file write" is the defining shape of the new UX. + +**Independent Test**: Run `rad init` on a fresh machine, then run `rad env show` with no flags. Confirm that the environment named `default` from the resource group named `default` is shown \u2014 verifying that `rad init` created both, that the literal-`default` fallback resolved both keys without any `defaults:` block being written, and that no user-facing output mentioned \"workspace\". + +**Acceptance Scenarios**: + +1. **Given** a fresh machine with no Radius config and active kube context `my-kubecontext`, **When** the user runs `rad init` and accepts the defaults, **Then** a resource group named `default` and an environment named `default` are created on the cluster, no `defaults:` block is written to `~/.rad/config.yaml` solely for this purpose, and the success message contains no occurrence of "workspace". +2. **Given** a successful `rad init`, **When** the user runs `rad app list` immediately afterward without flags, **Then** the command succeeds by resolving to group `default` and environment `default` via the built-in literal fallback. +3. **Given** a successful `rad init`, **When** the user runs `rad workspace list` or `rad env switch` or `rad group switch`, **Then** the CLI fails with a "command removed" error that names the `rad configure --defaults` replacement. +4. **Given** a successful `rad init` followed by `rad configure --defaults group=my-rg`, **When** the user runs `rad app list`, **Then** the command targets `my-rg` (the configured default takes precedence over the literal `default` fallback). + +--- + +### User Story 2 - Set, list, and clear defaults with `rad configure` (Priority: P1) + +A user manages defaults via `rad configure --defaults =` (set), `rad configure --defaults =` (clear, empty value), and `rad configure --list-defaults` (inspect). + +**Why this priority**: This is the primary replacement surface for `rad workspace switch`, `rad group switch`, and `rad env switch`. Users must be able to perform every workspace-default operation through `rad configure`. + +**Independent Test**: From any valid Radius config, run `rad configure --defaults group=dev-rg`, `rad configure --defaults environment=dev-env`, then `rad configure --list-defaults`. Verify the values appear under the active kube context and that a scoped command uses them. Then run `rad configure --defaults group=` and verify the next scoped command falls through to the built-in literal `default` fallback (or fails with a clear remediation message if no resource group named `default` exists on the cluster). + +**Acceptance Scenarios**: + +1. **Given** an active kube context `my-kubecontext` and no existing defaults entry for it, **When** the user runs `rad configure --defaults group=my-rg`, **Then** the CLI validates that `my-rg` exists on the cluster connected via `my-kubecontext`, persists `defaults.my-kubecontext.group: my-rg` to `~/.rad/config.yaml`, and prints a confirmation including the kube context, key, and new value. +2. **Given** existing defaults for `my-kubecontext`, **When** the user runs `rad configure --defaults environment=my-env`, **Then** the CLI validates the environment within the configured default group and persists `defaults.my-kubecontext.environment: my-env`. +3. **Given** a config with defaults for one or more contexts, **When** the user runs `rad configure --list-defaults`, **Then** the CLI prints all `defaults:` entries grouped by kube context (with the active context indicated), and supports `--output json` returning a stable schema. +4. **Given** a default group is set for the active context, **When** the user runs `rad configure --defaults group=` (empty value), **Then** that single key is removed from `defaults.`. Subsequent commands fall through the resolution order; if no `workspaces.default` supplies the key, they fall through to the literal `default` fallback. If the literal `default` group does not exist on the cluster, the command fails with the standard remediation message. +5. **Given** removing the last key for a context leaves an empty map, **When** the operation completes, **Then** the empty `defaults.` entry is removed from the file (no empty `{}` left behind). +6. **Given** the user passes `rad configure --defaults group=` for a group that does not exist on the connected cluster, **When** the command runs, **Then** the CLI fails with a clear error and does not modify the config file. +7. **Given** the user passes multiple key/value pairs, e.g. `rad configure --defaults group=my-rg environment=my-env`, **When** the command runs, **Then** the CLI validates and persists all of them atomically (all-or-nothing) under the active kube context. +8. **Given** the user passes an unknown key, e.g. `rad configure --defaults foo=bar`, **When** the command runs, **Then** the CLI fails with a clear error listing the supported keys (`group`, `environment`) and does not modify the file. + +--- + +### User Story 3 - Per-context defaults make kube context switches automatic (Priority: P1) + +A user configures defaults for two kube contexts (`dev-cluster` and `prod-cluster`). They switch between them with `kubectl config use-context` and run `rad` commands. Each `rad` invocation automatically picks up the right defaults for the active context — no warnings, no prompts, no `rad config set context` step. + +**Why this priority**: This is the design's signature feature. The kube-context-keyed `defaults:` block replaces the static workspace-to-context binding and eliminates the mismatch class of problems entirely. + +**Independent Test**: Create defaults for two contexts, switch contexts via `kubectl`, run scoped commands without flags, and verify each one targets the matching cluster's defaults. + +**Acceptance Scenarios**: + +1. **Given** `defaults.dev-cluster.group: dev-rg` and `defaults.prod-cluster.group: prod-rg`, and active context `dev-cluster`, **When** the user runs `rad app list`, **Then** the command targets `dev-cluster` with group `dev-rg`. +2. **Given** the same config, **When** the user runs `kubectl config use-context prod-cluster && rad app list`, **Then** the command targets `prod-cluster` with group `prod-rg` automatically — no warning or prompt. +3. **Given** active kube context `unknown-cluster` and no `defaults.unknown-cluster` entry, no `workspaces.default` resolving the missing keys, and resource groups/environments named `default` exist on `unknown-cluster`, **When** the user runs `rad app list`, **Then** the command succeeds via the literal `default` fallback. If those literal-`default` resources do not exist, the command fails with a remediation message naming `--group`, `--workspace`, and `rad configure --defaults`. +4. **Given** the user has no active kube context (e.g., `KUBECONFIG` empty or current-context unset) and no `--workspace` flag is passed, **When** the user runs any scoped `rad` command, **Then** the CLI fails fast with a remediation message pointing at `kubectl config use-context` and `rad init`. (If `--workspace ` is passed, that workspace's `connection.context` is used and the command proceeds.) + +--- + +### User Story 4 - Per-command flags override; `--workspace` remains a one-shot selector (Priority: P1) + +Every scoped command supports `-g/--group`, `-e/--environment`, and `-w/--workspace` flags. Per-command `-g`/`-e` flags always win. `-w ` selects a named workspace from the legacy `workspaces.items` block as a single unit (its `connection.context`, `scope`, and `environment` are used for any keys not already supplied by `-g`/`-e`). None of these flags mutate the config file. + +**Why this priority**: Without consistent override semantics, the new model is worse than workspaces. `-w` is preserved so existing scripts and users who rely on named workspaces keep working without re-tooling. + +**Independent Test**: With defaults set, run scoped commands without flags and confirm they target the defaults. Repeat with `-g` and `-e` and confirm they override without changing the config. Repeat with `-w ` against a config that contains a non-default named workspace and confirm the command targets that workspace's group, environment, and connection — still without mutating the file. + +**Acceptance Scenarios**: + +1. **Given** `defaults.my-kubecontext.group: dev-rg, environment: dev-env` and active context `my-kubecontext`, **When** the user runs `rad deploy app.bicep`, **Then** the deployment targets `dev-rg`/`dev-env` and the config file is unchanged. +2. **Given** the same defaults, **When** the user runs `rad deploy app.bicep -g prod-rg -e prod-env`, **Then** the deployment targets `prod-rg`/`prod-env`, the config file is unchanged, and no prompt or warning about workspaces appears. +3. **Given** a config containing `workspaces.items.azure` with its own `connection.context`, `scope`, and `environment`, **When** the user runs `rad deploy app.bicep -w azure`, **Then** the deployment uses that workspace's connection, group, and environment as a unit; per-command `-g`/`-e` if also supplied still take precedence over the workspace's values. +4. **Given** no group default for the active context, no `--workspace`, no legacy `workspaces.default` resolving the key, **and** no resource group named `default` exists on the cluster, **When** the user runs `rad app list` without `--group`, **Then** the CLI fails with a remediation message that names `--group`, `--workspace`, and `rad configure --defaults`. (If a resource group named `default` does exist, the literal-`default` fallback satisfies the key and the command succeeds.) + +--- + +### User Story 5 - Read-only back-compat with the legacy `workspaces:` block (Priority: P2) + +An existing user upgrades the CLI without re-running `rad init`. Their `~/.rad/config.yaml` already contains a populated `workspaces:` block. Their workflows continue to work unchanged. The legacy block is read but never written by the new CLI; the management commands are gone but the data and the `--workspace` flag remain functional. + +**Why this priority**: Keeps existing users from being broken on upgrade, while still moving the codebase to the new model. + +**Independent Test**: Take an existing pre-refactor `~/.rad/config.yaml` (with `workspaces.default` and at least one item with `scope`, `environment`, `connection.context`). Upgrade the CLI. Run `rad app list` without modifying the file. Verify the command resolves group, environment, and Kubernetes connection from `workspaces.default`. + +**Acceptance Scenarios**: + +1. **Given** a pre-refactor config with `workspaces.default: default`, `workspaces.items.default.connection.context: my-kubecontext`, `workspaces.items.default.scope: /planes/radius/local/resourceGroups/foo`, and `workspaces.items.default.environment: /planes/radius/local/resourceGroups/foo/providers/Applications.Core/environments/bar`, **When** the user runs `rad app list`, **Then** the command targets group `foo`, environment `bar`, and the cluster reachable via kube context `my-kubecontext`, all resolved from `workspaces.default`. +2. **Given** the same config, **When** the user runs `rad configure --list-defaults`, **Then** the listing includes the values resolved from `workspaces.default`, clearly labels them as coming from the legacy block, and points the user at `rad configure --defaults group=...` if they want to migrate. +3. **Given** the same config, **When** the user runs `rad configure --defaults group=new-rg` while the active kube context is `my-kubecontext`, **Then** the CLI writes `defaults.my-kubecontext.group: new-rg`, **does not modify** the `workspaces:` block, and on subsequent commands `defaults.my-kubecontext.group` takes precedence over `workspaces.default.scope`. +4. **Given** no `defaults:` entry for the active kube context, no `workspaces.default`, and no resource group/environment named `default` on the cluster, **When** any scoped command runs without `-g`/`-e`/`-w`, **Then** it fails with the standard remediation message naming all three flags and `rad configure --defaults`. (If `default`/`default` resources exist, the literal fallback satisfies the keys and the command succeeds.) +5. **Given** the user invokes `rad workspace create/list/show/switch/delete`, `rad group switch`, or `rad env switch`, **When** they run, **Then** they fail with a "command removed" error naming the `rad configure --defaults` replacement and pointing at migration docs. The `--workspace ` flag, however, remains functional on scoped commands. + +--- + +### User Story 6 - JSON / scripting friendliness (Priority: P3) + +A user automating Radius operations in CI wants a stable, scriptable interface for reading and setting defaults. + +**Why this priority**: Useful but not blocking; humans can use the table output during the rollout. + +**Independent Test**: Run `rad configure --list-defaults --output json` and pipe to `jq`. Run `rad configure --defaults group=` in CI without TTY and confirm non-interactive success when the target exists. + +**Acceptance Scenarios**: + +1. **Given** a configured CLI, **When** the user runs `rad configure --list-defaults --output json`, **Then** the output is valid JSON keyed by kube context with stable field names (`group`, `environment`, plus a `source` field of `"defaults"` or `"workspaces"`). +2. **Given** a CI environment with no TTY, **When** the user runs `rad configure --defaults group=my-rg`, **Then** the command succeeds without prompts when the group exists, and fails non-zero with a stable error code when it does not. + +--- + +### Edge Cases + +- **Cluster unreachable during `rad configure --defaults`**: Fail without mutating the config; clearly distinguish "cluster unreachable" from "group not found". +- **Group default set, environment unset, no `default` environment on cluster**: Commands needing only a group succeed; commands needing an environment fall through to the literal `default` fallback and fail with a precise "no default environment" message only if a `default` environment does not exist. +- **Both unset, fresh install before `rad init`**: `rad configure --list-defaults` prints an empty result and a hint to run `rad init`. +- **Stale environment value**: Default environment was deleted out of band. Next scoped command fails with a remediation (run `rad env list`, then `rad configure --defaults environment=`). +- **Concurrent edits to `~/.rad/config.yaml`**: Two `rad configure --defaults …` invocations run in parallel; the file must not become corrupted (last-writer-wins with file locking is acceptable). +- **`rad init` re-run on an already-configured machine**: Existing defaults for the active context must not be silently overwritten without user confirmation. +- **Active kube context contains characters unusual in YAML keys** (dots, slashes, colons): These MUST be preserved verbatim as the YAML map key (quoted as needed). +- **Two contexts point at the same cluster**: Treated as independent entries in `defaults:`; the user can configure them identically or differently. No deduplication by Radius. +- **`KUBECONFIG` references multiple files / a non-default path**: The "active kube context" is resolved via the standard kubeconfig precedence rules used elsewhere in the CLI; no new resolution logic is introduced. +- **Legacy block contains a workspace whose `connection.context` matches the active kube context AND `defaults.` exists**: `defaults:` always wins over `workspaces.default`, key by key. Missing keys fall through to `workspaces.default` per the resolution order — the two sources are merged per-key. +- **`-w ` names a workspace that does not exist in `workspaces.items`**: Command fails with a clear error before contacting the cluster. +- **`-w ` plus `-g`/`-e` flags**: `-g`/`-e` win for those keys; the workspace supplies the remaining keys (notably `connection.context`). +- **`-w` is set but the workspace has no `scope` or `environment`** (e.g., the `azure` entry in the user's example config): the workspace supplies only `connection.context`; group/environment must come from `-g`/`-e`/`defaults:`/`workspaces.default` per the standard resolution order, otherwise a clear error. + +## Requirements *(mandatory)* + +### Functional Requirements + +#### Command surface + +- **FR-001**: The CLI MUST expose a `rad configure` command. It MUST support `--defaults = [=…]` (set), `--defaults =` with an empty value (clear that key), and `--list-defaults` (inspect). `--list-defaults` MUST support `--output json`. +- **FR-002**: Supported keys for `--defaults` MUST include `group` and `environment`. Unknown keys MUST fail the command with a message listing supported keys. +- **FR-003**: All `--defaults` operations MUST always target the entry for the **active Kubernetes context** in the new `defaults:` block. There is no flag to target a different context. +- **FR-004**: `rad configure --defaults group=` MUST validate that the named resource group exists on the cluster reachable via the active kube context before persisting. `rad configure --defaults environment=` MUST validate the environment exists within the configured (or already-set) default group. +- **FR-005**: When multiple keys are provided in one invocation (e.g., `group=… environment=…`), the operation MUST be atomic: validation of all keys MUST succeed before any write; on any failure, the file MUST be unchanged. +- **FR-006**: `rad configure --list-defaults` MUST display all entries from the `defaults:` block, plus any values resolved from the legacy `workspaces:` block (clearly labeled as such), with the active kube context highlighted. + +#### Storage and schema + +- **FR-007**: New commands MUST write defaults to a top-level `defaults:` block keyed by Kubernetes context name. Within each context entry, supported subkeys are `group` and `environment` (string values, names — not URIs). +- **FR-008**: New commands MUST NOT write to the legacy `workspaces:` block under any circumstance. +- **FR-009**: Setting a key to an empty value (clear) MUST remove that key from `defaults.`. If the resulting context entry has no remaining keys, the entry itself MUST be removed. If the resulting `defaults:` block is empty, the block MUST be removed. +- **FR-010**: `rad configure --defaults …` operations MUST be safe under concurrent execution (no file corruption); last-writer-wins is acceptable. The file MUST be written atomically (write-temp-then-rename or equivalent). +- **FR-011**: On any validation failure, the config file MUST remain byte-for-byte unchanged. + +#### Resolution and command behavior + +- **FR-012**: All scoped commands (including but not limited to `rad deploy`, `rad app list/show/delete/connections/status/graph`, `rad env list/show/create/delete`, `rad group list/show/create/delete`, `rad resource list/show/delete`, `rad recipe list/show/register/unregister`, `rad credential …`) MUST resolve group, environment, and Kubernetes connection using this exact order, applied **per key independently**: + 1. Per-command `-g/--group` / `-e/--environment` flag. + 2. Per-command `-w/--workspace ` flag: if present, the named entry in `workspaces.items.` supplies any keys (group from `scope`, environment from `environment`, connection from `connection.context`) not yet resolved by step 1. + 3. `defaults..` from the new `defaults:` block. + 4. `workspaces.default`: the workspace named by `workspaces.default` supplies any keys still unresolved (group from `scope`, environment from `environment`, connection from `connection.context`). + 5. **Built-in literal `default`**: for the `group` and `environment` keys only, the literal string `default` MUST be used if no earlier source supplied a value. This pairs with `rad init`'s creation of a resource group named `default` and an environment named `default` to deliver a working zero-config experience. + 6. Error — only reachable when the resolved Kubernetes connection cannot be determined, **or** when the user has explicitly cleared a default (e.g., via `rad configure --defaults group=`) and the literal `default` does not exist on the cluster. The remediation message MUST name `--group`, `--environment`, `--workspace`, and `rad configure --defaults`. +- **FR-013**: Per-key resolution MUST stop at the first source supplying a value for that key. Sources MAY supply different keys: e.g., `-g` from the command line, `environment` from `defaults:`, and `connection.context` from `workspaces.default` is a valid combined resolution. +- **FR-014**: When the Kubernetes connection cannot be determined from any source (no `-w`, no active kube context, no usable `workspaces.default`), any scoped command MUST fail fast with a remediation message and MUST NOT pick a default arbitrarily. +- **FR-015**: When the resolved Kubernetes connection refers to a cluster that is unreachable, command behavior MUST mirror today's behavior for unreachable clusters (this refactor does not change connection-failure semantics). + +#### `rad init` + +- **FR-016**: `rad init` MUST NOT create, name, or reference a "workspace" in any user-facing output. +- **FR-017**: `rad init` MUST create a resource group named `default` and an environment named `default` on the connected cluster (matching the literal-`default` fallback in FR-012 step 5). It MUST NOT write to `~/.rad/config.yaml` for the purpose of recording these defaults; the literal-`default` resolution rule provides the same effect with no file mutation. Users who want non-`default` names use `rad configure --defaults group= environment=` after `rad init`. +- **FR-017a**: `rad init` MAY still write `~/.rad/config.yaml` for non-default purposes (e.g., recording cloud-provider credentials or other configuration outside the `defaults:` and `workspaces:` blocks). Any such writes MUST NOT touch the `defaults:` or `workspaces:` blocks. + +#### Removal of legacy commands + +- **FR-018**: The following commands MUST be removed: `rad workspace create`, `rad workspace list`, `rad workspace show`, `rad workspace switch`, `rad workspace delete`, `rad group switch`, `rad env switch`. Invoking any of them MUST fail with a "command removed" error message that names the `rad configure --defaults` replacement and links to migration docs. +- **FR-019**: The `-w/--workspace ` **flag** on scoped commands MUST be **preserved**. It selects a named entry from `workspaces.items` for the duration of one command invocation per FR-012 step 2. Help text for the flag MUST describe it as a per-command override that reads the legacy `workspaces:` block. +- **FR-020**: The Go type `workspaces.Workspace` and related infrastructure MAY remain inside the codebase. It MUST NOT be exposed in any new public CLI surface other than the `--workspace` flag described in FR-019, and new help text/documentation MUST NOT introduce the term "workspace" outside the `--workspace` flag's own help and the migration guide. + +#### Documentation and discoverability + +- **FR-021**: All new help text, getting-started docs, and error messages MUST avoid introducing the term "workspace" to new users. Documentation MUST include a migration guide from `rad workspace`/`rad group switch`/`rad env switch` to `rad configure --defaults`, and from a populated `workspaces:` block to a `defaults:` block (showing equivalent commands). + +### Key Entities + +- **Defaults Entry**: A map keyed by Kubernetes context name. Each value contains the default `group` (resource group name) and `environment` (environment name) for `rad` commands run while that context is active. Stored under the new top-level `defaults:` key in `~/.rad/config.yaml`. Replaces the user-visible "workspace" concept. +- **Legacy Workspace Entry**: The pre-existing structure under `workspaces.items.` containing `connection`, `scope`, and `environment`. Read-only after this refactor. Used (a) per-key as a fallback when `defaults.` does not provide a value (the workspace named by `workspaces.default`), and (b) as a one-shot override when the user passes `-w ` on a command. +- **Active Kubernetes Context**: The current `current-context` resolved from the standard kubeconfig precedence chain. The lookup key for `defaults:`. The single source of truth for which cluster the CLI talks to. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: After `rad init` on a clean machine, a user can run `rad app list` and `rad deploy app.bicep` to completion **without ever seeing or typing the word "workspace"**. +- **SC-002**: Help text and getting-started docs contain **zero occurrences** of "workspace" outside the migration guide. +- **SC-003**: A user with an unmodified pre-refactor `~/.rad/config.yaml` can upgrade the CLI and run their previous scoped workflows with **no manual file edits** and **no command failures** caused by schema changes (assuming the active kube context matches the legacy default workspace's `connection.context`). +- **SC-004**: A user with `defaults:` entries for two kube contexts can switch between contexts via `kubectl config use-context` and run scoped `rad` commands against each cluster **with zero additional `rad` configuration steps** between switches. +- **SC-005**: `rad configure --defaults …` either succeeds and persists the value(s), or fails and leaves the config file byte-for-byte unchanged, in **100%** of test runs (including network failures, validation failures, and unknown keys). +- **SC-006**: Every operation previously achievable via `rad workspace switch`, `rad group switch`, or `rad env switch` is achievable via a single `rad configure --defaults …` invocation; **no operation requires more steps after the refactor than before**. +- **SC-007**: Time-to-first-deploy for a new user (time from `rad init` start to a successful `rad deploy app.bicep`) does not increase versus the pre-refactor baseline. +- **SC-008**: Inside the codebase, references to the legacy workspace types are confined to one well-defined compatibility shim package; no scoped command's resolution path reads workspace fields directly. + +## Assumptions + +- **Schema is additive, not replacing**: A new top-level `defaults:` key is added. The existing `workspaces:` key is read for back-compat but never written. This honors the "leave schema as-is if possible" priority while delivering a clean new UX. +- **Kube-context-keyed defaults remove the mismatch problem**: Because the lookup key is the active kube context, switching contexts in `kubectl` automatically selects the matching defaults. There is no longer a "stored context vs. active context" mismatch to detect, prompt about, or auto-update. The earlier proposed FR for context-mismatch UX is therefore moot and dropped from this revision. +- **Hard removal of legacy management commands; flag preserved**: `rad workspace*`, `rad group switch`, and `rad env switch` are removed. The `-w/--workspace ` flag on scoped commands is **preserved** as a one-shot selector that reads the legacy `workspaces.items` block. This keeps existing scripts working and gives users a per-command escape hatch without bringing back the workspace-management surface. +- **One defaults entry per active kube context**: Multiple named "profiles" per context are out of scope. Users wanting multiple environments on the same cluster can use `--group`/`--environment` flags or maintain separate kube contexts. +- **Kubernetes-only connections in scope**: This refactor targets the Kubernetes connection kind. Non-Kubernetes connection kinds (none currently end-user-facing) are out of scope. +- **No new auth model**: Authentication against Kubernetes and Radius control planes is unchanged. +- **Tests as parity oracle**: Existing functional tests for scoped commands provide the parity oracle: every test that passes pre-refactor must pass post-refactor, with `--workspace` removed and `--group`/`--environment` flags or `defaults:` entries used in its place. From 663bf2699aed972d25c206b6ab362bb3232fb5c7 Mon Sep 17 00:00:00 2001 From: Zach Casper Date: Wed, 6 May 2026 11:06:08 -0500 Subject: [PATCH 4/5] Renamed to correct convention Signed-off-by: Zach Casper --- .../cli/2026-04-28-workspace-refactor.md | 264 ------------------ .../cli/2026-04-workspace-refactor.md | 2 +- 2 files changed, 1 insertion(+), 265 deletions(-) delete mode 100644 eng/design-notes/cli/2026-04-28-workspace-refactor.md diff --git a/eng/design-notes/cli/2026-04-28-workspace-refactor.md b/eng/design-notes/cli/2026-04-28-workspace-refactor.md deleted file mode 100644 index 9f66425a70..0000000000 --- a/eng/design-notes/cli/2026-04-28-workspace-refactor.md +++ /dev/null @@ -1,264 +0,0 @@ -# Feature Specification: Replace Radius Workspaces with `rad configure --defaults` - -## Background - -### The workspace concept confuses users - -Radius today exposes a CLI-only concept called a **workspace**, configured through commands like `rad workspace create`, `rad workspace switch`, `rad workspace list`, and `rad workspace delete`. Despite the API-style verbs, a workspace is **not** a Radius API object — it is purely a client-side bundle stored in `~/.rad/config.yaml` that combines: - -- a Kubernetes connection (a kube context name), -- a default resource group (stored as a fully-qualified URI like `/planes/radius/local/resourceGroups/default`), and -- a default environment (also stored as a URI). - -This naming and command shape causes recurring user confusion: - -- **"Is a workspace an API resource?"** `rad workspace create` reads like `rad app create` or `rad env create`, which *do* create server-side objects. Users assume workspaces are similar and look for them in `rad resource list`, in the dashboard, or in their cluster's CRDs — where they do not exist. -- **"What is the difference between a workspace, a resource group, and an environment?"** New users see three concepts in the docs (`workspace`, `group`, `environment`) but only two of them (group, environment) actually correspond to anything on the cluster. The third is a CLI-local container whose only job is to remember which group and environment to use by default. -- **"Why do I need to switch workspaces to switch resource groups?"** Day-to-day, what users actually want is to change which resource group or environment their next `rad` commands target. Today that is spread across three commands (`rad workspace switch`, `rad group switch`, `rad env switch`) with subtly different semantics, plus per-command `--workspace`, `--group`, and `--environment` flags whose precedence is not obvious. -- **"Why does my workspace stop working when I switch clusters?"** A workspace pins a specific kube context. If the user switches clusters with `kubectl config use-context`, the workspace either silently uses the old (now-wrong) context or fails opaquely, depending on how the workspace was set up. - -The net effect is a surface area whose primary job is to remember two strings (a default group name and a default environment name) per cluster, but which appears to users as a fourth top-level concept on equal footing with apps, environments, and resource groups. - -### `az configure --defaults` as the model - -The `az` CLI solves the same problem with a much smaller surface: `az configure --defaults group=my-rg location=westus2`. There is no client-side "workspace" object — `az` simply remembers a few key/value defaults and applies them to subsequent commands. Users discover the feature once, learn one command, and never have to reason about a CLI-only entity that mirrors API objects. - -This refactor adopts the same model for Radius. Users set defaults with `rad configure --defaults group= environment=`, list them with `rad configure --list-defaults`, and clear individual keys by setting them to an empty value. The defaults are scoped to the active Kubernetes context (so switching clusters with `kubectl config use-context` automatically selects the right defaults), and Radius commands fall back through a clear, per-key precedence chain when a default is not set. New users never encounter the word "workspace"; existing users keep working without manual migration because the legacy `workspaces:` block is still read. - -The "workspace" concept is thereby downgraded from a top-level CLI noun to an internal storage detail and a one-shot `-w` flag for users who already rely on named workspace bundles. - -## Storage strategy - -The existing `workspaces:` block in `~/.rad/config.yaml` is **never written** by new commands and is **read only as a fallback** when no `defaults:` entry matches the active Kubernetes context. New commands write a single new top-level block: - -```yaml -defaults: - my-kubecontext: - group: my-group - environment: my-env - my-other-kubecontext: - group: prod-group - environment: prod-env -workspaces: # read-only fallback (unchanged on disk by new commands) - default: default - items: - default: - connection: { context: my-kubecontext, kind: kubernetes } - scope: /planes/radius/local/resourceGroups/default - environment: /planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/default -``` - -**Resolution order** for the group/environment used by a `rad` command (highest precedence first): - -1. **Per-command `-g/--group` / `-e/--environment` flag** on the individual command. -2. **Per-command `-w/--workspace ` flag**: if present, the named workspace from `workspaces.items.` supplies group (from `scope`), environment (from `environment`), and Kubernetes connection (from `connection.context`) for keys not satisfied by step 1. -3. **`defaults..`** in the new `defaults:` block. -4. **`workspaces.default`** — the entry named by `workspaces.default` supplies group/environment/connection for any key still unresolved. -5. **Built-in fallback**: the literal name `default` is used for `group` and/or `environment` if still unresolved. Because `rad init` creates a resource group named `default` and an environment named `default`, this gives a working zero-config experience without `rad init` needing to write to `~/.rad/config.yaml` at all. -6. Error only if even the built-in `default` fallback is unusable (e.g., the user has explicitly cleared a default with `rad configure --defaults group=` and the literal `default` group does not exist on the cluster). The remediation message names `--group`/`--environment`, `--workspace`, and `rad configure --defaults`. - -Resolution is **per key independently**: e.g., `--group` set on the command line, `environment` resolved from `defaults:`, and Kubernetes connection from `workspaces.default` is a valid combination. Steps stop at the first source that supplies each key. - -The Kubernetes connection has no `default` literal fallback — it must come from `-w`, `workspaces.default`, or the active kube context (the active context is implicit and used by all sources in steps 3–5 unless `-w` or `workspaces.default` overrides it). - -## User Scenarios & Testing *(mandatory)* - -### User Story 1 - First-time user runs `rad init` with no config file written for defaults (Priority: P1) - -A new user installs the CLI, points `kubectl` at their cluster, and runs `rad init`. The CLI installs the Radius control plane and creates a resource group named `default` and an environment named `default` on the cluster. **It does not need to write a `defaults:` block to `~/.rad/config.yaml`** — subsequent `rad` commands resolve to the literal name `default` via the built-in fallback (FR-012 step 5). No "workspace" is created, named, or mentioned anywhere in user-visible output. - -**Why this priority**: This is the entry point for every new user. The combination of "no workspace concept" and "no required config file write" is the defining shape of the new UX. - -**Independent Test**: Run `rad init` on a fresh machine, then run `rad env show` with no flags. Confirm that the environment named `default` from the resource group named `default` is shown — verifying that `rad init` created both, that the literal-`default` fallback resolved both keys without any `defaults:` block being written, and that no user-facing output mentioned "workspace". - -**Acceptance Scenarios**: - -1. **Given** a fresh machine with no Radius config and active kube context `my-kubecontext`, **When** the user runs `rad init` and accepts the defaults, **Then** a resource group named `default` and an environment named `default` are created on the cluster, no `defaults:` block is written to `~/.rad/config.yaml` solely for this purpose, and the success message contains no occurrence of "workspace". -2. **Given** a successful `rad init`, **When** the user runs `rad app list` immediately afterward without flags, **Then** the command succeeds by resolving to group `default` and environment `default` via the built-in literal fallback. -3. **Given** a successful `rad init`, **When** the user runs `rad workspace list` or `rad env switch` or `rad group switch`, **Then** the CLI fails with a "command removed" error that names the `rad configure --defaults` replacement. -4. **Given** a successful `rad init` followed by `rad configure --defaults group=my-rg`, **When** the user runs `rad app list`, **Then** the command targets `my-rg` (the configured default takes precedence over the literal `default` fallback). - ---- - -### User Story 2 - Set, list, and clear defaults with `rad configure` (Priority: P1) - -A user manages defaults via `rad configure --defaults =` (set), `rad configure --defaults =` (clear, empty value), and `rad configure --list-defaults` (inspect). - -**Why this priority**: This is the primary replacement surface for `rad workspace switch`, `rad group switch`, and `rad env switch`. Users must be able to perform every workspace-default operation through `rad configure`. - -**Independent Test**: From any valid Radius config, run `rad configure --defaults group=dev-rg`, `rad configure --defaults environment=dev-env`, then `rad configure --list-defaults`. Verify the values appear under the active kube context and that a scoped command uses them. Then run `rad configure --defaults group=` and verify the next scoped command falls through to the built-in literal `default` fallback (or fails with a clear remediation message if no resource group named `default` exists on the cluster). - -**Acceptance Scenarios**: - -1. **Given** an active kube context `my-kubecontext` and no existing defaults entry for it, **When** the user runs `rad configure --defaults group=my-rg`, **Then** the CLI validates that `my-rg` exists on the cluster connected via `my-kubecontext`, persists `defaults.my-kubecontext.group: my-rg` to `~/.rad/config.yaml`, and prints a confirmation including the kube context, key, and new value. -2. **Given** existing defaults for `my-kubecontext`, **When** the user runs `rad configure --defaults environment=my-env`, **Then** the CLI validates the environment within the configured default group and persists `defaults.my-kubecontext.environment: my-env`. -3. **Given** a config with defaults for one or more contexts, **When** the user runs `rad configure --list-defaults`, **Then** the CLI prints all `defaults:` entries grouped by kube context (with the active context indicated), and supports `--output json` returning a stable schema. -4. **Given** a default group is set for the active context, **When** the user runs `rad configure --defaults group=` (empty value), **Then** that single key is removed from `defaults.`. Subsequent commands fall through the resolution order; if no `workspaces.default` supplies the key, they fall through to the literal `default` fallback. If the literal `default` group does not exist on the cluster, the command fails with the standard remediation message. -5. **Given** removing the last key for a context leaves an empty map, **When** the operation completes, **Then** the empty `defaults.` entry is removed from the file (no empty `{}` left behind). -6. **Given** the user passes `rad configure --defaults group=` for a group that does not exist on the connected cluster, **When** the command runs, **Then** the CLI fails with a clear error and does not modify the config file. -7. **Given** the user passes multiple key/value pairs, e.g. `rad configure --defaults group=my-rg environment=my-env`, **When** the command runs, **Then** the CLI validates and persists all of them atomically (all-or-nothing) under the active kube context. -8. **Given** the user passes an unknown key, e.g. `rad configure --defaults foo=bar`, **When** the command runs, **Then** the CLI fails with a clear error listing the supported keys (`group`, `environment`) and does not modify the file. - ---- - -### User Story 3 - Per-context defaults make kube context switches automatic (Priority: P1) - -A user configures defaults for two kube contexts (`dev-cluster` and `prod-cluster`). They switch between them with `kubectl config use-context` and run `rad` commands. Each `rad` invocation automatically picks up the right defaults for the active context — no warnings, no prompts, no `rad config set context` step. - -**Why this priority**: This is the design's signature feature. The kube-context-keyed `defaults:` block replaces the static workspace-to-context binding and eliminates the mismatch class of problems entirely. - -**Independent Test**: Create defaults for two contexts, switch contexts via `kubectl`, run scoped commands without flags, and verify each one targets the matching cluster's defaults. - -**Acceptance Scenarios**: - -1. **Given** `defaults.dev-cluster.group: dev-rg` and `defaults.prod-cluster.group: prod-rg`, and active context `dev-cluster`, **When** the user runs `rad app list`, **Then** the command targets `dev-cluster` with group `dev-rg`. -2. **Given** the same config, **When** the user runs `kubectl config use-context prod-cluster && rad app list`, **Then** the command targets `prod-cluster` with group `prod-rg` automatically — no warning or prompt. -3. **Given** active kube context `unknown-cluster` and no `defaults.unknown-cluster` entry, no `workspaces.default` resolving the missing keys, and resource groups/environments named `default` exist on `unknown-cluster`, **When** the user runs `rad app list`, **Then** the command succeeds via the literal `default` fallback. If those literal-`default` resources do not exist, the command fails with a remediation message naming `--group`, `--workspace`, and `rad configure --defaults`. -4. **Given** the user has no active kube context (e.g., `KUBECONFIG` empty or current-context unset) and no `--workspace` flag is passed, **When** the user runs any scoped `rad` command, **Then** the CLI fails fast with a remediation message pointing at `kubectl config use-context` and `rad init`. (If `--workspace ` is passed, that workspace's `connection.context` is used and the command proceeds.) - ---- - -### User Story 4 - Per-command flags override; `--workspace` remains a one-shot selector (Priority: P1) - -Every scoped command supports `-g/--group`, `-e/--environment`, and `-w/--workspace` flags. Per-command `-g`/`-e` flags always win. `-w ` selects a named workspace from the legacy `workspaces.items` block as a single unit (its `connection.context`, `scope`, and `environment` are used for any keys not already supplied by `-g`/`-e`). None of these flags mutate the config file. - -**Why this priority**: Without consistent override semantics, the new model is worse than workspaces. `-w` is preserved so existing scripts and users who rely on named workspaces keep working without re-tooling. - -**Independent Test**: With defaults set, run scoped commands without flags and confirm they target the defaults. Repeat with `-g` and `-e` and confirm they override without changing the config. Repeat with `-w ` against a config that contains a non-default named workspace and confirm the command targets that workspace's group, environment, and connection — still without mutating the file. - -**Acceptance Scenarios**: - -1. **Given** `defaults.my-kubecontext.group: dev-rg, environment: dev-env` and active context `my-kubecontext`, **When** the user runs `rad deploy app.bicep`, **Then** the deployment targets `dev-rg`/`dev-env` and the config file is unchanged. -2. **Given** the same defaults, **When** the user runs `rad deploy app.bicep -g prod-rg -e prod-env`, **Then** the deployment targets `prod-rg`/`prod-env`, the config file is unchanged, and no prompt or warning about workspaces appears. -3. **Given** a config containing `workspaces.items.azure` with its own `connection.context`, `scope`, and `environment`, **When** the user runs `rad deploy app.bicep -w azure`, **Then** the deployment uses that workspace's connection, group, and environment as a unit; per-command `-g`/`-e` if also supplied still take precedence over the workspace's values. -4. **Given** no group default for the active context, no `--workspace`, no legacy `workspaces.default` resolving the key, **and** no resource group named `default` exists on the cluster, **When** the user runs `rad app list` without `--group`, **Then** the CLI fails with a remediation message that names `--group`, `--workspace`, and `rad configure --defaults`. (If a resource group named `default` does exist, the literal-`default` fallback satisfies the key and the command succeeds.) - ---- - -### User Story 5 - Read-only back-compat with the legacy `workspaces:` block (Priority: P2) - -An existing user upgrades the CLI without re-running `rad init`. Their `~/.rad/config.yaml` already contains a populated `workspaces:` block. Their workflows continue to work unchanged. The legacy block is read but never written by the new CLI; the management commands are gone but the data and the `--workspace` flag remain functional. - -**Why this priority**: Keeps existing users from being broken on upgrade, while still moving the codebase to the new model. - -**Independent Test**: Take an existing pre-refactor `~/.rad/config.yaml` (with `workspaces.default` and at least one item with `scope`, `environment`, `connection.context`). Upgrade the CLI. Run `rad app list` without modifying the file. Verify the command resolves group, environment, and Kubernetes connection from `workspaces.default`. - -**Acceptance Scenarios**: - -1. **Given** a pre-refactor config with `workspaces.default: default`, `workspaces.items.default.connection.context: my-kubecontext`, `workspaces.items.default.scope: /planes/radius/local/resourceGroups/foo`, and `workspaces.items.default.environment: /planes/radius/local/resourceGroups/foo/providers/Applications.Core/environments/bar`, **When** the user runs `rad app list`, **Then** the command targets group `foo`, environment `bar`, and the cluster reachable via kube context `my-kubecontext`, all resolved from `workspaces.default`. -2. **Given** the same config, **When** the user runs `rad configure --list-defaults`, **Then** the listing includes the values resolved from `workspaces.default`, clearly labels them as coming from the legacy block, and points the user at `rad configure --defaults group=...` if they want to migrate. -3. **Given** the same config, **When** the user runs `rad configure --defaults group=new-rg` while the active kube context is `my-kubecontext`, **Then** the CLI writes `defaults.my-kubecontext.group: new-rg`, **does not modify** the `workspaces:` block, and on subsequent commands `defaults.my-kubecontext.group` takes precedence over `workspaces.default.scope`. -4. **Given** no `defaults:` entry for the active kube context, no `workspaces.default`, and no resource group/environment named `default` on the cluster, **When** any scoped command runs without `-g`/`-e`/`-w`, **Then** it fails with the standard remediation message naming all three flags and `rad configure --defaults`. (If `default`/`default` resources exist, the literal fallback satisfies the keys and the command succeeds.) -5. **Given** the user invokes `rad workspace create/list/show/switch/delete`, `rad group switch`, or `rad env switch`, **When** they run, **Then** they fail with a "command removed" error naming the `rad configure --defaults` replacement and pointing at migration docs. The `--workspace ` flag, however, remains functional on scoped commands. - ---- - -### User Story 6 - JSON / scripting friendliness (Priority: P3) - -A user automating Radius operations in CI wants a stable, scriptable interface for reading and setting defaults. - -**Why this priority**: Useful but not blocking; humans can use the table output during the rollout. - -**Independent Test**: Run `rad configure --list-defaults --output json` and pipe to `jq`. Run `rad configure --defaults group=` in CI without TTY and confirm non-interactive success when the target exists. - -**Acceptance Scenarios**: - -1. **Given** a configured CLI, **When** the user runs `rad configure --list-defaults --output json`, **Then** the output is valid JSON keyed by kube context with stable field names (`group`, `environment`, plus a `source` field of `"defaults"` or `"workspaces"`). -2. **Given** a CI environment with no TTY, **When** the user runs `rad configure --defaults group=my-rg`, **Then** the command succeeds without prompts when the group exists, and fails non-zero with a stable error code when it does not. - ---- - -### Edge Cases - -- **Cluster unreachable during `rad configure --defaults`**: Fail without mutating the config; clearly distinguish "cluster unreachable" from "group not found". -- **Group default set, environment unset, no `default` environment on cluster**: Commands needing only a group succeed; commands needing an environment fall through to the literal `default` fallback and fail with a precise "no default environment" message only if a `default` environment does not exist. -- **Both unset, fresh install before `rad init`**: `rad configure --list-defaults` prints an empty result and a hint to run `rad init`. -- **Stale environment value**: Default environment was deleted out of band. Next scoped command fails with a remediation (run `rad env list`, then `rad configure --defaults environment=`). -- **Concurrent edits to `~/.rad/config.yaml`**: Two `rad configure --defaults …` invocations run in parallel; the file must not become corrupted (last-writer-wins with file locking is acceptable). -- **`rad init` re-run on an already-configured machine**: Existing defaults for the active context must not be silently overwritten without user confirmation. -- **Active kube context contains characters unusual in YAML keys** (dots, slashes, colons): These MUST be preserved verbatim as the YAML map key (quoted as needed). -- **Two contexts point at the same cluster**: Treated as independent entries in `defaults:`; the user can configure them identically or differently. No deduplication by Radius. -- **`KUBECONFIG` references multiple files / a non-default path**: The "active kube context" is resolved via the standard kubeconfig precedence rules used elsewhere in the CLI; no new resolution logic is introduced. -- **Legacy block contains a workspace whose `connection.context` matches the active kube context AND `defaults.` exists**: `defaults:` always wins over `workspaces.default`, key by key. Missing keys fall through to `workspaces.default` per the resolution order — the two sources are merged per-key. -- **`-w ` names a workspace that does not exist in `workspaces.items`**: Command fails with a clear error before contacting the cluster. -- **`-w ` plus `-g`/`-e` flags**: `-g`/`-e` win for those keys; the workspace supplies the remaining keys (notably `connection.context`). -- **`-w` is set but the workspace has no `scope` or `environment`** (e.g., the `azure` entry in the user's example config): the workspace supplies only `connection.context`; group/environment must come from `-g`/`-e`/`defaults:`/`workspaces.default` per the standard resolution order, otherwise a clear error. - -## Requirements *(mandatory)* - -### Functional Requirements - -#### Command surface - -- **FR-001**: The CLI MUST expose a `rad configure` command. It MUST support `--defaults = [=…]` (set), `--defaults =` with an empty value (clear that key), and `--list-defaults` (inspect). `--list-defaults` MUST support `--output json`. -- **FR-002**: Supported keys for `--defaults` MUST include `group` and `environment`. Unknown keys MUST fail the command with a message listing supported keys. -- **FR-003**: All `--defaults` operations MUST always target the entry for the **active Kubernetes context** in the new `defaults:` block. There is no flag to target a different context. -- **FR-004**: `rad configure --defaults group=` MUST validate that the named resource group exists on the cluster reachable via the active kube context before persisting. `rad configure --defaults environment=` MUST validate the environment exists within the configured (or already-set) default group. -- **FR-005**: When multiple keys are provided in one invocation (e.g., `group=… environment=…`), the operation MUST be atomic: validation of all keys MUST succeed before any write; on any failure, the file MUST be unchanged. -- **FR-006**: `rad configure --list-defaults` MUST display all entries from the `defaults:` block, plus any values resolved from the legacy `workspaces:` block (clearly labeled as such), with the active kube context highlighted. - -#### Storage and schema - -- **FR-007**: New commands MUST write defaults to a top-level `defaults:` block keyed by Kubernetes context name. Within each context entry, supported subkeys are `group` and `environment` (string values, names — not URIs). -- **FR-008**: New commands MUST NOT write to the legacy `workspaces:` block under any circumstance. -- **FR-009**: Setting a key to an empty value (clear) MUST remove that key from `defaults.`. If the resulting context entry has no remaining keys, the entry itself MUST be removed. If the resulting `defaults:` block is empty, the block MUST be removed. -- **FR-010**: `rad configure --defaults …` operations MUST be safe under concurrent execution (no file corruption); last-writer-wins is acceptable. The file MUST be written atomically (write-temp-then-rename or equivalent). -- **FR-011**: On any validation failure, the config file MUST remain byte-for-byte unchanged. - -#### Resolution and command behavior - -- **FR-012**: All scoped commands (including but not limited to `rad deploy`, `rad app list/show/delete/connections/status/graph`, `rad env list/show/create/delete`, `rad group list/show/create/delete`, `rad resource list/show/delete`, `rad recipe list/show/register/unregister`, `rad credential …`) MUST resolve group, environment, and Kubernetes connection using this exact order, applied **per key independently**: - 1. Per-command `-g/--group` / `-e/--environment` flag. - 2. Per-command `-w/--workspace ` flag: if present, the named entry in `workspaces.items.` supplies any keys (group from `scope`, environment from `environment`, connection from `connection.context`) not yet resolved by step 1. - 3. `defaults..` from the new `defaults:` block. - 4. `workspaces.default`: the workspace named by `workspaces.default` supplies any keys still unresolved (group from `scope`, environment from `environment`, connection from `connection.context`). - 5. **Built-in literal `default`**: for the `group` and `environment` keys only, the literal string `default` MUST be used if no earlier source supplied a value. This pairs with `rad init`'s creation of a resource group named `default` and an environment named `default` to deliver a working zero-config experience. - 6. Error — only reachable when the resolved Kubernetes connection cannot be determined, **or** when the user has explicitly cleared a default (e.g., via `rad configure --defaults group=`) and the literal `default` does not exist on the cluster. The remediation message MUST name `--group`, `--environment`, `--workspace`, and `rad configure --defaults`. -- **FR-013**: Per-key resolution MUST stop at the first source supplying a value for that key. Sources MAY supply different keys: e.g., `-g` from the command line, `environment` from `defaults:`, and `connection.context` from `workspaces.default` is a valid combined resolution. -- **FR-014**: When the Kubernetes connection cannot be determined from any source (no `-w`, no active kube context, no usable `workspaces.default`), any scoped command MUST fail fast with a remediation message and MUST NOT pick a default arbitrarily. -- **FR-015**: When the resolved Kubernetes connection refers to a cluster that is unreachable, command behavior MUST mirror today's behavior for unreachable clusters (this refactor does not change connection-failure semantics). - -#### `rad init` - -- **FR-016**: `rad init` MUST NOT create, name, or reference a "workspace" in any user-facing output. -- **FR-017**: `rad init` MUST create a resource group named `default` and an environment named `default` on the connected cluster (matching the literal-`default` fallback in FR-012 step 5). It MUST NOT write to `~/.rad/config.yaml` for the purpose of recording these defaults; the literal-`default` resolution rule provides the same effect with no file mutation. Users who want non-`default` names use `rad configure --defaults group= environment=` after `rad init`. -- **FR-017a**: `rad init` MAY still write `~/.rad/config.yaml` for non-default purposes (e.g., recording cloud-provider credentials or other configuration outside the `defaults:` and `workspaces:` blocks). Any such writes MUST NOT touch the `defaults:` or `workspaces:` blocks. - -#### Removal of legacy commands - -- **FR-018**: The following commands MUST be removed: `rad workspace create`, `rad workspace list`, `rad workspace show`, `rad workspace switch`, `rad workspace delete`, `rad group switch`, `rad env switch`. Invoking any of them MUST fail with a "command removed" error message that names the `rad configure --defaults` replacement and links to migration docs. -- **FR-019**: The `-w/--workspace ` **flag** on scoped commands MUST be **preserved**. It selects a named entry from `workspaces.items` for the duration of one command invocation per FR-012 step 2. Help text for the flag MUST describe it as a per-command override that reads the legacy `workspaces:` block. -- **FR-020**: The Go type `workspaces.Workspace` and related infrastructure MAY remain inside the codebase. It MUST NOT be exposed in any new public CLI surface other than the `--workspace` flag described in FR-019, and new help text/documentation MUST NOT introduce the term "workspace" outside the `--workspace` flag's own help and the migration guide. - -#### Documentation and discoverability - -- **FR-021**: All new help text, getting-started docs, and error messages MUST avoid introducing the term "workspace" to new users. Documentation MUST include a migration guide from `rad workspace`/`rad group switch`/`rad env switch` to `rad configure --defaults`, and from a populated `workspaces:` block to a `defaults:` block (showing equivalent commands). - -### Key Entities - -- **Defaults Entry**: A map keyed by Kubernetes context name. Each value contains the default `group` (resource group name) and `environment` (environment name) for `rad` commands run while that context is active. Stored under the new top-level `defaults:` key in `~/.rad/config.yaml`. Replaces the user-visible "workspace" concept. -- **Legacy Workspace Entry**: The pre-existing structure under `workspaces.items.` containing `connection`, `scope`, and `environment`. Read-only after this refactor. Used (a) per-key as a fallback when `defaults.` does not provide a value (the workspace named by `workspaces.default`), and (b) as a one-shot override when the user passes `-w ` on a command. -- **Active Kubernetes Context**: The current `current-context` resolved from the standard kubeconfig precedence chain. The lookup key for `defaults:`. The single source of truth for which cluster the CLI talks to. - -## Success Criteria *(mandatory)* - -### Measurable Outcomes - -- **SC-001**: After `rad init` on a clean machine, a user can run `rad app list` and `rad deploy app.bicep` to completion **without ever seeing or typing the word "workspace"**. -- **SC-002**: Help text and getting-started docs contain **zero occurrences** of "workspace" outside the migration guide. -- **SC-003**: A user with an unmodified pre-refactor `~/.rad/config.yaml` can upgrade the CLI and run their previous scoped workflows with **no manual file edits** and **no command failures** caused by schema changes (assuming the active kube context matches the legacy default workspace's `connection.context`). -- **SC-004**: A user with `defaults:` entries for two kube contexts can switch between contexts via `kubectl config use-context` and run scoped `rad` commands against each cluster **with zero additional `rad` configuration steps** between switches. -- **SC-005**: `rad configure --defaults …` either succeeds and persists the value(s), or fails and leaves the config file byte-for-byte unchanged, in **100%** of test runs (including network failures, validation failures, and unknown keys). -- **SC-006**: Every operation previously achievable via `rad workspace switch`, `rad group switch`, or `rad env switch` is achievable via a single `rad configure --defaults …` invocation; **no operation requires more steps after the refactor than before**. -- **SC-007**: Time-to-first-deploy for a new user (time from `rad init` start to a successful `rad deploy app.bicep`) does not increase versus the pre-refactor baseline. -- **SC-008**: Inside the codebase, references to the legacy workspace types are confined to one well-defined compatibility shim package; no scoped command's resolution path reads workspace fields directly. - -## Assumptions - -- **Schema is additive, not replacing**: A new top-level `defaults:` key is added. The existing `workspaces:` key is read for back-compat but never written. This honors the "leave schema as-is if possible" priority while delivering a clean new UX. -- **Kube-context-keyed defaults remove the mismatch problem**: Because the lookup key is the active kube context, switching contexts in `kubectl` automatically selects the matching defaults. There is no longer a "stored context vs. active context" mismatch to detect, prompt about, or auto-update. The earlier proposed FR for context-mismatch UX is therefore moot and dropped from this revision. -- **Hard removal of legacy management commands; flag preserved**: `rad workspace*`, `rad group switch`, and `rad env switch` are removed. The `-w/--workspace ` flag on scoped commands is **preserved** as a one-shot selector that reads the legacy `workspaces.items` block. This keeps existing scripts working and gives users a per-command escape hatch without bringing back the workspace-management surface. -- **One defaults entry per active kube context**: Multiple named "profiles" per context are out of scope. Users wanting multiple environments on the same cluster can use `--group`/`--environment` flags or maintain separate kube contexts. -- **Kubernetes-only connections in scope**: This refactor targets the Kubernetes connection kind. Non-Kubernetes connection kinds (none currently end-user-facing) are out of scope. -- **No new auth model**: Authentication against Kubernetes and Radius control planes is unchanged. -- **Tests as parity oracle**: Existing functional tests for scoped commands provide the parity oracle: every test that passes pre-refactor must pass post-refactor, with `--workspace` removed and `--group`/`--environment` flags or `defaults:` entries used in its place. diff --git a/eng/design-notes/cli/2026-04-workspace-refactor.md b/eng/design-notes/cli/2026-04-workspace-refactor.md index 01cca93fb2..f6ea53a27b 100644 --- a/eng/design-notes/cli/2026-04-workspace-refactor.md +++ b/eng/design-notes/cli/2026-04-workspace-refactor.md @@ -71,7 +71,7 @@ A new user installs the CLI, points `kubectl` at their cluster, and runs `rad in **Why this priority**: This is the entry point for every new user. The combination of "no workspace concept" and "no required config file write" is the defining shape of the new UX. -**Independent Test**: Run `rad init` on a fresh machine, then run `rad env show` with no flags. Confirm that the environment named `default` from the resource group named `default` is shown \u2014 verifying that `rad init` created both, that the literal-`default` fallback resolved both keys without any `defaults:` block being written, and that no user-facing output mentioned \"workspace\". +**Independent Test**: Run `rad init` on a fresh machine, then run `rad env show` with no flags. Confirm that the environment named `default` from the resource group named `default` is shown — verifying that `rad init` created both, that the literal-`default` fallback resolved both keys without any `defaults:` block being written, and that no user-facing output mentioned "workspace". **Acceptance Scenarios**: From 73ff74eb5707a4c7d7d14f692be31511646b6189 Mon Sep 17 00:00:00 2001 From: Zach Casper Date: Tue, 12 May 2026 11:29:48 -0500 Subject: [PATCH 5/5] Aligned with feature spec template Signed-off-by: Zach Casper --- .../cli/2026-04-workspace-refactor.md | 388 ++++++++++-------- 1 file changed, 210 insertions(+), 178 deletions(-) diff --git a/eng/design-notes/cli/2026-04-workspace-refactor.md b/eng/design-notes/cli/2026-04-workspace-refactor.md index f6ea53a27b..7c20dd7e1c 100644 --- a/eng/design-notes/cli/2026-04-workspace-refactor.md +++ b/eng/design-notes/cli/2026-04-workspace-refactor.md @@ -1,47 +1,103 @@ -# Feature Specification: Replace Radius Workspaces with `rad configure --defaults` +# Topic: Replace Radius Workspaces with `rad configure --defaults` -Author: zachcasper +* **Author**: Zach Casper (@zachcasper) +* **Feature Branch**: `workspace-refactor` +* **Status**: Draft +* **Tracking PR**: [radius-project/radius#11775](https://github.com/radius-project/radius/pull/11775) -## Background +## Topic Summary -### The workspace concept confuses users +Radius today exposes a CLI-only concept called a **workspace**, configured through `rad workspace create/list/switch/delete`. Despite the API-style verbs, a workspace is not a Radius API object — it is a client-side bundle stored in `~/.rad/config.yaml` that combines a Kubernetes connection, a default resource group, and a default environment. The naming and command shape causes recurring user confusion, the most common questions being "is a workspace an API resource?", "what is the difference between a workspace, a resource group, and an environment?", and "why does my workspace stop working when I switch clusters?". -Radius today exposes a CLI-only concept called a **workspace**, configured through commands like `rad workspace create`, `rad workspace switch`, `rad workspace list`, and `rad workspace delete`. Despite the API-style verbs, a workspace is **not** a Radius API object — it is purely a client-side bundle stored in `~/.rad/config.yaml` that combines: +This topic replaces the user-visible workspace concept with `rad configure --defaults group= environment=`, modeled on `az configure --defaults`. Defaults are scoped to the active Kubernetes context, so switching clusters with `kubectl config use-context` automatically selects the right defaults. The legacy `workspaces:` block in `~/.rad/config.yaml` is read for backward compatibility but never written by new commands, the `--workspace` flag is preserved as a one-shot selector, and the management commands (`rad workspace*`, `rad group switch`, `rad env switch`) are removed. New users never encounter the word "workspace"; existing scripts and config files keep working without manual migration. -- a Kubernetes connection (a kube context name), -- a default resource group (stored as a fully-qualified URI like `/planes/radius/local/resourceGroups/default`), and -- a default environment (also stored as a URI). +### Top level goals -This naming and command shape causes recurring user confusion: +- Eliminate "workspace" as a user-visible concept in Radius CLI help, docs, and error messages for new users. +- Replace `rad workspace switch` / `rad group switch` / `rad env switch` with a single, discoverable `rad configure --defaults` surface. +- Make per-cluster defaults automatic by keying them on the active Kubernetes context, so `kubectl config use-context` is sufficient to switch Radius targets. +- Preserve backward compatibility for existing `~/.rad/config.yaml` files and for scripts that pass `--workspace `. +- Deliver a working zero-config experience after `rad init` without writing a `defaults:` entry, by falling back to the literal name `default`. -- **"Is a workspace an API resource?"** `rad workspace create` reads like `rad app create` or `rad env create`, which *do* create server-side objects. Users assume workspaces are similar and look for them in `rad resource list`, in the dashboard, or in their cluster's CRDs — where they do not exist. -- **"What is the difference between a workspace, a resource group, and an environment?"** New users see three concepts in the docs (`workspace`, `group`, `environment`) but only two of them (group, environment) actually correspond to anything on the cluster. The third is a CLI-local container whose only job is to remember which group and environment to use by default. -- **"Why do I need to switch workspaces to switch resource groups?"** Day-to-day, what users actually want is to change which resource group or environment their next `rad` commands target. Today that is spread across three commands (`rad workspace switch`, `rad group switch`, `rad env switch`) with subtly different semantics, plus per-command `--workspace`, `--group`, and `--environment` flags whose precedence is not obvious. -- **"Why does my workspace stop working when I switch clusters?"** A workspace pins a specific kube context. If the user switches clusters with `kubectl config use-context`, the workspace either silently uses the old (now-wrong) context or fails opaquely, depending on how the workspace was set up. +### Non-goals (out of scope) -The net effect is a surface area whose primary job is to remember two strings (a default group name and a default environment name) per cluster, but which appears to users as a fourth top-level concept on equal footing with apps, environments, and resource groups. +- Multiple named "profiles" per kube context. Users wanting multiple environments on the same cluster use `--group`/`--environment` flags or maintain separate kube contexts. +- Non-Kubernetes connection kinds. The refactor targets Kubernetes connections only. +- Changes to authentication against Kubernetes or the Radius control plane. +- Replacing or rewriting the existing `workspaces:` schema on disk; the legacy block is left in place and read-only. +- Server-side persistence of CLI defaults. -### `az configure --defaults` as the model +## User profile and challenges -The `az` CLI solves the same problem with a much smaller surface: `az configure --defaults group=my-rg location=westus2`. There is no client-side "workspace" object — `az` simply remembers a few key/value defaults and applies them to subsequent commands. Users discover the feature once, learn one command, and never have to reason about a CLI-only entity that mirrors API objects. +### User persona(s) -This refactor adopts the same model for Radius. Users set defaults with `rad configure --defaults group= environment=`, list them with `rad configure --list-defaults`, and clear individual keys by setting them to an empty value. The defaults are scoped to the active Kubernetes context (so switching clusters with `kubectl config use-context` automatically selects the right defaults), and Radius commands fall back through a clear, per-key precedence chain when a default is not set. New users never encounter the word "workspace"; existing users keep working without manual migration because the legacy `workspaces:` block is still read. +The primary user is a **Radius CLI user** — application developers and platform engineers who interact with Radius through the `rad` command. The most affected sub-population is **new users** in the first hour of using Radius, because the workspace concept appears at `rad init` time and shapes their entire mental model. A secondary user is the **existing user** who already has a populated `~/.rad/config.yaml` and a set of scripts that depend on `--workspace`. -The "workspace" concept is thereby downgraded from a top-level CLI noun to an internal storage detail and a one-shot `-w` flag for users who already rely on named workspace bundles. +### Challenge(s) faced by the user -## Storage strategy +Workspaces are a fourth top-level concept on equal footing with apps, environments, and resource groups, but their only job is to remember two strings (a default group name and a default environment name) per cluster. This causes: -The existing `workspaces:` block in `~/.rad/config.yaml` is **never written** by new commands and is **read only as a fallback** when no `defaults:` entry matches the active Kubernetes context. New commands write a single new top-level block: +- **Mistaking workspaces for API objects.** `rad workspace create` reads like `rad app create`, so users look for workspaces in `rad resource list`, in the dashboard, or in cluster CRDs — where they do not exist. +- **Three concepts where two would do.** Docs introduce workspace, resource group, and environment, but only the latter two correspond to anything on the cluster. +- **Three switch commands instead of one.** Changing what the next `rad` command targets is spread across `rad workspace switch`, `rad group switch`, and `rad env switch`, with subtly different semantics and per-command `--workspace`/`--group`/`--environment` flags whose precedence is not obvious. +- **Silent breakage on cluster switch.** A workspace pins a specific kube context. If the user switches clusters with `kubectl config use-context`, the workspace either silently uses the old (now-wrong) context or fails opaquely. + +The `az` CLI solves the same problem with a much smaller surface (`az configure --defaults group=my-rg location=westus2`), and Radius users already know that pattern from working with Azure. + +### Positive user outcome + +A new user runs `rad init`, then `rad deploy app.bicep`, and never types or sees the word "workspace". An experienced user changes the active kube context and their next `rad` command automatically targets the matching cluster's defaults, with no extra Radius step. An existing user upgrades the CLI and their pre-refactor config file and `--workspace`-using scripts keep working unchanged. The CLI surface for "what does my next command target?" collapses from three commands plus a confusing precedence chain into one command (`rad configure --defaults`) and a clear, per-key resolution order. + +## Key scenarios + +### Scenario 1: First-time user runs `rad init` without writing defaults + +A new user runs `rad init`, which creates a resource group named `default` and an environment named `default` on the cluster. Subsequent `rad` commands resolve to those names via a built-in literal-`default` fallback, so the CLI does not need to write a `defaults:` block at all. No "workspace" is created, named, or mentioned. + +### Scenario 2: Set, list, and clear defaults with `rad configure` + +A user sets defaults with `rad configure --defaults group= environment=`, lists them with `rad configure --list-defaults`, and clears individual keys by setting them to an empty value. Validation runs against the live cluster and the operation is atomic (all-or-nothing). + +### Scenario 3: Per-context defaults make kube context switches automatic + +A user configures defaults for two kube contexts (`dev-cluster`, `prod-cluster`), switches between them with `kubectl config use-context`, and runs `rad` commands. Each invocation automatically picks up the right defaults for the active context — no warnings, no prompts, no `rad config set context` step. + +### Scenario 4: Per-command flags override; `--workspace` remains a one-shot selector + +Every scoped command supports `-g/--group`, `-e/--environment`, and `-w/--workspace` flags. Per-command `-g`/`-e` always win. `-w ` selects a named workspace from the legacy `workspaces.items` block as a single unit for the duration of one command. None of these flags mutate the config file. + +### Scenario 5: Read-only backward compatibility with the legacy `workspaces:` block + +An existing user upgrades the CLI without re-running `rad init`. Their populated `workspaces:` block keeps working. The block is read but never written by new commands; the management commands are gone but the data and the `--workspace` flag remain functional. + +### Scenario 6: JSON-friendly defaults for scripting and CI + +`rad configure --list-defaults --output json` returns a stable, scriptable schema. `rad configure --defaults …` runs non-interactively in CI, exiting non-zero with a stable error code on validation failure. + +## Key dependencies and risks + +- **Dependency: kubeconfig precedence.** "Active kube context" is resolved using the standard kubeconfig precedence chain already used elsewhere in the CLI. No new resolution logic is introduced; this refactor inherits whatever the rest of `rad` already does. +- **Dependency: cluster reachability for validation.** `rad configure --defaults group=` validates against the live cluster before persisting. If the cluster is unreachable, the command fails without mutating the file. The error message must distinguish "cluster unreachable" from "group not found". +- **Risk: silently breaking existing scripts.** Removing `rad workspace`, `rad group switch`, and `rad env switch` will break scripts that call them. Mitigation: those commands fail with a "command removed" error that names the `rad configure --defaults` replacement and links to migration docs. The `--workspace ` flag is preserved on scoped commands so the most common scripted use is unaffected. +- **Risk: surprise from the literal-`default` fallback.** A user who explicitly clears a default expects "no default" behavior, but the literal-`default` fallback could quietly resolve to a `default` group that happens to exist. Mitigation: document the fallback prominently; have `rad configure --list-defaults` show when the literal fallback would apply; remediation messages name `--group`, `--workspace`, and `rad configure --defaults` so the user can correct course quickly. +- **Risk: codebase-wide reach of the legacy `Workspace` type.** `workspaces.Workspace` is referenced by many command paths today. A scattered, half-finished refactor risks two parallel resolution code paths. Mitigation: confine legacy workspace reads to a single compatibility shim package; assert via tests that no scoped command's resolution path reads workspace fields directly (SC-008). +- **Risk: concurrent edits to `~/.rad/config.yaml`.** Two `rad configure --defaults …` invocations in parallel must not corrupt the file. Mitigation: write atomically (temp-file + rename) with file locking; document last-writer-wins semantics. + +## Key assumptions to test and questions to answer + +- **Assumption:** keying defaults on the active Kubernetes context is more intuitive than naming a separate "profile" because users already think in terms of clusters/contexts. Validation: existing user feedback during the rollout; observe whether any user re-asks for a named-profile concept. +- **Assumption:** the literal-`default` fallback (FR-012 step 5) is preferable to writing a `defaults:` entry from `rad init`. The first-run experience stays clean and the config file stays empty until the user changes something. Validation: zero-config flow works in CI on a fresh machine; users do not file bugs about "where is my config file". +- **Assumption:** preserving only the `--workspace` flag (without the management commands) is enough backward compatibility for existing scripts. Validation: survey/issue-tracker review; functional tests covering the `-w ` path. +- **Assumption:** validating against the live cluster on `rad configure --defaults` is acceptable latency. Validation: measure round-trip time during implementation; if it is too slow for CI, add an opt-out flag. +- **Question (open):** how should `rad configure --list-defaults` label values that come from the legacy `workspaces:` block — as `source: workspaces` JSON field plus a visual hint in table output, or as a separate section? To be answered when the listing UI is implemented. +- **Question (open):** should `rad init` re-run on an already-configured machine prompt before touching anything related to defaults? Current intent is yes. To be confirmed during `rad init` work. + +## Current state + +`~/.rad/config.yaml` today contains a `workspaces:` block that combines a Kubernetes connection, a default resource group (as a fully-qualified URI), and a default environment (also a URI): ```yaml -defaults: - my-kubecontext: - group: my-group - environment: my-env - my-other-kubecontext: - group: prod-group - environment: prod-env -workspaces: # read-only fallback (unchanged on disk by new commands) +workspaces: default: default items: default: @@ -50,217 +106,193 @@ workspaces: # read-only fallback (unchanged on disk by new commands) environment: /planes/radius/local/resourceGroups/default/providers/Applications.Core/environments/default ``` -**Resolution order** for the group/environment used by a `rad` command (highest precedence first): - -1. **Per-command `-g/--group` / `-e/--environment` flag** on the individual command. -2. **Per-command `-w/--workspace ` flag**: if present, the named workspace from `workspaces.items.` supplies group (from `scope`), environment (from `environment`), and Kubernetes connection (from `connection.context`) for keys not satisfied by step 1. -3. **`defaults..`** in the new `defaults:` block. -4. **`workspaces.default`** — the entry named by `workspaces.default` supplies group/environment/connection for any key still unresolved. -5. **Built-in fallback**: the literal name `default` is used for `group` and/or `environment` if still unresolved. Because `rad init` creates a resource group named `default` and an environment named `default`, this gives a working zero-config experience without `rad init` needing to write to `~/.rad/config.yaml` at all. -6. Error only if even the built-in `default` fallback is unusable (e.g., the user has explicitly cleared a default with `rad configure --defaults group=` and the literal `default` group does not exist on the cluster). The remediation message names `--group`/`--environment`, `--workspace`, and `rad configure --defaults`. - -Resolution is **per key independently**: e.g., `--group` set on the command line, `environment` resolved from `defaults:`, and Kubernetes connection from `workspaces.default` is a valid combination. Steps stop at the first source that supplies each key. - -The Kubernetes connection has no `default` literal fallback — it must come from `-w`, `workspaces.default`, or the active kube context (the active context is implicit and used by all sources in steps 3–5 unless `-w` or `workspaces.default` overrides it). - -## User Scenarios & Testing *(mandatory)* +The user-facing surface is `rad workspace create/list/show/switch/delete` plus `rad group switch`, `rad env switch`, and per-command `--workspace`/`--group`/`--environment` flags whose precedence is documented but not always obvious in error messages. The `workspaces.Workspace` Go type is referenced widely across command implementations. -### User Story 1 - First-time user runs `rad init` with no config file written for defaults (Priority: P1) +There is no prior in-flight investment in replacing this surface; this topic is the first concrete proposal. -A new user installs the CLI, points `kubectl` at their cluster, and runs `rad init`. The CLI installs the Radius control plane and creates a resource group named `default` and an environment named `default` on the cluster. **It does not need to write a `defaults:` block to `~/.rad/config.yaml`** — subsequent `rad` commands resolve to the literal name `default` via the built-in fallback (FR-012 step 5). No "workspace" is created, named, or mentioned anywhere in user-visible output. +## Details of user problem -**Why this priority**: This is the entry point for every new user. The combination of "no workspace concept" and "no required config file write" is the defining shape of the new UX. +> When I install Radius and run `rad init`, the CLI prints something about a "workspace". I assume a workspace is a Radius resource — like an app or an environment — and I look for it in `rad resource list` and in the dashboard. It's not there. I read the docs and discover that a workspace is something stored in `~/.rad/config.yaml` whose only job is to remember which resource group and environment to use by default. I now have to keep three concepts straight (workspace, resource group, environment) where only two of them are real things on the cluster. -**Independent Test**: Run `rad init` on a fresh machine, then run `rad env show` with no flags. Confirm that the environment named `default` from the resource group named `default` is shown — verifying that `rad init` created both, that the literal-`default` fallback resolved both keys without any `defaults:` block being written, and that no user-facing output mentioned "workspace". +> When I want to point my next `rad` command at a different group or environment, I have to choose between `rad workspace switch`, `rad group switch`, and `rad env switch`, plus per-command `--workspace`/`--group`/`--environment` flags. The precedence is not obvious from the error messages, and I am never sure whether the change I just made is for one command or persisted. -**Acceptance Scenarios**: +> When I switch clusters with `kubectl config use-context`, my Radius workspace silently keeps using the old context (because it pinned it at creation time). My next `rad` command either targets the wrong cluster or fails opaquely. I have to remember to run `rad workspace switch` separately, and I have to keep my workspaces and my kube contexts manually in sync. -1. **Given** a fresh machine with no Radius config and active kube context `my-kubecontext`, **When** the user runs `rad init` and accepts the defaults, **Then** a resource group named `default` and an environment named `default` are created on the cluster, no `defaults:` block is written to `~/.rad/config.yaml` solely for this purpose, and the success message contains no occurrence of "workspace". -2. **Given** a successful `rad init`, **When** the user runs `rad app list` immediately afterward without flags, **Then** the command succeeds by resolving to group `default` and environment `default` via the built-in literal fallback. -3. **Given** a successful `rad init`, **When** the user runs `rad workspace list` or `rad env switch` or `rad group switch`, **Then** the CLI fails with a "command removed" error that names the `rad configure --defaults` replacement. -4. **Given** a successful `rad init` followed by `rad configure --defaults group=my-rg`, **When** the user runs `rad app list`, **Then** the command targets `my-rg` (the configured default takes precedence over the literal `default` fallback). +> The result is that the workspace concept costs me time on day one, costs me confusion every time I switch clusters, and adds a fourth concept that I have to teach to anyone I onboard onto Radius. ---- +## Desired user experience outcome -### User Story 2 - Set, list, and clear defaults with `rad configure` (Priority: P1) +> After this is implemented, I can install Radius, run `rad init`, and immediately deploy an application without ever typing or seeing the word "workspace". When I want different defaults, I run one command — `rad configure --defaults group= environment=` — modeled on the `az configure --defaults` pattern I already know. When I switch Kubernetes clusters with `kubectl config use-context`, my Radius defaults follow automatically because they are keyed on the active context. When I need to override defaults for a single command, I pass `-g`, `-e`, or `-w` and the precedence is consistent and predictable. As a result, my mental model is two concepts (resource group, environment) instead of three, my cluster-switching is one command instead of two, and my onboarding doc is shorter. -A user manages defaults via `rad configure --defaults =` (set), `rad configure --defaults =` (clear, empty value), and `rad configure --list-defaults` (inspect). +### Detailed user experience -**Why this priority**: This is the primary replacement surface for `rad workspace switch`, `rad group switch`, and `rad env switch`. Users must be able to perform every workspace-default operation through `rad configure`. +**Step 1 — `rad init` on a fresh machine.** The user installs the CLI, points `kubectl` at their cluster, and runs `rad init`. The CLI installs the Radius control plane and creates a resource group named `default` and an environment named `default` on the cluster. **No `~/.rad/config.yaml` is written at all** — the file is only created the first time the user runs `rad configure --defaults`. Success output contains no occurrence of "workspace". -**Independent Test**: From any valid Radius config, run `rad configure --defaults group=dev-rg`, `rad configure --defaults environment=dev-env`, then `rad configure --list-defaults`. Verify the values appear under the active kube context and that a scoped command uses them. Then run `rad configure --defaults group=` and verify the next scoped command falls through to the built-in literal `default` fallback (or fails with a clear remediation message if no resource group named `default` exists on the cluster). +**Step 2 — Deploy without configuration.** The user runs `rad deploy app.bicep`. The CLI resolves group and environment to the literal name `default` (FR-012 step 5), targeting the resources `rad init` just created. The deployment succeeds with no extra setup. -**Acceptance Scenarios**: +**Step 3 — Set non-default values when desired.** The user runs: -1. **Given** an active kube context `my-kubecontext` and no existing defaults entry for it, **When** the user runs `rad configure --defaults group=my-rg`, **Then** the CLI validates that `my-rg` exists on the cluster connected via `my-kubecontext`, persists `defaults.my-kubecontext.group: my-rg` to `~/.rad/config.yaml`, and prints a confirmation including the kube context, key, and new value. -2. **Given** existing defaults for `my-kubecontext`, **When** the user runs `rad configure --defaults environment=my-env`, **Then** the CLI validates the environment within the configured default group and persists `defaults.my-kubecontext.environment: my-env`. -3. **Given** a config with defaults for one or more contexts, **When** the user runs `rad configure --list-defaults`, **Then** the CLI prints all `defaults:` entries grouped by kube context (with the active context indicated), and supports `--output json` returning a stable schema. -4. **Given** a default group is set for the active context, **When** the user runs `rad configure --defaults group=` (empty value), **Then** that single key is removed from `defaults.`. Subsequent commands fall through the resolution order; if no `workspaces.default` supplies the key, they fall through to the literal `default` fallback. If the literal `default` group does not exist on the cluster, the command fails with the standard remediation message. -5. **Given** removing the last key for a context leaves an empty map, **When** the operation completes, **Then** the empty `defaults.` entry is removed from the file (no empty `{}` left behind). -6. **Given** the user passes `rad configure --defaults group=` for a group that does not exist on the connected cluster, **When** the command runs, **Then** the CLI fails with a clear error and does not modify the config file. -7. **Given** the user passes multiple key/value pairs, e.g. `rad configure --defaults group=my-rg environment=my-env`, **When** the command runs, **Then** the CLI validates and persists all of them atomically (all-or-nothing) under the active kube context. -8. **Given** the user passes an unknown key, e.g. `rad configure --defaults foo=bar`, **When** the command runs, **Then** the CLI fails with a clear error listing the supported keys (`group`, `environment`) and does not modify the file. +```bash +rad configure --defaults group=my-rg environment=my-env +``` ---- +Both keys are validated against the live cluster, persisted atomically under `defaults.` in `~/.rad/config.yaml`, and confirmed in the success output. -### User Story 3 - Per-context defaults make kube context switches automatic (Priority: P1) +**Step 4 — Inspect defaults.** The user runs `rad configure --list-defaults`. The CLI prints all entries from the new `defaults:` block (grouped by kube context, with the active one highlighted) plus any values resolved from the legacy `workspaces:` block, clearly labeled. `--output json` returns the same data with stable field names. -A user configures defaults for two kube contexts (`dev-cluster` and `prod-cluster`). They switch between them with `kubectl config use-context` and run `rad` commands. Each `rad` invocation automatically picks up the right defaults for the active context — no warnings, no prompts, no `rad config set context` step. +**Step 5 — Clear a single key.** The user runs `rad configure --defaults environment=` (empty value). The `environment` key is removed from `defaults.`. Subsequent commands fall through the resolution order. If the resulting context entry is empty, it is removed; if the resulting `defaults:` block is empty, the block is removed. -**Why this priority**: This is the design's signature feature. The kube-context-keyed `defaults:` block replaces the static workspace-to-context binding and eliminates the mismatch class of problems entirely. +**Step 6 — Switch clusters with `kubectl`.** The user runs `kubectl config use-context prod-cluster && rad app list`. The command targets `prod-cluster` with whatever defaults are configured for that context, automatically — no Radius command in between, no warning, no prompt. -**Independent Test**: Create defaults for two contexts, switch contexts via `kubectl`, run scoped commands without flags, and verify each one targets the matching cluster's defaults. +**Step 7 — One-off override.** The user runs `rad deploy app.bicep -g prod-rg -e prod-env`. The deployment targets `prod-rg`/`prod-env` for that single command; the config file is unchanged. -**Acceptance Scenarios**: +**Step 8 — Existing scripts keep working.** A pre-existing script that runs `rad deploy app.bicep -w azure` continues to work: the CLI resolves connection, group, and environment from the legacy `workspaces.items.azure` entry as a unit. The user gets a deprecation pointer to `rad configure --defaults` only when they invoke removed commands like `rad workspace switch`. -1. **Given** `defaults.dev-cluster.group: dev-rg` and `defaults.prod-cluster.group: prod-rg`, and active context `dev-cluster`, **When** the user runs `rad app list`, **Then** the command targets `dev-cluster` with group `dev-rg`. -2. **Given** the same config, **When** the user runs `kubectl config use-context prod-cluster && rad app list`, **Then** the command targets `prod-cluster` with group `prod-rg` automatically — no warning or prompt. -3. **Given** active kube context `unknown-cluster` and no `defaults.unknown-cluster` entry, no `workspaces.default` resolving the missing keys, and resource groups/environments named `default` exist on `unknown-cluster`, **When** the user runs `rad app list`, **Then** the command succeeds via the literal `default` fallback. If those literal-`default` resources do not exist, the command fails with a remediation message naming `--group`, `--workspace`, and `rad configure --defaults`. -4. **Given** the user has no active kube context (e.g., `KUBECONFIG` empty or current-context unset) and no `--workspace` flag is passed, **When** the user runs any scoped `rad` command, **Then** the CLI fails fast with a remediation message pointing at `kubectl config use-context` and `rad init`. (If `--workspace ` is passed, that workspace's `connection.context` is used and the command proceeds.) +**Resolution order** (per key independently, applied by every scoped command): ---- +1. Per-command `-g/--group` / `-e/--environment` flag. +2. Per-command `-w/--workspace ` flag (the named entry from `workspaces.items` supplies group/environment/connection for keys not yet resolved). +3. `defaults..` from the new `defaults:` block. +4. `workspaces.default` (the workspace named by `workspaces.default` supplies group/environment/connection for keys still unresolved). +5. Built-in literal `default` for the `group` and `environment` keys only. +6. Error — only when the active kube context is unset and no `-w` was passed, or when the user explicitly cleared a default and the literal `default` does not exist on the cluster. The remediation message names `--group`, `--environment`, `--workspace`, and `rad configure --defaults`. -### User Story 4 - Per-command flags override; `--workspace` remains a one-shot selector (Priority: P1) +## Breaking changes -Every scoped command supports `-g/--group`, `-e/--environment`, and `-w/--workspace` flags. Per-command `-g`/`-e` flags always win. `-w ` selects a named workspace from the legacy `workspaces.items` block as a single unit (its `connection.context`, `scope`, and `environment` are used for any keys not already supplied by `-g`/`-e`). None of these flags mutate the config file. +This refactor removes user-visible CLI surface. The changes below are breaking; everything not listed here is preserved or unchanged. -**Why this priority**: Without consistent override semantics, the new model is worse than workspaces. `-w` is preserved so existing scripts and users who rely on named workspaces keep working without re-tooling. +| Command | Change | Replacement | Notes | +|---|---|---|---| +| `rad workspace create` | Removed | `rad configure --defaults group= environment=` | Validates against the live cluster before persisting. | +| `rad workspace list` | Removed | `rad configure --list-defaults` (also `--output json`) | Includes values from the legacy `workspaces:` block, clearly labeled. | +| `rad workspace show` | Removed | `rad configure --list-defaults` | No per-entry "show" verb. | +| `rad workspace switch` | Removed | `rad configure --defaults group= environment=`; for cluster switching, use `kubectl config use-context` | Defaults are keyed on the active kube context, so a `kubectl` switch is sufficient to switch Radius targets. | +| `rad workspace delete` | Removed | `rad configure --defaults group= environment=` (clear keys) | Clearing the last key removes the context entry; clearing the last entry removes the file. | +| `rad group switch` | Removed | `rad configure --defaults group=` | Per-key default; persisted under the active kube context. | +| `rad env switch` | Removed | `rad configure --defaults environment=` | Per-key default; persisted under the active kube context. | +| `~/.rad/config.yaml` `workspaces:` block (writes) | No longer written by any new command | New `defaults:` block | Existing `workspaces:` entries on disk are preserved and read as a fallback. | -**Independent Test**: With defaults set, run scoped commands without flags and confirm they target the defaults. Repeat with `-g` and `-e` and confirm they override without changing the config. Repeat with `-w ` against a config that contains a non-default named workspace and confirm the command targets that workspace's group, environment, and connection — still without mutating the file. +**Preserved (not breaking):** -**Acceptance Scenarios**: +- The `-w/--workspace ` flag on every scoped command. It continues to read `workspaces.items.` from the legacy block. +- The `~/.rad/config.yaml` `workspaces:` block on disk. Existing entries keep working as a fallback in the resolution chain. +- The `-g/--group` and `-e/--environment` flags on every scoped command. -1. **Given** `defaults.my-kubecontext.group: dev-rg, environment: dev-env` and active context `my-kubecontext`, **When** the user runs `rad deploy app.bicep`, **Then** the deployment targets `dev-rg`/`dev-env` and the config file is unchanged. -2. **Given** the same defaults, **When** the user runs `rad deploy app.bicep -g prod-rg -e prod-env`, **Then** the deployment targets `prod-rg`/`prod-env`, the config file is unchanged, and no prompt or warning about workspaces appears. -3. **Given** a config containing `workspaces.items.azure` with its own `connection.context`, `scope`, and `environment`, **When** the user runs `rad deploy app.bicep -w azure`, **Then** the deployment uses that workspace's connection, group, and environment as a unit; per-command `-g`/`-e` if also supplied still take precedence over the workspace's values. -4. **Given** no group default for the active context, no `--workspace`, no legacy `workspaces.default` resolving the key, **and** no resource group named `default` exists on the cluster, **When** the user runs `rad app list` without `--group`, **Then** the CLI fails with a remediation message that names `--group`, `--workspace`, and `rad configure --defaults`. (If a resource group named `default` does exist, the literal-`default` fallback satisfies the key and the command succeeds.) +**User impact:** ---- +- Scripts that call any removed subcommand fail with a "command removed" error that names the `rad configure --defaults` replacement and links to migration docs. They will not silently misbehave. +- Scripts that pass `-w ` keep working unchanged. +- Existing config files keep working unchanged. No migration step is required to upgrade. -### User Story 5 - Read-only back-compat with the legacy `workspaces:` block (Priority: P2) +## Key investments -An existing user upgrades the CLI without re-running `rad init`. Their `~/.rad/config.yaml` already contains a populated `workspaces:` block. Their workflows continue to work unchanged. The legacy block is read but never written by the new CLI; the management commands are gone but the data and the `--workspace` flag remain functional. +### Feature 1 — `rad configure --defaults` command surface -**Why this priority**: Keeps existing users from being broken on upgrade, while still moving the codebase to the new model. +A new `rad configure` command supporting `--defaults = [=…]` (set), `--defaults =` with empty value (clear), and `--list-defaults` (inspect, with `--output json`). Supported keys are `group` and `environment`; unknown keys fail with a message listing the supported set. All `--defaults` operations target the active kube context only — there is no flag to target a different context. Multi-key invocations are atomic: validation succeeds for every key before any write, and any failure leaves the file unchanged. -**Independent Test**: Take an existing pre-refactor `~/.rad/config.yaml` (with `workspaces.default` and at least one item with `scope`, `environment`, `connection.context`). Upgrade the CLI. Run `rad app list` without modifying the file. Verify the command resolves group, environment, and Kubernetes connection from `workspaces.default`. +### Feature 2 — `defaults:` storage block -**Acceptance Scenarios**: +A new top-level `defaults:` block in `~/.rad/config.yaml`, keyed by Kubernetes context name, with `group` and `environment` subkeys (string names, not URIs). Writes are atomic (temp-file + rename) and safe under concurrent execution (last-writer-wins acceptable). Clearing the last key in a context entry removes the entry; clearing the last entry removes the block. -1. **Given** a pre-refactor config with `workspaces.default: default`, `workspaces.items.default.connection.context: my-kubecontext`, `workspaces.items.default.scope: /planes/radius/local/resourceGroups/foo`, and `workspaces.items.default.environment: /planes/radius/local/resourceGroups/foo/providers/Applications.Core/environments/bar`, **When** the user runs `rad app list`, **Then** the command targets group `foo`, environment `bar`, and the cluster reachable via kube context `my-kubecontext`, all resolved from `workspaces.default`. -2. **Given** the same config, **When** the user runs `rad configure --list-defaults`, **Then** the listing includes the values resolved from `workspaces.default`, clearly labels them as coming from the legacy block, and points the user at `rad configure --defaults group=...` if they want to migrate. -3. **Given** the same config, **When** the user runs `rad configure --defaults group=new-rg` while the active kube context is `my-kubecontext`, **Then** the CLI writes `defaults.my-kubecontext.group: new-rg`, **does not modify** the `workspaces:` block, and on subsequent commands `defaults.my-kubecontext.group` takes precedence over `workspaces.default.scope`. -4. **Given** no `defaults:` entry for the active kube context, no `workspaces.default`, and no resource group/environment named `default` on the cluster, **When** any scoped command runs without `-g`/`-e`/`-w`, **Then** it fails with the standard remediation message naming all three flags and `rad configure --defaults`. (If `default`/`default` resources exist, the literal fallback satisfies the keys and the command succeeds.) -5. **Given** the user invokes `rad workspace create/list/show/switch/delete`, `rad group switch`, or `rad env switch`, **When** they run, **Then** they fail with a "command removed" error naming the `rad configure --defaults` replacement and pointing at migration docs. The `--workspace ` flag, however, remains functional on scoped commands. +### Feature 3 — Per-key resolution chain shared by all scoped commands ---- +All scoped commands (`rad deploy`, `rad app *`, `rad env *`, `rad group *`, `rad resource *`, `rad recipe *`, `rad credential *`, …) resolve group, environment, and Kubernetes connection through the precedence order in _Detailed user experience → Resolution order_, applied per key independently. A single shared resolver implements this so no scoped command reads workspace fields directly (SC-008). -### User Story 6 - JSON / scripting friendliness (Priority: P3) +### Feature 4 — `rad init` zero-config flow -A user automating Radius operations in CI wants a stable, scriptable interface for reading and setting defaults. +`rad init` creates a resource group named `default` and an environment named `default` on the connected cluster. It does not create or write `~/.rad/config.yaml` at all — the literal-`default` resolution rule (Feature 3, step 5) provides a working zero-config experience with no file on disk. The config file is created the first time the user runs `rad configure --defaults`. No user-facing output mentions "workspace". -**Why this priority**: Useful but not blocking; humans can use the table output during the rollout. +### Feature 5 — Removal of legacy management commands; preservation of `--workspace` -**Independent Test**: Run `rad configure --list-defaults --output json` and pipe to `jq`. Run `rad configure --defaults group=` in CI without TTY and confirm non-interactive success when the target exists. +`rad workspace create/list/show/switch/delete`, `rad group switch`, and `rad env switch` are removed. Invocations fail with a "command removed" error that names the `rad configure --defaults` replacement and links to migration docs. The `-w/--workspace ` flag on scoped commands is **preserved** as a per-command, one-shot selector that reads the legacy `workspaces.items` block. The Go `workspaces.Workspace` type may remain inside the codebase, confined to a compatibility shim package, but is not exposed in any new CLI surface other than the `--workspace` flag. -**Acceptance Scenarios**: +### Feature 6 — Read-only backward compatibility with the legacy `workspaces:` block -1. **Given** a configured CLI, **When** the user runs `rad configure --list-defaults --output json`, **Then** the output is valid JSON keyed by kube context with stable field names (`group`, `environment`, plus a `source` field of `"defaults"` or `"workspaces"`). -2. **Given** a CI environment with no TTY, **When** the user runs `rad configure --defaults group=my-rg`, **Then** the command succeeds without prompts when the group exists, and fails non-zero with a stable error code when it does not. +The existing `workspaces:` block is read but never written by new commands. The workspace named by `workspaces.default` supplies any keys still unresolved after `defaults:` (Feature 3, step 4). `rad configure --list-defaults` surfaces values from this source with a clear label and a hint to migrate. New commands writing to the file leave the `workspaces:` block byte-for-byte unchanged. ---- +### Feature 7 — Documentation, error messages, and migration guide -### Edge Cases +All new help text, getting-started docs, and error messages avoid introducing the term "workspace" to new users. Documentation includes a migration guide from `rad workspace`/`rad group switch`/`rad env switch` to `rad configure --defaults`, and from a populated `workspaces:` block to a `defaults:` block, with side-by-side equivalents. Remediation messages on resolution failure name `--group`, `--environment`, `--workspace`, and `rad configure --defaults` together so the user can pick the right escape hatch. -- **Cluster unreachable during `rad configure --defaults`**: Fail without mutating the config; clearly distinguish "cluster unreachable" from "group not found". -- **Group default set, environment unset, no `default` environment on cluster**: Commands needing only a group succeed; commands needing an environment fall through to the literal `default` fallback and fail with a precise "no default environment" message only if a `default` environment does not exist. -- **Both unset, fresh install before `rad init`**: `rad configure --list-defaults` prints an empty result and a hint to run `rad init`. -- **Stale environment value**: Default environment was deleted out of band. Next scoped command fails with a remediation (run `rad env list`, then `rad configure --defaults environment=`). -- **Concurrent edits to `~/.rad/config.yaml`**: Two `rad configure --defaults …` invocations run in parallel; the file must not become corrupted (last-writer-wins with file locking is acceptable). -- **`rad init` re-run on an already-configured machine**: Existing defaults for the active context must not be silently overwritten without user confirmation. -- **Active kube context contains characters unusual in YAML keys** (dots, slashes, colons): These MUST be preserved verbatim as the YAML map key (quoted as needed). -- **Two contexts point at the same cluster**: Treated as independent entries in `defaults:`; the user can configure them identically or differently. No deduplication by Radius. -- **`KUBECONFIG` references multiple files / a non-default path**: The "active kube context" is resolved via the standard kubeconfig precedence rules used elsewhere in the CLI; no new resolution logic is introduced. -- **Legacy block contains a workspace whose `connection.context` matches the active kube context AND `defaults.` exists**: `defaults:` always wins over `workspaces.default`, key by key. Missing keys fall through to `workspaces.default` per the resolution order — the two sources are merged per-key. -- **`-w ` names a workspace that does not exist in `workspaces.items`**: Command fails with a clear error before contacting the cluster. -- **`-w ` plus `-g`/`-e` flags**: `-g`/`-e` win for those keys; the workspace supplies the remaining keys (notably `connection.context`). -- **`-w` is set but the workspace has no `scope` or `environment`** (e.g., the `azure` entry in the user's example config): the workspace supplies only `connection.context`; group/environment must come from `-g`/`-e`/`defaults:`/`workspaces.default` per the standard resolution order, otherwise a clear error. +## Detailed Requirements (appendix) -## Requirements *(mandatory)* +The detailed FRs and acceptance scenarios that the implementation tracks against are kept here for reference; they expand on _Key investments_ and _Detailed user experience_ above. -### Functional Requirements +### Functional requirements #### Command surface -- **FR-001**: The CLI MUST expose a `rad configure` command. It MUST support `--defaults = [=…]` (set), `--defaults =` with an empty value (clear that key), and `--list-defaults` (inspect). `--list-defaults` MUST support `--output json`. -- **FR-002**: Supported keys for `--defaults` MUST include `group` and `environment`. Unknown keys MUST fail the command with a message listing supported keys. -- **FR-003**: All `--defaults` operations MUST always target the entry for the **active Kubernetes context** in the new `defaults:` block. There is no flag to target a different context. -- **FR-004**: `rad configure --defaults group=` MUST validate that the named resource group exists on the cluster reachable via the active kube context before persisting. `rad configure --defaults environment=` MUST validate the environment exists within the configured (or already-set) default group. -- **FR-005**: When multiple keys are provided in one invocation (e.g., `group=… environment=…`), the operation MUST be atomic: validation of all keys MUST succeed before any write; on any failure, the file MUST be unchanged. -- **FR-006**: `rad configure --list-defaults` MUST display all entries from the `defaults:` block, plus any values resolved from the legacy `workspaces:` block (clearly labeled as such), with the active kube context highlighted. +- **FR-001** The CLI MUST expose a `rad configure` command supporting `--defaults = [=…]` (set), `--defaults =` (clear), and `--list-defaults` (inspect). `--list-defaults` MUST support `--output json`. +- **FR-002** Supported keys for `--defaults` MUST include `group` and `environment`. Unknown keys MUST fail with a message listing supported keys. +- **FR-003** All `--defaults` operations MUST target the entry for the active Kubernetes context. There is no flag to target a different context. +- **FR-004** `rad configure --defaults group=` MUST validate that the named resource group exists on the cluster reachable via the active kube context before persisting. `rad configure --defaults environment=` MUST validate the environment exists within the configured (or already-set) default group. +- **FR-005** When multiple keys are provided in one invocation, the operation MUST be atomic: validation of all keys MUST succeed before any write; on any failure, the file MUST be unchanged. +- **FR-006** `rad configure --list-defaults` MUST display all entries from the `defaults:` block, plus any values resolved from the legacy `workspaces:` block (clearly labeled), with the active kube context highlighted. #### Storage and schema -- **FR-007**: New commands MUST write defaults to a top-level `defaults:` block keyed by Kubernetes context name. Within each context entry, supported subkeys are `group` and `environment` (string values, names — not URIs). -- **FR-008**: New commands MUST NOT write to the legacy `workspaces:` block under any circumstance. -- **FR-009**: Setting a key to an empty value (clear) MUST remove that key from `defaults.`. If the resulting context entry has no remaining keys, the entry itself MUST be removed. If the resulting `defaults:` block is empty, the block MUST be removed. -- **FR-010**: `rad configure --defaults …` operations MUST be safe under concurrent execution (no file corruption); last-writer-wins is acceptable. The file MUST be written atomically (write-temp-then-rename or equivalent). -- **FR-011**: On any validation failure, the config file MUST remain byte-for-byte unchanged. +- **FR-007** New commands MUST write defaults to a top-level `defaults:` block keyed by Kubernetes context name. Within each context entry, supported subkeys are `group` and `environment` (string names, not URIs). +- **FR-008** New commands MUST NOT write to the legacy `workspaces:` block. +- **FR-009** Setting a key to an empty value MUST remove that key from `defaults.`. If the resulting context entry has no remaining keys, the entry itself MUST be removed. If the resulting `defaults:` block is empty, the block MUST be removed. +- **FR-010** `rad configure --defaults …` operations MUST be safe under concurrent execution (no file corruption); last-writer-wins is acceptable. The file MUST be written atomically. +- **FR-011** On any validation failure, the config file MUST remain byte-for-byte unchanged. #### Resolution and command behavior -- **FR-012**: All scoped commands (including but not limited to `rad deploy`, `rad app list/show/delete/connections/status/graph`, `rad env list/show/create/delete`, `rad group list/show/create/delete`, `rad resource list/show/delete`, `rad recipe list/show/register/unregister`, `rad credential …`) MUST resolve group, environment, and Kubernetes connection using this exact order, applied **per key independently**: - 1. Per-command `-g/--group` / `-e/--environment` flag. - 2. Per-command `-w/--workspace ` flag: if present, the named entry in `workspaces.items.` supplies any keys (group from `scope`, environment from `environment`, connection from `connection.context`) not yet resolved by step 1. - 3. `defaults..` from the new `defaults:` block. - 4. `workspaces.default`: the workspace named by `workspaces.default` supplies any keys still unresolved (group from `scope`, environment from `environment`, connection from `connection.context`). - 5. **Built-in literal `default`**: for the `group` and `environment` keys only, the literal string `default` MUST be used if no earlier source supplied a value. This pairs with `rad init`'s creation of a resource group named `default` and an environment named `default` to deliver a working zero-config experience. - 6. Error — only reachable when the resolved Kubernetes connection cannot be determined, **or** when the user has explicitly cleared a default (e.g., via `rad configure --defaults group=`) and the literal `default` does not exist on the cluster. The remediation message MUST name `--group`, `--environment`, `--workspace`, and `rad configure --defaults`. -- **FR-013**: Per-key resolution MUST stop at the first source supplying a value for that key. Sources MAY supply different keys: e.g., `-g` from the command line, `environment` from `defaults:`, and `connection.context` from `workspaces.default` is a valid combined resolution. -- **FR-014**: When the Kubernetes connection cannot be determined from any source (no `-w`, no active kube context, no usable `workspaces.default`), any scoped command MUST fail fast with a remediation message and MUST NOT pick a default arbitrarily. -- **FR-015**: When the resolved Kubernetes connection refers to a cluster that is unreachable, command behavior MUST mirror today's behavior for unreachable clusters (this refactor does not change connection-failure semantics). +- **FR-012** All scoped commands MUST resolve group, environment, and Kubernetes connection using the per-key precedence order described in _Detailed user experience → Resolution order_. +- **FR-013** Per-key resolution MUST stop at the first source supplying a value for that key. Sources MAY supply different keys. +- **FR-014** When the Kubernetes connection cannot be determined from any source, any scoped command MUST fail fast with a remediation message and MUST NOT pick a default arbitrarily. +- **FR-015** When the resolved Kubernetes connection refers to a cluster that is unreachable, command behavior MUST mirror today's behavior for unreachable clusters (this refactor does not change connection-failure semantics). #### `rad init` -- **FR-016**: `rad init` MUST NOT create, name, or reference a "workspace" in any user-facing output. -- **FR-017**: `rad init` MUST create a resource group named `default` and an environment named `default` on the connected cluster (matching the literal-`default` fallback in FR-012 step 5). It MUST NOT write to `~/.rad/config.yaml` for the purpose of recording these defaults; the literal-`default` resolution rule provides the same effect with no file mutation. Users who want non-`default` names use `rad configure --defaults group= environment=` after `rad init`. -- **FR-017a**: `rad init` MAY still write `~/.rad/config.yaml` for non-default purposes (e.g., recording cloud-provider credentials or other configuration outside the `defaults:` and `workspaces:` blocks). Any such writes MUST NOT touch the `defaults:` or `workspaces:` blocks. +- **FR-016** `rad init` MUST NOT create, name, or reference a "workspace" in any user-facing output. +- **FR-017** `rad init` MUST create a resource group named `default` and an environment named `default` on the connected cluster (matching the literal-`default` fallback). It MUST NOT create or write `~/.rad/config.yaml`. The config file is created only on the first invocation of `rad configure --defaults`. #### Removal of legacy commands -- **FR-018**: The following commands MUST be removed: `rad workspace create`, `rad workspace list`, `rad workspace show`, `rad workspace switch`, `rad workspace delete`, `rad group switch`, `rad env switch`. Invoking any of them MUST fail with a "command removed" error message that names the `rad configure --defaults` replacement and links to migration docs. -- **FR-019**: The `-w/--workspace ` **flag** on scoped commands MUST be **preserved**. It selects a named entry from `workspaces.items` for the duration of one command invocation per FR-012 step 2. Help text for the flag MUST describe it as a per-command override that reads the legacy `workspaces:` block. -- **FR-020**: The Go type `workspaces.Workspace` and related infrastructure MAY remain inside the codebase. It MUST NOT be exposed in any new public CLI surface other than the `--workspace` flag described in FR-019, and new help text/documentation MUST NOT introduce the term "workspace" outside the `--workspace` flag's own help and the migration guide. +- **FR-018** `rad workspace create/list/show/switch/delete`, `rad group switch`, and `rad env switch` MUST be removed. Invoking any of them MUST fail with a "command removed" error naming the `rad configure --defaults` replacement and linking to migration docs. +- **FR-019** The `-w/--workspace ` flag on scoped commands MUST be preserved. It selects a named entry from `workspaces.items` for the duration of one command invocation per FR-012 step 2. Help text MUST describe it as a per-command override that reads the legacy `workspaces:` block. +- **FR-020** The Go type `workspaces.Workspace` and related infrastructure MAY remain inside the codebase, confined to a compatibility shim package. New help text and documentation MUST NOT introduce the term "workspace" outside the `--workspace` flag's own help and the migration guide. #### Documentation and discoverability -- **FR-021**: All new help text, getting-started docs, and error messages MUST avoid introducing the term "workspace" to new users. Documentation MUST include a migration guide from `rad workspace`/`rad group switch`/`rad env switch` to `rad configure --defaults`, and from a populated `workspaces:` block to a `defaults:` block (showing equivalent commands). - -### Key Entities - -- **Defaults Entry**: A map keyed by Kubernetes context name. Each value contains the default `group` (resource group name) and `environment` (environment name) for `rad` commands run while that context is active. Stored under the new top-level `defaults:` key in `~/.rad/config.yaml`. Replaces the user-visible "workspace" concept. -- **Legacy Workspace Entry**: The pre-existing structure under `workspaces.items.` containing `connection`, `scope`, and `environment`. Read-only after this refactor. Used (a) per-key as a fallback when `defaults.` does not provide a value (the workspace named by `workspaces.default`), and (b) as a one-shot override when the user passes `-w ` on a command. -- **Active Kubernetes Context**: The current `current-context` resolved from the standard kubeconfig precedence chain. The lookup key for `defaults:`. The single source of truth for which cluster the CLI talks to. - -## Success Criteria *(mandatory)* - -### Measurable Outcomes - -- **SC-001**: After `rad init` on a clean machine, a user can run `rad app list` and `rad deploy app.bicep` to completion **without ever seeing or typing the word "workspace"**. -- **SC-002**: Help text and getting-started docs contain **zero occurrences** of "workspace" outside the migration guide. -- **SC-003**: A user with an unmodified pre-refactor `~/.rad/config.yaml` can upgrade the CLI and run their previous scoped workflows with **no manual file edits** and **no command failures** caused by schema changes (assuming the active kube context matches the legacy default workspace's `connection.context`). -- **SC-004**: A user with `defaults:` entries for two kube contexts can switch between contexts via `kubectl config use-context` and run scoped `rad` commands against each cluster **with zero additional `rad` configuration steps** between switches. -- **SC-005**: `rad configure --defaults …` either succeeds and persists the value(s), or fails and leaves the config file byte-for-byte unchanged, in **100%** of test runs (including network failures, validation failures, and unknown keys). -- **SC-006**: Every operation previously achievable via `rad workspace switch`, `rad group switch`, or `rad env switch` is achievable via a single `rad configure --defaults …` invocation; **no operation requires more steps after the refactor than before**. -- **SC-007**: Time-to-first-deploy for a new user (time from `rad init` start to a successful `rad deploy app.bicep`) does not increase versus the pre-refactor baseline. -- **SC-008**: Inside the codebase, references to the legacy workspace types are confined to one well-defined compatibility shim package; no scoped command's resolution path reads workspace fields directly. - -## Assumptions - -- **Schema is additive, not replacing**: A new top-level `defaults:` key is added. The existing `workspaces:` key is read for back-compat but never written. This honors the "leave schema as-is if possible" priority while delivering a clean new UX. -- **Kube-context-keyed defaults remove the mismatch problem**: Because the lookup key is the active kube context, switching contexts in `kubectl` automatically selects the matching defaults. There is no longer a "stored context vs. active context" mismatch to detect, prompt about, or auto-update. The earlier proposed FR for context-mismatch UX is therefore moot and dropped from this revision. -- **Hard removal of legacy management commands; flag preserved**: `rad workspace*`, `rad group switch`, and `rad env switch` are removed. The `-w/--workspace ` flag on scoped commands is **preserved** as a one-shot selector that reads the legacy `workspaces.items` block. This keeps existing scripts working and gives users a per-command escape hatch without bringing back the workspace-management surface. -- **One defaults entry per active kube context**: Multiple named "profiles" per context are out of scope. Users wanting multiple environments on the same cluster can use `--group`/`--environment` flags or maintain separate kube contexts. -- **Kubernetes-only connections in scope**: This refactor targets the Kubernetes connection kind. Non-Kubernetes connection kinds (none currently end-user-facing) are out of scope. -- **No new auth model**: Authentication against Kubernetes and Radius control planes is unchanged. -- **Tests as parity oracle**: Existing functional tests for scoped commands provide the parity oracle: every test that passes pre-refactor must pass post-refactor, with `--workspace` removed and `--group`/`--environment` flags or `defaults:` entries used in its place. +- **FR-021** All new help text, getting-started docs, and error messages MUST avoid introducing the term "workspace" to new users. Documentation MUST include a migration guide. + +### Acceptance scenarios + +The acceptance scenarios that map one-to-one to the user stories in _Key scenarios_ are tracked alongside the implementation tasks. The most important of them: + +1. After `rad init` on a clean machine, `rad app list` and `rad deploy app.bicep` succeed via the literal-`default` fallback with no `defaults:` written. +2. `rad configure --defaults group=my-rg environment=my-env` validates and persists atomically; a single bad key leaves the file unchanged. +3. With `defaults.dev-cluster` and `defaults.prod-cluster` configured, `kubectl config use-context prod-cluster && rad app list` automatically targets `prod-cluster` with no Radius step in between. +4. `rad deploy app.bicep -g prod-rg -e prod-env` overrides defaults for that command without mutating the config file. +5. A pre-refactor `~/.rad/config.yaml` keeps working unchanged; `rad workspace switch` fails with a "command removed" error naming `rad configure --defaults`; `rad deploy -w azure` continues to work. +6. `rad configure --list-defaults --output json` returns valid JSON keyed by kube context with stable field names (`group`, `environment`, `source`). + +### Edge cases + +- Cluster unreachable during `rad configure --defaults`: fail without mutating the config; distinguish "cluster unreachable" from "group not found". +- Group default set, environment unset, no `default` environment on cluster: commands needing only a group succeed; commands needing an environment fall through to literal `default` and fail with a precise message only if no `default` environment exists. +- Both unset, fresh install before `rad init`: `rad configure --list-defaults` prints empty and hints to run `rad init`. +- Stale environment value: default environment was deleted out of band. Next scoped command fails with a remediation (run `rad env list`, then `rad configure --defaults environment=`). +- Concurrent edits: file must not corrupt; last-writer-wins with file locking is acceptable. +- `rad init` re-run on an already-configured machine: existing defaults for the active context must not be silently overwritten without confirmation. +- Active kube context contains characters unusual in YAML keys: preserved verbatim, quoted as needed. +- Two contexts pointing at the same cluster: independent entries; no deduplication. +- `KUBECONFIG` references multiple files: standard kubeconfig precedence; no new resolution logic. +- Legacy block contains a workspace whose `connection.context` matches the active kube context AND `defaults.` exists: `defaults:` always wins per key; missing keys fall through to `workspaces.default` per the resolution order. +- `-w ` names a workspace not in `workspaces.items`: command fails with a clear error before contacting the cluster. +- `-w ` plus `-g`/`-e`: `-g`/`-e` win for those keys; the workspace supplies the remaining keys (notably `connection.context`). +- `-w` set but the workspace has no `scope` or `environment`: the workspace supplies only `connection.context`; group/environment must come from `-g`/`-e`/`defaults:`/`workspaces.default`, otherwise a clear error. + +### Success criteria + +- **SC-001** After `rad init` on a clean machine, a user can run `rad app list` and `rad deploy app.bicep` to completion without ever seeing or typing the word "workspace". +- **SC-002** Help text and getting-started docs contain zero occurrences of "workspace" outside the migration guide. +- **SC-003** A user with an unmodified pre-refactor `~/.rad/config.yaml` can upgrade the CLI and run their previous scoped workflows with no manual file edits and no command failures caused by schema changes. +- **SC-004** A user with `defaults:` entries for two kube contexts can switch between contexts via `kubectl config use-context` and run scoped `rad` commands against each cluster with zero additional `rad` configuration steps between switches. +- **SC-005** `rad configure --defaults …` either succeeds and persists the value(s), or fails and leaves the config file byte-for-byte unchanged, in 100% of test runs. +- **SC-006** Every operation previously achievable via `rad workspace switch`, `rad group switch`, or `rad env switch` is achievable via a single `rad configure --defaults …` invocation; no operation requires more steps after the refactor than before. +- **SC-007** Time-to-first-deploy for a new user does not increase versus the pre-refactor baseline. +- **SC-008** Inside the codebase, references to the legacy workspace types are confined to one well-defined compatibility shim package; no scoped command's resolution path reads workspace fields directly.