You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: enhancements/hypershift/dual-stream-rhel-nodepool.md
+31-24Lines changed: 31 additions & 24 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -29,11 +29,10 @@ This enhancement adds a `spec.osImageStream` field to the NodePool API, threads
29
29
30
30
-**OS stream** — A RHEL major version variant of the node OS image. Current streams are `rhel-9` and `rhel-10`. Each stream has its own `rhel-coreos*` container image and boot disk images (AMIs, VHDs).
31
31
-**OSImageStream** — A TechPreview CRD (`machineconfiguration.openshift.io/v1alpha1`) introduced by the MCO. The singleton `cluster` resource declares `spec.defaultStream` and reports `status.availableStreams` discovered from OCI labels on release images.
32
-
-**MCO bootstrap pipeline** — Three one-shot binaries (`machine-config-operator`, `machine-config-controller`, `machine-config-server`) run sequentially inside the ignition server pod to generate ignition payloads. The ignition server orchestrates them in `GetPayload()` via four steps:
33
-
1.`runMCO()` — executes `machine-config-operator bootstrap`. Reads `--image-references` from the release payload and produces raw manifests (ControllerConfig, MachineConfigPools, MachineConfigs) in an output directory.
34
-
2.`copyMCOOutputToMCC()` — copies MCO output manifests plus CPO-generated pool overrides (`*.machineconfigpool.yaml`) into the MCC input directory (`mccDir`). This is the directory the MCC reads via `--manifest-dir`.
35
-
3.`runMCC()` — executes `machine-config-controller bootstrap`. Reads all manifests from `mccDir`, including ControllerConfig, FeatureGate, MachineConfigPools, and (with this enhancement) OSImageStream. When the `OSStreams` feature gate is active, the MCC calls `fetchOSImageStream()` to inspect OCI labels on release images, discover available streams, select the stream from `OSImageStream.spec.defaultStream`, and override `ControllerConfig.baseOSContainerImage` with the selected stream's image. It then renders final MachineConfigs with the correct `osImageURL`.
36
-
4.`runMCSAndFetchPayload()` — executes `machine-config-server` which reads the rendered MachineConfigs and produces the ignition JSON payload.
32
+
-**MCO bootstrap pipeline** — Three one-shot binaries (`machine-config-operator`, `machine-config-controller`, `machine-config-server`) run sequentially inside the ignition server pod to generate ignition payloads. The ignition server orchestrates them in `GetPayload()` via three steps:
33
+
1.`runMCO()` — executes `machine-config-operator bootstrap`. Reads `--image-references` from the release payload and produces raw manifests (ControllerConfig, MachineConfigPools, MachineConfigs) in an output directory. Internally calls `copyMCOOutputToMCC()` to copy MCO output manifests plus CPO-generated pool overrides (`*.machineconfigpool.yaml`) into the MCC input directory (`mccDir`).
34
+
2.`runMCC()` — executes `machine-config-controller bootstrap`. Reads all manifests from `mccDir`, including ControllerConfig, FeatureGate, MachineConfigPools, and (with this enhancement) OSImageStream. When the `OSStreams` feature gate is active, the MCC calls `fetchOSImageStream()` to inspect OCI labels on release images, discover available streams, select the stream from `OSImageStream.spec.defaultStream`, and override `ControllerConfig.baseOSContainerImage` with the selected stream's image. It then renders final MachineConfigs with the correct `osImageURL`.
35
+
3.`runMCSAndFetchPayload()` — executes `machine-config-server` which reads the rendered MachineConfigs and produces the ignition JSON payload.
37
36
A preparatory step, `runFeatureGateRender()`, runs before `runMCO()` to write the FeatureGate manifest into `mccDir`.
38
37
-**Token secret** — A per-NodePool Secret in the control plane namespace containing the ignition token, release image, config hash, and other data needed by the ignition server to generate a payload.
39
38
-**Boot image** — The platform-specific disk image (AMI, VHD, qcow2) used to launch a new node. In the layered model, this is a base CoreOS image (kernel, systemd, ignition) without OCP packages. The node rebases to the full node image on first boot.
@@ -264,13 +263,15 @@ The `spec.osImageStream` field is additive and optional. OKE clusters using Hype
264
263
-**Azure**: resolve the stream-specific VHD image URL.
265
264
-**GCP**: resolve the stream-specific GCE image.
266
265
-**KubeVirt/OpenStack/Agent**: resolve the stream-specific disk image or container image as applicable.
267
-
All platforms fall back to the legacy `stream` key for payloads that don't carry the multi-stream `streams` key.
266
+
All platforms fall back to the legacy `stream` key for payloads that don't carry the multi-stream `streams` key. If the `streams` key is present but the requested stream has no boot image for the target platform or region, the resolution function returns an error surfaced through `setPlatformConditions` as `NodePoolValidPlatformImageType=False` — consistent with how missing boot images are already handled today.
267
+
268
+
4.**Karpenter** — Karpenter uses the same `defaultNodePoolAMI` function as standard NodePools for boot image resolution. Since Karpenter NodePools have no `spec.osImageStream` field, the version-derived default stream is always passed (rhel-9 for < 5.0, rhel-10 for >= 5.0). The in-memory NodePool created by `KarpenterIgnitionReconciler.createInMemoryNodePool()` carries no `osImageStream`, so the ignition payload uses the default stream. The AMI label scheme (`hypershift.openshift.io/ami`) currently assumes one AMI per architecture; extending it with per-stream labels is out of scope for this enhancement and would be addressed if Karpenter gains explicit stream selection in a future phase. No changes to `OpenshiftEC2NodeClass` are planned.
268
269
269
270
**Phase 2: OS Stream Plumbing into Payload Generation**
270
271
271
272
Thread the OS stream selection from the NodePool controller through the token secret to the ignition server, where it drives OSImageStream CR generation for the MCC bootstrap pipeline. No new NodePool API fields in this phase — the API was added in Phase 0.
272
273
273
-
The following diagram shows how the OS stream propagates through the ignition server's bootstrap pipeline. The annotated pipeline shows the four existing stages plus the new injection point:
274
+
The following diagram shows how the OS stream propagates through the ignition server's bootstrap pipeline. The annotated pipeline shows the three existing stages plus the new injection point:
- Existing NodePools with no explicit stream produce the same hash as before the feature is introduced, avoiding accidental rollouts on upgrade.
377
377
- Upgrading to >= 5.0 (where the implicit default shifts to `rhel-10`) does not trigger a rollout from the stream alone — the rollout is already driven by the release version component (`releaseImage.Version()`).
378
378
379
+
**Design invariant**: the `rhelStream` field in `rolloutConfig` MUST be populated directly from `spec.osImageStream.name` (empty string when unset), never from the resolved return value of `getRHELStream()`. Using the resolved value would inject a non-empty default (e.g., `"rhel-10"` for >= 5.0) into the hash for every NodePool without an explicit field, triggering a fleet-wide mass rollout on upgrade.
380
+
379
381
7.**Pass stream through `IgnitionProvider` interface** — add `osStream string` parameter to `GetPayload()` in the `IgnitionProvider` interface (`tokensecret_controller.go`). The `TokenSecretReconciler` reads `os-stream` from the token secret data and passes it to `GetPayload()`. Update the mock `IgnitionProvider` in tests accordingly.
380
382
381
383
```go
@@ -398,7 +400,7 @@ Implementation steps:
398
400
399
401
10. **Report `status.osImageStream`** from observed node state. The NodePool controller reads `node.Status.NodeInfo.OSImage` (which contains the RHEL version) and sets `status.osImageStream.name` accordingly.
400
402
401
-
11. **E2E tests** — run on TechPreview CI job. Test dual-stream NodePools (RHEL 9 + RHEL 10 in same HostedCluster), stream validation rejection on < 5.0 payloads, default stream resolution, runc fallback.
403
+
11. **E2E tests** — add a new test case to the existing `TestNodePool` suite that creates 8 additional NodePools to validate all stream scenarios in parallel. Runs in `e2e-test-preview` until GA. See Test Plan for details.
402
404
403
405
**Phase 3: GA API**
404
406
@@ -526,6 +528,8 @@ The `rhel-coreos*` images are NOT runtime containers — they are ostree commits
526
528
527
529
4. **Mixed-stream clusters.** A HostedCluster with RHEL 9 and RHEL 10 NodePools is a new topology. Component compatibility across RHEL versions must be validated. *Mitigation:* This is the same topology supported by standalone clusters via per-MachineConfigPool stream selection.
528
530
531
+
5. **Disconnected environments require mirroring both streams.** Dual-stream payloads carry two sets of node OS container images (one per RHEL stream). Disconnected customers using both streams must mirror both sets. Boot images are platform-specific and already handled outside the payload (e.g. pre-uploaded AMIs, VHDs). The OS container images are referenced via `ImageDigestMirrorSet` / `ImageTagMirrorSet` — existing IDMS/ITMS mirroring workflows handle this transparently as long as both stream images are included in the mirror list. No additional HyperShift-specific mirroring tooling is needed.
532
+
529
533
### Drawbacks
530
534
531
535
The primary drawback is additional complexity in the ignition pipeline. The token secret gains a new field, the `IgnitionProvider` interface changes, and the `GetPayload` function must generate an additional manifest. However, this complexity mirrors what the installer already does for standalone clusters — it is not HyperShift-specific logic but rather bringing HyperShift into parity with the standalone bootstrap flow.
@@ -558,12 +562,17 @@ This was rejected because:
558
562
- `GetPayload` generates `99_osimagestream.yaml` with correct `spec.defaultStream`.
559
563
- Config hash changes when an explicit `spec.osImageStream.name` is set, but remains unchanged when stream is implicit (empty `rhelStream`).
560
564
561
-
- **E2E tests** (TechPreview CI job):
562
-
- Create a HostedCluster with two NodePools: one `rhel-9`, one `rhel-10`. Verify both pools have nodes running the correct RHEL version via `node.Status.NodeInfo.OSImage`.
563
-
- Create a NodePool with `osImageStream.name: "rhel-10"` on a < 5.0 release. Verify `NodePoolValidMachineConfigConditionType=False` and no machines created.
564
-
- Upgrade a HostedCluster to 5.0 with no explicit `osImageStream`. Verify nodes move to RHEL 10 by default.
565
-
- Create a NodePool with runc MachineConfig and `osImageStream.name: "rhel-10"`. Verify `NodePoolValidMachineConfigConditionType=False`.
566
-
- Create a NodePool with runc MachineConfig and no explicit `osImageStream` on 5.0. Verify it stays on RHEL 9.
565
+
- **E2E tests**: Add a new test case to the existing `TestNodePool` suite that creates 8 additional NodePools to validate all stream scenarios in parallel:
566
+
1. **Explicit rhel-9**: NodePool with `osImageStream.name: "rhel-9"`. Verify nodes report RHEL 9 via `node.Status.NodeInfo.OSImage`.
3. **Implicit default**: NodePool with no `osImageStream`. Verify nodes run the release version's default (RHEL 10 for >= 5.0).
569
+
4. **Validation rejection**: NodePool with `osImageStream.name: "rhel-10"` on a < 5.0 release. Verify `NodePoolValidMachineConfigConditionType=False` and no machines created.
570
+
5. **Runc rejection**: NodePool with runc `ContainerRuntimeConfig` and `osImageStream.name: "rhel-10"`. Verify `NodePoolValidMachineConfigConditionType=False`.
571
+
6. **Runc fallback**: NodePool with runc `ContainerRuntimeConfig` and no explicit `osImageStream` on >= 5.0. Verify it stays on RHEL 9 with informational condition message.
572
+
7. **Upgrade implicit stream switch (Replace)**: NodePool with `upgradeType: Replace` on a < 5.0 release (implicitly rhel-9). Upgrade the NodePool to a 5.0+ release. Verify nodes are replaced and report RHEL 10 as the new implicit default.
573
+
8. **Upgrade implicit stream switch (InPlace)**: NodePool with `upgradeType: InPlace` on a < 5.0 release (implicitly rhel-9). Upgrade the NodePool to a 5.0+ release. Verify nodes rebase to RHEL 10 in place.
574
+
575
+
HyperShift will run this test case only in the `e2e-test-preview` test suite until the NodePool API fields GA. Additionally the test will adjust the TestNodePool HostedCluster's TechPreview feature set as needed to pick up the MCO's `OSStreams` feature gate.
567
576
568
577
## Graduation Criteria
569
578
@@ -597,9 +606,7 @@ N/A. This is a new feature.
597
606
- No manual action required.
598
607
599
608
**Downgrade to a version without this feature:**
600
-
- `spec.osImageStream` is ignored by older controllers. NodePools revert to the single-stream behavior (always using the `rhel-coreos` image from the release payload).
601
-
- `status.osImageStream` stops being reported.
602
-
- No data loss or disruption — nodes continue running whatever OS they booted.
609
+
- Downgrades are not supported. HyperShift does not provide guardrails against NodePool downgrades today, but downgrading to a release that predates this feature is not a tested or supported workflow.
0 commit comments