diff --git a/enhancements/OSAC-1421-cluster-and-vm-provisioning-wizard/design.md b/enhancements/OSAC-1421-cluster-and-vm-provisioning-wizard/design.md index 76ba5f7df..227f258f2 100644 --- a/enhancements/OSAC-1421-cluster-and-vm-provisioning-wizard/design.md +++ b/enhancements/OSAC-1421-cluster-and-vm-provisioning-wizard/design.md @@ -3,7 +3,7 @@ title: cluster-and-vm-provisioning-wizard authors: - brotman@redhat.com creation-date: 2026-06-22 -last-updated: 2026-07-09 +last-updated: 2026-07-27 tracking-link: - https://redhat.atlassian.net/browse/OSAC-1421 prd: @@ -27,7 +27,7 @@ Rewrite the osac-ui catalog provision wizard with static fields per resource typ - Rewrite `catalogProvision/` with Formik/Yup, PatternFly Wizard, `OsacForm`, i18n (`useTranslation`), shared Formik-connected field components, and per-adapter Configuration/Networking step components. - Host the wizard on routed create pages; list **Create** navigates to `/vms/create` or `/clusters/create`. -- Implement the cluster adapter end-to-end (catalog, tenant-managed `node_sets` table, create). +- Implement the cluster adapter end-to-end (catalog, template-driven `node_sets` rows, create). - Next always enabled; validate every field on the current step when Next is clicked, including fields that have not blurred. - On successful create, navigate to the VM or cluster Details page using `id` from the POST response (`/vms/{id}`, `/clusters/{id}`). - Component tests (Vitest + jsdom + Testing Library) cover step validation, Back navigation with preserved values, Cancel/discard guard, and submit error paths for both VM and cluster adapters (see [Test Plan](#test-plan)). @@ -36,9 +36,9 @@ Rewrite the osac-ui catalog provision wizard with static fields per resource typ Rewrite under `osac-ui/apps/app-frontend/src/components/catalogProvision/`. `CatalogProvisionWizard` embeds in create pages and owns shared steps (Catalog Item, General, Review). **Configuration** and **Networking** are adapter components — VM pickers and cluster `node_sets`/CIDR fields are not shareable. -Static field paths are hardcoded per resource type (PRD §2.1.1). Catalog `field_definitions` overlay matching static paths on **Configuration**, **Networking** non-picker fields, and **General basics** fields (`ssh_key`, `ssh_public_key`, `pull_secret`) for `display_name`, `editable`, and `validation_schema`. Picker-backed paths (`spec.instance_type`, `spec.network_attachments` and nested paths, cluster `spec.node_sets` host type per row) ignore catalog `field_definitions` in v1. Create payloads include only PRD §2.1.1 paths plus catalog item reference; VM hardcodes `spec.image.source_type` = `registry`. +Static field paths are hardcoded per resource type (PRD §2.1.1). Catalog `field_definitions` overlay matching static paths on **Configuration**, **Networking** non-picker fields, and **General basics** fields (`ssh_key`, `ssh_public_key`, `pull_secret`) for `display_name`, `editable`, and `validation_schema`. Picker-backed paths (`spec.instance_type`, `spec.network_attachments` and nested paths) ignore catalog `field_definitions` in v1; cluster `spec.node_sets` is template-driven rather than picker-backed (see below) and also ignores catalog `field_definitions` for now. Create payloads include only PRD §2.1.1 paths plus catalog item reference; VM hardcodes `spec.image.source_type` = `registry`. -New hooks in `libs/ui-components/src/api/v1/`: instance types, virtual networks, subnets, security groups, cluster catalog items, host types (list), cluster create. VM picker fields depend on fulfillment-service `spec.instance_type` and `spec.is_windows` (PRs #735, #734). Cluster Configuration uses `HostTypes.List` for per-row host type pickers; it does **not** call `ClusterTemplates.Get` for `node_sets`. +New hooks in `libs/ui-components/src/api/v1/`: instance types, virtual networks, subnets, security groups, cluster templates, cluster catalog items, host types (list), cluster create. VM picker fields depend on fulfillment-service `spec.instance_type` and `spec.is_windows` (PRs #735, #734). **Revised 2026-07-27:** Cluster Configuration calls `ClusterTemplates.Get` for the catalog item's template to determine `node_sets` keys and host types (fulfillment-service now rejects node sets that don't match the template — see PRD §1.2, §2.1.6), and uses `HostTypes.List` only to resolve each host type's display name. ### Workflow Description @@ -68,7 +68,7 @@ sequenceDiagram |------|-------|---------| | Catalog Item | Shared | `adapter.useCatalogItems()` | | General | Shared | Name (required), optional SSH key (catalog `ssh_key` overlay); cluster adds required pull secret and optional `ssh_public_key` overlay | -| Configuration | Adapter | VM: image, OS family, instance type, user data, boot disk, run strategy. Cluster: release image, tenant-managed `node_sets` table (add/remove rows) | +| Configuration | Adapter | VM: image, OS family, instance type, user data, boot disk, run strategy. Cluster: release image, template-driven `node_sets` rows (fixed keys/host types from `ClusterTemplate`, size only) | | Networking | Adapter | VM: VN → subnet → SG pickers (single `network_attachments` entry). Cluster: pod/service CIDR | | Review | Shared | `adapter.getReviewSections()` — same labels and values as wizard steps; submit via `buildCreatePayload` | @@ -102,7 +102,7 @@ Non-editable fields without a catalog `default` render blank and read-only (disa **VM Networking specifics:** Load VN list first; on selection, filter subnets and security groups with `this.spec.virtual_network == ""`. Assemble one `network_attachments` element: `{ "subnet": "", "security_groups": [""] }`. Virtual network ID is not sent in the attachment payload. -**Cluster Configuration specifics:** `spec.node_sets` is **tenant-composed** — the wizard does **not** load, display, or apply `ClusterTemplate.spec.node_sets`. On Configuration, render an editable table with **Add node set** / **Remove** actions. Each row: **Host type** (`SelectField` from `HostTypes.List` — [PRD §2.1.6](prd.md#216-cluster-host-type-picker-api)) and **Nodes** (`size` number input, > 0). `ClusterNodeSet` requires only `host_type` and `size` — no separate name column. Validation: at least one row required; host type and positive `size` required per row; **duplicate host types blocked** (each host type id at most once). `buildClusterCreatePayload` uses **host type id as the map key** and sets `host_type` on the value to the same id. Review shows host type label and node count per row. Filter or disable host types already selected on other rows in remaining dropdowns. `ClusterConfigurationStep` loads the host type list on mount; no `useClusterTemplate` call. +**Cluster Configuration specifics (revised 2026-07-27 — was "tenant-composed"; see PRD §1.2 superseded non-goal):** `spec.node_sets` is **template-driven**. fulfillment-service's `PrivateClustersServer.validateNodeSets` (`internal/servers/private_clusters_server.go`) rejects any `node_sets` map key or `host_type` that isn't defined in the associated `ClusterTemplate.spec.node_sets`, so the wizard loads that template (`ClusterTemplates.Get`, keyed off the catalog item's `template` field) and renders **one fixed row per template node-set key** — no add/remove. Each row: node-set key as the row label, **host type** shown as read-only text (resolved to a display name via `HostTypes.List`, falling back to the raw id while loading or if not found), and **Nodes** (`size` number input, > 0). `ClusterNodeSet` requires only `host_type` and `size` — no separate name column, and `host_type` is never user-editable. Validation: every template-defined row requires a positive `size`; no duplicate-host-type check is needed since rows are keyed by the template's own unique keys. `buildClusterCreatePayload` uses the **template's node-set key as the map key** (not the host type id) and copies `host_type` verbatim from the template's entry for that key. Review shows the node-set key, host type label, and node count per row. `ClusterConfigurationStep` loads the template on mount (or reuses it from Catalog/General) and the host type list in parallel for display only. **Cluster General specifics:** `spec.ssh_public_key` and `spec.pull_secret` follow the same General basics overlay rules as VM `spec.ssh_key` (prefill catalog `default`, label, editable, validation). `spec.pull_secret` remains required on the wizard when no catalog rule makes it optional. @@ -112,7 +112,7 @@ Non-editable fields without a catalog `default` render blank and read-only (disa ### API Extensions -No API extensions to create payloads. The wizard consumes existing `ComputeInstanceCatalogItems`, `ClusterCatalogItems`, `InstanceTypes`, networking list APIs (`GET /api/fulfillment/v1/virtual_networks`, `.../subnets`, `.../security_groups`), `HostTypes.List` (`GET /api/fulfillment/v1/host_types`), and create APIs. Server-side catalog validation (`catalog_item_validation.go` / `applyFieldDefinitions`) still applies catalog `field_definitions` on create when the client omits a field the wizard left blank. The wizard does **not** use `ClusterTemplates.Get` for Configuration `node_sets`. +No API extensions to create payloads. The wizard consumes existing `ComputeInstanceCatalogItems`, `ClusterCatalogItems`, `InstanceTypes`, networking list APIs (`GET /api/fulfillment/v1/virtual_networks`, `.../subnets`, `.../security_groups`), `HostTypes.List` (`GET /api/fulfillment/v1/host_types`), `ClusterTemplates.Get` (`GET /api/fulfillment/v1/cluster_templates/{id}` — **new dependency, 2026-07-27**, for Configuration `node_sets`), and create APIs. Server-side catalog validation (`catalog_item_validation.go` / `applyFieldDefinitions`) still applies catalog `field_definitions` on create when the client omits a field the wizard left blank; `PrivateClustersServer.validateNodeSets` separately validates `node_sets` against the template regardless of what the client sends. ### Implementation Details/Notes/Constraints @@ -163,15 +163,15 @@ Each component wraps a PatternFly `FormGroup` (label, `fieldId`, `isRequired`, h **`OsacForm` wrapper:** Every wizard step that renders editable fields wraps its field list in `OsacForm` from `@osac/ui-components` (`libs/ui-components/src/components/Form/OsacForm.tsx`) — not raw PatternFly `Form`. `OsacForm` provides responsive grid layout and blocks native submit; wizard navigation stays on PatternFly Wizard footer buttons. ESLint already requires `OsacForm` over direct `Form` imports in osac-ui. -**i18n:** All user-visible wizard copy uses i18next via `useTranslation` from `@osac/ui-components/hooks/useTranslation` (never import from `react-i18next` directly). Use hardcoded string keys in `t('...')` so `pnpm i18n` can extract keys into `libs/i18n/locales/en/translation.json` (committed with source changes; CI fails if out of sync). Apply to step titles, intros, field labels (wizard defaults), buttons, validation alert text, node-sets add/remove actions, and Review section headings. Catalog `display_name` from `field_definitions` overrides the wizard default label when present and is shown as-is (server-provided, not passed through `t()`). Pure helpers (e.g. `getReviewSections`, static field descriptors) accept `t: TFunction` from the calling component rather than calling `useTranslation` internally. +**i18n:** All user-visible wizard copy uses i18next via `useTranslation` from `@osac/ui-components/hooks/useTranslation` (never import from `react-i18next` directly). Use hardcoded string keys in `t('...')` so `pnpm i18n` can extract keys into `libs/i18n/locales/en/translation.json` (committed with source changes; CI fails if out of sync). Apply to step titles, intros, field labels (wizard defaults), buttons, validation alert text, node-set row labels, and Review section headings. Catalog `display_name` from `field_definitions` overrides the wizard default label when present and is shown as-is (server-provided, not passed through `t()`). Pure helpers (e.g. `getReviewSections`, static field descriptors) accept `t: TFunction` from the calling component rather than calling `useTranslation` internally. Adapter steps use Formik context, own API hooks and loading UI, and export Yup fragments. Shared helpers: `buildWizardSchema` (compose adapter fragments + overlay merge for non-picker Configuration/Networking paths and General basics), `applyCatalogOverlay`, `validateStepFields` (subset validation for the current step). Paths use PRD `spec.*` notation; wire builders output camelCase OpenAPI shapes. **Formik/Yup:** Single `` in the orchestrator with one wizard-level Yup schema from `adapter.getWizardSchema(fieldDefinitions)` — not per-step schemas. A single schema lets future cross-step rules reference values from any step (e.g. Networking validation depending on Configuration choices) without re-plumbing. Validate-on-Next runs Yup against only the current step's field paths via `adapter.getStepFieldPaths(stepId)` while the full schema retains access to all `values`. Each step body: `OsacForm` → shared `InputField` / `SelectField` / `RadioButtonField` from `@osac/ui-components` bound to Formik state — no raw PatternFly `Form` and no duplicated error wiring. Overlay merge applies to General basics and non-picker Configuration and Networking fields. `editable: false` passes `isDisabled` to field components; catalog `default` is applied to Formik on catalog selection when present; merge `validation_schema` into Yup for the supported JSON Schema subset. Validate-on-Next uses the same Formik `errors` / `touched` state those components display. Yup validation messages that surface to the user should use i18n keys where the schema supports message overrides. -**Catalog item change:** Do not use `enableReinitialize` — it would reset user edits whenever `initialValues` changes. Instead, `onCatalogItemSelected` explicitly calls `resetForm({ values: getInitialValues(item) })` and applies catalog overlay defaults so reinitialization happens only on intentional catalog selection, not on unrelated parent re-renders. Cluster catalog selection does **not** fetch `ClusterTemplates.Get` or seed `spec.node_sets` from the template. +**Catalog item change:** Do not use `enableReinitialize` — it would reset user edits whenever `initialValues` changes. Instead, `onCatalogItemSelected` explicitly calls `resetForm({ values: getInitialValues(item) })` and applies catalog overlay defaults so reinitialization happens only on intentional catalog selection, not on unrelated parent re-renders. **Revised 2026-07-27:** cluster catalog selection **does** fetch `ClusterTemplates.Get` for the catalog item's `template` — required to know the `node_sets` keys and host types to render (see [§Cluster Configuration specifics](#proposal)) — but still does not seed `spec.node_sets` *sizes* from the template; only the row shape (keys/host types) comes from it. -**PRD §5 decisions (v1):** Ignore catalog `field_definitions` on picker-backed paths (`spec.instance_type`, `spec.network_attachments`, `spec.node_sets` host type picker). No wizard UI for `spec.additional_disks` — boot disk only. Cluster `node_sets` are tenant-composed (add/remove rows); template `node_sets` are ignored. PRD `?` fields are **optional**: `spec.boot_disk.size_gib`, `spec.network.pod_cidr`, and `spec.network.service_cidr` — omit from payload when blank. `spec.ssh_key` / `spec.ssh_public_key` are optional basics fields — prefill catalog `default` when defined; omit from client payload only when blank. +**PRD §5 decisions (v1):** Ignore catalog `field_definitions` on picker-backed paths (`spec.instance_type`, `spec.network_attachments`). No wizard UI for `spec.additional_disks` — boot disk only. **Revised 2026-07-27:** Cluster `node_sets` are **template-driven** (fixed rows from `ClusterTemplate.spec.node_sets`, size only) — see the superseded "tenant-composed add/remove" resolution in PRD §5. PRD `?` fields are **optional**: `spec.boot_disk.size_gib`, `spec.network.pod_cidr`, and `spec.network.service_cidr` — omit from payload when blank. `spec.ssh_key` / `spec.ssh_public_key` are optional basics fields — prefill catalog `default` when defined; omit from client payload only when blank. **Removed:** `partitionFieldDefinitions`, generic `ConfigurationStep`/`CatalogFieldInput`, `canProceedWizardStep`, text-based networking rows, catalog-driven field discovery. Replaced by static field tables, `OsacForm`, and Formik-connected `InputField` / `SelectField` / `RadioButtonField` components. @@ -202,7 +202,8 @@ No server writes until create succeeds. | PatternFly modal / picker behavior in jsdom | Shared `wizardFlow.helpers.ts`; test-setup mocks (`matchMedia`, `ResizeObserver`); manual smoke for visual regressions | | fulfillment-service version skew (`instance_type`, `is_windows`) | Coordinate osac-installer image pins; document in Version Skew Strategy | | Catalog overlay edge cases on read-only fields without defaults | PRD defines blank read-only UX; test with catalog items that lock fields without defaults | -| Cluster provision with no node sets defined | Configuration validation requires at least one row before Next; surface inline errors on the table | +| Cluster template with no `node_sets` defined | Configuration shows an empty-state message instead of a table; wizard cannot proceed to create a cluster with zero node sets — flag as a template data-quality issue | +| Cluster template lookup fails or template deleted after catalog item creation | Configuration shows a load error instead of node-set rows; block Next until resolved | ## Test Plan @@ -259,15 +260,15 @@ apps/app-frontend/src/pages/ | Invalid CIDR format on cluster Networking (value present) | Format error on offending field; no advance | | Invalid catalog `validation_schema` on overlay field | Merged Yup rule fires on Next | | Valid step after errors | Fix values → Next advances; errors clear on corrected fields | -| Empty cluster `node_sets` (no rows) | Configuration blocks Next; inline error on table (at least one node set required) | -| Duplicate host type on cluster Configuration | Inline error; host type excluded from other row pickers; no advance until resolved | +| Cluster template node set left at zero/blank size | Configuration blocks Next; inline error on the offending row | +| Selected template has no `node_sets` | Configuration shows an empty-state message; wizard cannot proceed | #### Back navigation and form state | Scenario | Assert | |----------|--------| | General → Configuration → Back | Name, SSH key, pull secret (cluster) unchanged in inputs | -| Configuration → Networking → Back | Release image, node set host types and sizes preserved | +| Configuration → Networking → Back | Release image and node set sizes preserved (host types are read-only, derived from the template) | | Networking → Review → Back | Picker selections and CIDR values preserved | | Review → Back through all steps | Every field still matches values entered earlier | | Change catalog item after editing | `onCatalogItemSelected` resets to `getInitialValues`; prior edits discarded | @@ -288,7 +289,7 @@ apps/app-frontend/src/pages/ | Scenario | Assert | |----------|--------| | Happy path VM | Select catalog item → fill required fields on each step → Review shows same labels/values as steps | -| Happy path cluster | Tenant adds one or more node set rows; selects host type from dropdown and node count; Review lists host type and size per row | +| Happy path cluster | Select catalog item → template's node-set rows render automatically (host type read-only) → tenant enters node count per row; Review lists host type and size per row | | Optional basics / config fields left blank | Review shows empty/omitted state; client payload omits those keys (assert via mocked create handler) | | Catalog ssh_key default on select | General SSH field prefilled with parsed catalog default; create payload includes plain-string `ssh_key` unless tenant clears the field | | Single-option picker lists | Instance type / VN / subnet / SG auto-selected; value visible on Review after Back | @@ -307,7 +308,7 @@ apps/app-frontend/src/pages/ - **VM Configuration:** OS family radio toggles `spec.is_windows`; obsolete instance types excluded from picker options. - **VM Networking:** Subnet/SG lists filter after VN selection; changing VN clears dependent picks unless auto-select applies. -- **Cluster Configuration:** Tenant can add/remove node set rows; host type dropdown from `HostTypes.List`; `host_type` and `size` > 0 validated per row; at least one row required; duplicate host types blocked; payload map key = host type id. +- **Cluster Configuration:** Node-set rows match the selected template's `node_sets` keys exactly (no add/remove control rendered); host type shown as read-only text; `size` > 0 validated per row; every template row required; payload map key = template's node-set key (not host type id). - **Cluster Networking:** Optional CIDR fields — empty allowed; invalid format blocked on Next only when non-empty. Component tests are required for merge; add cases when fixing wizard regressions. @@ -319,4 +320,4 @@ Component tests are required for merge; add cases when fixing wizard regressions ### Manual smoke -End-to-end VM and cluster provision via `/vms/create` and `/clusters/create`; cluster wizard with manually added node sets and host type dropdown; submit with optional fields left blank; verify Details page after successful create. +End-to-end VM and cluster provision via `/vms/create` and `/clusters/create`; cluster wizard with template-driven node sets (enter sizes only, host types pre-populated and read-only); submit with optional fields left blank; verify Details page after successful create. diff --git a/enhancements/OSAC-1421-cluster-and-vm-provisioning-wizard/prd.md b/enhancements/OSAC-1421-cluster-and-vm-provisioning-wizard/prd.md index 4fd65d92f..3d9ef6691 100644 --- a/enhancements/OSAC-1421-cluster-and-vm-provisioning-wizard/prd.md +++ b/enhancements/OSAC-1421-cluster-and-vm-provisioning-wizard/prd.md @@ -3,7 +3,7 @@ title: Configuration Wizard for Cluster and VM Resources authors: - brotman@redhat.com creation-date: 2026-06-14 -last-updated: 2026-07-09 +last-updated: 2026-07-27 tracking-link: - https://redhat.atlassian.net/browse/OSAC-1421 see-also: @@ -30,9 +30,10 @@ superseded-by: - **BareMetalInstance** provisioning (separate PRD) - **Template parameters** - **Multi-NIC** — wizard submits one `network_attachments` entry (one VN, one subnet, security groups); no add/remove NIC rows -- **Cluster template `node_sets` defaults** — the wizard does **not** load, display, or apply `ClusterTemplate.spec.node_sets` (`host_type` or `size` defaults) - **`spec.additional_disks`** — wizard scope undecided ([§5](#5-open-decisions)); default: boot disk only +> **Superseded (2026-07-27):** an earlier revision of this PRD listed "load/apply `ClusterTemplate.spec.node_sets`" as a non-goal, on the premise that node sets were fully tenant-composed. fulfillment-service's cluster create validation (`PrivateClustersServer.validateNodeSets`) now rejects any `node_sets` entry whose map key or host type isn't defined in the associated `ClusterTemplate.spec.node_sets` — so loading and applying the template's node sets is now a **hard requirement**, not a non-goal. See [§2.1.1](#211-static-wizard-fields), [§2.1.6](#216-cluster-host-type-picker-api), and [§5](#5-open-decisions). + ## 2. Requirements ### 2.1 Field model @@ -75,13 +76,13 @@ Fields are hardcoded per resource type, not discovered from `field_definitions`. | General | `spec.ssh_public_key` | SSH public key | Text (multiline) | Optional | | General | `spec.pull_secret` | Pull secret | Text (multiline, masked) | Required | | Configuration | `spec.release_image` | OpenShift version (release image) | Text | Required | -| Configuration | `spec.node_sets` | Worker node sets | Editable table (add/remove rows) | Required | +| Configuration | `spec.node_sets` | Worker node sets | Fixed rows from `ClusterTemplate.spec.node_sets` (size editable only) | Required | | Networking | `spec.network.pod_cidr` | Pod network CIDR | Text | ? | | Networking | `spec.network.service_cidr` | Service network CIDR | Text | ? | **Notes:** -- **`spec.node_sets`**: tenant-managed node sets on the Configuration step. The wizard **does not** read `ClusterTemplate.spec.node_sets`. Tenants **add** and **remove** rows. Each row collects only **`host_type`** (picker — [§2.1.6](#216-cluster-host-type-picker-api)) and **`size`** (number of nodes, must be > 0) per `ClusterNodeSet` — no separate name or map-key field in the UI. At least one row is required before leaving Configuration. **Each `host_type` may appear on at most one row** — duplicate host types are blocked by validation. The create payload is `spec.node_sets` as a map keyed by **host type id** (the map key equals `host_type` on each entry); each value is `{ host_type, size }` only. **v1:** catalog item `field_definitions` defaults for `spec.node_sets` (including `host_type` and `size`) **do not apply** — the node-sets table starts empty on catalog selection; tenants compose all rows manually ([§2.1.2](#212-catalog-overlay-and-defaults)). +- **`spec.node_sets`** (**revised 2026-07-27** — see superseded non-goal above): **template-driven**, not tenant-composed. The wizard loads the selected catalog item's `ClusterTemplate` ([§2.1.6](#216-cluster-host-type-picker-api)) and renders exactly one row per key in `ClusterTemplate.spec.node_sets` — no add/remove. Each row shows the node-set key (e.g. `workers`) and its **host type** (read-only, inherited from the template) and collects only **`size`** (number of nodes, must be > 0) per `ClusterNodeSet`. All rows require a positive size before leaving Configuration. The create payload is `spec.node_sets` as a map using the **same keys as the template** (not the host type id); each value is `{ host_type, size }`, with `host_type` copied verbatim from the template's entry for that key. fulfillment-service (`PrivateClustersServer.validateNodeSets`) rejects any key or host type that doesn't match the template, so the wizard cannot construct an invalid payload by construction. **v1:** catalog item `field_definitions` defaults for `spec.node_sets` still do not apply — sizes are not prefilled from the catalog item, only from the template's own rows ([§2.1.2](#212-catalog-overlay-and-defaults)). **Create payload:** Only paths in [§2.1.1](#211-static-wizard-fields) plus catalog item reference; VM hardcodes `spec.image.source_type` = `registry`; VM sends `spec.instance_type` and `spec.is_windows` explicitly, not `spec.cores` or `spec.memory_gib`. @@ -89,7 +90,7 @@ Fields are hardcoded per resource type, not discovered from `field_definitions`. For each static **non-picker** field, match `field_definitions` by `path` (spec-relative paths such as `ssh_key`, `boot_disk.size_gib`, or `spec.image.source_ref` — fulfillment accepts both forms). **General basics** paths (`spec.ssh_key`, `spec.ssh_public_key`, `spec.pull_secret`) and **Configuration** / **Networking** non-picker paths participate in overlay. Non-matching paths are **ignored** (not on Review, not in payload). -**Picker-backed fields (v1):** `spec.instance_type`, `spec.network_attachments` (including nested paths such as `spec.network_attachments.subnet`), and cluster `spec.node_sets` **host type** (per-row dropdown) load options from list APIs ([§2.1.5](#215-vm-instance-type-picker-api), [§2.1.4](#214-vm-networking-picker-apis), [§2.1.6](#216-cluster-host-type-picker-api)). Matching catalog `field_definitions` for these paths are **ignored** — wizard labels, editability, validation, and **defaults** come from wizard defaults and list-API behavior only. **Cluster `spec.node_sets` (v1):** no catalog item defaults apply — the wizard does not prefill node set rows from `field_definitions` on catalog selection; the table starts empty. Catalog overlay on picker fields is **deferred** to a later release ([§5](#5-open-decisions)). +**Picker-backed fields (v1):** `spec.instance_type` and `spec.network_attachments` (including nested paths such as `spec.network_attachments.subnet`) load options from list APIs ([§2.1.5](#215-vm-instance-type-picker-api), [§2.1.4](#214-vm-networking-picker-apis)). Matching catalog `field_definitions` for these paths are **ignored** — wizard labels, editability, validation, and **defaults** come from wizard defaults and list-API behavior only. **Cluster `spec.node_sets` (revised 2026-07-27):** no longer picker-backed in the free-choice sense — rows and host types are fixed by the selected `ClusterTemplate.spec.node_sets` ([§2.1.6](#216-cluster-host-type-picker-api)); catalog item `field_definitions` still do not prefill sizes. Catalog overlay on `spec.instance_type` / `spec.network_attachments` is **deferred** to a later release ([§5](#5-open-decisions)). | Aspect | Matching entry (non-picker fields, including General basics) | No matching entry | | ---------- | --------------------------------------------------------------------------- | --------------------- | @@ -185,42 +186,39 @@ Do **not** send `cores` or `memory_gib` — they are mutually exclusive with `in **Load order:** load instance type list when entering Configuration → auto-select when the list returns exactly one item ([§2.1.2](#212-catalog-overlay-and-defaults)). -### 2.1.6 Cluster host type picker API +### 2.1.6 Cluster node sets: template source and host type display -The Configuration step loads host type options from the **public** fulfillment API (`osac.public.v1`). The UI uses the generated OpenAPI client (REST); gRPC equivalent listed for reference. +**Revised 2026-07-27** (was "Cluster host type picker API" — the host type is no longer picked freely; see superseded non-goal in [§1.2](#12-non-goals)). -| Picker | gRPC | REST | Purpose | -| ------ | ---- | ---- | ------- | -| Host type | `HostTypes.List` | `GET /api/fulfillment/v1/host_types` | Tenant-visible host types for node set selection | +The Configuration step's node-set rows come from the `ClusterTemplate` referenced by the selected catalog item's `template` field, **not** from a free host-type picker. The UI uses the generated OpenAPI client (REST); gRPC equivalent listed for reference. -**List request parameters:** optional query `filter` (CEL), `limit`, `offset`, `order`. Tenant scope is implicit from the authenticated session. +| Call | gRPC | REST | Purpose | +| ---- | ---- | ---- | ------- | +| Template lookup | `ClusterTemplates.Get` | `GET /api/fulfillment/v1/cluster_templates/{id}` | Authoritative source of node-set keys and host types (`ClusterTemplate.spec.node_sets`) | +| Host type | `HostTypes.List` | `GET /api/fulfillment/v1/host_types` | Display name only — resolves each template node set's `host_type` id to a friendly label; **not** used for selection | -**Picker display and values:** +**Row rendering:** one row per key in `ClusterTemplate.spec.node_sets` (no add/remove). Each row shows the node-set key and the host type's display name (`title` or `metadata.name`, fallback `id`) as **read-only** text, and collects only **`size`** (number input, must be > 0). -| Picker | Option label | Selected value | -| ------ | ------------ | -------------- | -| Host type | `title` or `metadata.name` (fallback `id`) | Host type `id` — used as both the row selection and the `spec.node_sets` **map key**; `host_type` on the entry value matches the key | - -**Create payload** — one map entry per wizard row; **map key = host type id** (same as `host_type` on the value): +**Create payload** — one map entry per template key, using the **template's own keys** (not the host type id) and copying `host_type` verbatim from the template's entry for that key: ```json { "node_sets": { - "acme_1tb": { + "workers": { "host_type": "acme_1tb", "size": 3 }, - "acme_1tb_h100": { + "masters": { "host_type": "acme_1tb_h100", - "size": 2 + "size": 3 } } } ``` -Per `ClusterNodeSet` in `cluster_type.proto` — each value has **`host_type`** and **`size`** only. The wizard enforces **unique host types** across rows (no duplicate keys). **v1:** catalog item `field_definitions` defaults for node sets do not apply — no prefill from the selected `ClusterCatalogItem`. Catalog `field_definitions` for `spec.node_sets` paths are **ignored** in v1 ([§2.1.2](#212-catalog-overlay-and-defaults)) — node set composition is API-driven via the host type list, not template- or catalog-default-driven. +Per `ClusterNodeSet` in `cluster_type.proto` — each value has **`host_type`** and **`size`** only. fulfillment-service's `PrivateClustersServer.validateNodeSets` (`internal/servers/private_clusters_server.go`) rejects any `node_sets` map key that isn't a key in the template, and any `host_type` that doesn't match the template's value for that key — so the wizard cannot construct a request the server would reject. **v1:** catalog item `field_definitions` defaults for node sets still do not apply — no prefill from the selected `ClusterCatalogItem`, only from the template's own rows. -**Load order:** load host type list when entering Configuration (or when the node-sets table mounts). No auto-select from `ClusterTemplate`; tenants choose host type per row from the dropdown. Host types already selected on another row are excluded from (or blocked in) remaining row pickers. +**Load order:** load the template (`ClusterTemplates.Get`) when entering Configuration (or reuse it if already fetched for catalog/General display); load the host type list in parallel for display-name resolution only. ### 2.2 Wizard behavior @@ -243,14 +241,15 @@ flowchart LR - Review shows the same values as on wizard step fields (blank, default-driven, or user-entered). - Catalog overlay and default rules per [§2.1.2](#212-catalog-overlay-and-defaults) on Configuration and Networking **non-picker** fields and General **basics** fields; picker-backed paths ignore `field_definitions` in v1; catalog `default` prefills matching wizard fields on catalog selection; non-editable fields without `default` appear blank and read-only; non-editable fields with `default` appear read-only with value and are included in the client payload. - VM: single `network_attachments` entry assembled from picker APIs; instance type picker sets `spec.instance_type` (not `cores`/`memory_gib`); OS family radio sets `spec.is_windows` (default **Linux**); optional `user_data` omitted when empty; create warnings for deprecated instance types are shown to the user. -- Cluster: `node_sets` is tenant-composed on Configuration — add/remove rows; each row has `host_type` from `HostTypes.List` and `size` > 0 only (`ClusterNodeSet`); **unique host type per row**; map key = host type id; wizard does not load or apply `ClusterTemplate.spec.node_sets`; **catalog item defaults for `spec.node_sets` do not apply in v1** (empty table on catalog selection). +- Cluster: `node_sets` is **template-driven** on Configuration — one fixed row per key in `ClusterTemplate.spec.node_sets` (no add/remove); each row's `host_type` is read-only and inherited from the template; only `size` > 0 is tenant-entered (`ClusterNodeSet`); payload map key = template's node-set key (not host type id); **catalog item defaults for `spec.node_sets` do not apply in v1** (sizes come from tenant entry only, not prefilled). - All **?** requiredness decisions resolved before release ([§5](#5-open-decisions)). - On Next click, validate all fields on the current step (including untouched fields); surface hidden inline errors; show an alert if invalid; do not advance until the step is valid. ## 4. Dependencies - `ComputeInstanceCatalogItem`, `ClusterCatalogItem` (with `field_definitions`) -- `HostTypes.List` (cluster Configuration step — host type picker per node set row) +- `ClusterTemplates.Get` (cluster Configuration step — authoritative source of `node_sets` keys and host types; **new dependency, 2026-07-27**) +- `HostTypes.List` (cluster Configuration step — display-name resolution for template node-set host types only, not selection) - `VirtualNetworks.List`, `Subnets.List`, `SecurityGroups.List` (gRPC `osac.public.v1`) / REST `GET /api/fulfillment/v1/virtual_networks`, `.../subnets`, `.../security_groups` ([§2.1.4](#214-vm-networking-picker-apis)) - `InstanceTypes.List` (gRPC `osac.public.v1`) / REST `GET /api/fulfillment/v1/instance_types` ([§2.1.5](#215-vm-instance-type-picker-api)) - ComputeInstance and Cluster create APIs @@ -277,7 +276,14 @@ Resolve before implementation. ### Cluster `node_sets` composition -**Resolved:** Tenant-managed node sets on Configuration. The wizard ignores `ClusterTemplate.spec.node_sets` entirely. Tenants add/remove rows; each row collects `host_type` (dropdown from `HostTypes.List`) and `size` only. Map key = host type id; duplicate host types are not allowed. **v1:** catalog item `field_definitions` defaults for `spec.node_sets` do not apply — node set rows are not prefilled from the selected catalog item. See [§2.1.1](#211-static-wizard-fields) and [§2.1.6](#216-cluster-host-type-picker-api). +**Re-resolved 2026-07-27** (supersedes the prior resolution below): **template-driven** node sets on Configuration. fulfillment-service's `PrivateClustersServer.validateNodeSets` rejects any `node_sets` map key or host type that isn't defined in the associated `ClusterTemplate.spec.node_sets`, so the wizard now loads the template and renders one fixed row per template node-set key; only `size` is tenant-entered. No add/remove; no free host-type dropdown. Map key = template's node-set key (not host type id). **v1:** catalog item `field_definitions` defaults for `spec.node_sets` still do not apply — sizes are not prefilled from the selected catalog item. Confirmed against the CSP Admin catalog item wizard's equivalent fix ([OSAC-2936](https://redhat.atlassian.net/browse/OSAC-2936), osac-project/osac-ui#102). See [§2.1.1](#211-static-wizard-fields) and [§2.1.6](#216-cluster-node-sets-template-source-and-host-type-display). + +
+Prior resolution (2026-07-09, now superseded) + +Tenant-managed node sets on Configuration. The wizard ignores `ClusterTemplate.spec.node_sets` entirely. Tenants add/remove rows; each row collects `host_type` (dropdown from `HostTypes.List`) and `size` only. Map key = host type id; duplicate host types are not allowed. **v1:** catalog item `field_definitions` defaults for `spec.node_sets` do not apply — node set rows are not prefilled from the selected catalog item. + +
### Additional disks