Skip to content

feat: --odf — single-node OpenShift Data Foundation - #19

Merged
raghavendra-talur merged 18 commits into
TheEasyShift:mainfrom
raghavendra-talur:rtalur-odf
Aug 25, 2026
Merged

feat: --odf — single-node OpenShift Data Foundation#19
raghavendra-talur merged 18 commits into
TheEasyShift:mainfrom
raghavendra-talur:rtalur-odf

Conversation

@raghavendra-talur

Copy link
Copy Markdown
Collaborator

Summary

easyshift create -n foo --odf now produces an SNO cluster with a working OpenShift Data Foundation stack: RBD + CephFS StorageClasses backed by a dedicated per-cluster data disk, on both the libvirt and vfkit backends. Distilled from dfmicro's single-node ODF recipe (credit: the SINGLE_NODE=true switch, Immediate-binding StorageClass, and trimmed StorageCluster were worked out there), translated to real OCP and hardware-validated on 4.22.

  • CLI/config: --odf, --odf-disk (default 100 GB sparse, ≥12 GB floor; usable Ceph capacity ≈ a third), --master-cpus. --odf auto-raises the master to the validated floors (8 vCPU / 19456 MiB) with a log line, never an error.
  • Storage backend: a writable per-cluster data disk (VMManager.CreateDataDisk, sparse qcow2 in the libvirt pool / sparse raw in the vfkit state dir) attached after the bake store, so the guest device path is deterministic (/dev/vdb, or /dev/vdc with --bake-images). The LVM Storage operator builds the VG/thin pool in-node from an LVMCluster; an Immediate-binding StorageClass avoids the WaitForFirstConsumer mon/OSD PVC deadlock.
  • install-odf stage (after merge-kubeconfig): four idempotent, wait-bounded phases via a new providers/odf package — operators (LVMS + ODF via OLM, waiting on the dependent ocs-operator CSV + StorageCluster CRD), LVM + StorageClass, single-node enablement (SINGLE_NODE=true patched onto the subscription found by spec.name, node label, CephCSI Driver floors for all controller containers, a cluster-monitoring trim), and the trimmed StorageCluster (waits Ready and both StorageClasses). Rollback is a deliberate no-op (rationale in the spec/docs).
  • Three ODF 4.22 corrections to the original recipe, each found by running it on hardware: device set must be count: 3, replica: 1 (a replica: 3 re-raises getMinimumNodes past the SINGLE_NODE relaxation); empty placements are ignored and render an invalid empty-topologyKey TSC in SINGLE_NODE mode, so the device set carries non-empty no-op TSCs in placement + preparePlacement; the Driver CRs must floor every controller container (plugin alone defaults to 250Mi) and be applied before the StorageCluster to avoid a csi-operator scheduling deadlock.
  • Docs: docs/dev/odf.md internals, --odf usage docs, design + spike + validation record in docs/superpowers/specs/2026-08-25-odf-single-node-design.md, ROADMAP follow-ups (day-2 odf remove, host-RAM-aware --master-ram cap, sidecar lifetime fix).

Test plan

  • make check green throughout (unit tests for renderers value-by-value, installer polling/timeout semantics, stage ordering pinned against fakes, app-level --odf wiring, --simulate traces the four phases).
  • On hardware (Mac mini M4 Pro, OCP 4.22.9 / ODF 4.22.2 / LVMS 4.22.0), shipped code path with zero hand-edits: cluster converged in ~26 min, install-odf completed in ~13 min, StorageCluster Ready + Ceph HEALTH_OK + 3 OSDs + both StorageClasses; RBD PVC Bound in 21 s and a pod wrote/read a file; full easyshift stop/start cycle returned to Ready/HEALTH_OK (device paths stable across reboots).

🤖 Generated with Claude Code

Assisted-by: Claude Code/claude-fable-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Assisted-by: Claude Code/claude-fable-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Assisted-by: Claude Code/claude-fable-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Assisted-by: Claude Code/claude-fable-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Assisted-by: Claude Code/claude-fable-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Assisted-by: Claude Code/claude-fable-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Adds an idempotent CreateDataDisk to interfaces.VMManager for the ODF
data disk: libvirt reuses an existing pool volume by vol-path before
creating a new qcow2 one, vfkit reuses an existing state-dir file
before truncating a new sparse raw one. Both Delete implementations
clean up the disk by name (config.ODFVolName). Fakes mirror ImportDisk.

Assisted-by: Claude Code/claude-fable-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Add ODF data disk attachment in the create-master-vms stage, immediately
after the bake-images block so device ordering is deterministic. The disk is
writable and exclusive, allowing OpenShift Data Foundation to use it as the
backing device for Ceph. Sparse on both backends (APFS clonefile on macOS,
qcow2 on Linux) so disk-space preflight doesn't count it.

Test verifies the master VM carries a writable data disk after the bake store,
that CreateDataDisk was called once with correct parameters, and that plain
clusters (without --odf) have no ODF disk.

Assisted-by: Claude Code/claude-haiku-4-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Add the ODFSpec/ODFInstaller contract for the four ODF install phases
(InstallOperators, SetupLVM, EnableSingleNode, CreateStorageCluster),
wire it into Deps, and add a recording fake (pattern: fake Installer)
plus a simulate-trace section, so later stage/provider work has a
compilable seam to build against.

Assisted-by: Claude Code/claude-fable-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Pure renderer functions for the --odf storage stack (LVMS + ODF/Rook-Ceph
on a single node): operator install manifests, LVMCluster, the Immediate-
binding StorageClass, CephCSI Driver CRs with the hardware-validated
resource floors, the monitoring-config trim, the trimmed StorageCluster
(with the three ODF 4.22 single-node deltas from the spike: count:3/
replica:1, non-empty placement+preparePlacement no-op TSCs, and the
easyshift Immediate SC), and the SINGLE_NODE merge patch.

config.OLMChannelForVersion lives in config/paths.go (not providers/odf)
since the create stage that bumps MasterCPUs/MasterRAM for --odf cannot
import providers.

Assisted-by: Claude Code/claude-sonnet-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
…ators

odfOGName and odfSubName were declared but never referenced; the second
half of RenderOperators hardcoded "openshift-storage-operator-group" and
"odf-operator" as literals instead of interpolating them, unlike the
LVMS half which correctly parameterizes lvmsOGName/lvmsSubName. Wire
both constants into the Sprintf args so the two operator names are
single-sourced, matching the LVMS half.

Assisted-by: Claude Code/claude-sonnet-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Implements interfaces.ODFInstaller: drives the render.go manifests
against a running cluster via `oc`, all through the CommandRunner so
--simulate traces every invocation and tests assert exact calls.

odf.New(cmd) wires an Installer around two shared helpers: apply()
writes rendered content to <WorkDir>/odf/<file> (CommandRunner has no
stdin, so files are both the transport and a debugging artifact) then
`oc --kubeconfig ... apply -f`; pollUntil() polls a probe every 10s
until it reports true or a phase-scoped timeout fires, retrying
transient probe errors (API flaps) rather than failing on the first
one. Timeouts are package vars, not consts, so tests shrink them
instead of waiting out the real durations.

InstallOperators applies the LVMS/ODF Subscriptions and waits for both
CSVs Succeeded (found by name-prefix scan, since CSV names are
version-decorated) plus the StorageCluster CRD. SetupLVM applies the
LVMCluster, waits for status.state Ready, then applies the Immediate
StorageClass. EnableSingleNode looks up the ocs-operator Subscription
by spec.name (its metadata.name is catalog-decorated), patches it to
SINGLE_NODE, waits for the deployment restart to report the env value
and Available=True, labels the node, then applies both CephCSI Driver
CRs and the monitoring trim. CreateStorageCluster applies the
StorageCluster, waits for status.phase Ready, then waits for both ceph
StorageClasses to exist (ocs-client-operator creates them
asynchronously, so Ready alone isn't completion).

Assisted-by: Claude Code/claude-fable-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Review finding: the SINGLE_NODE env lookup in EnableSingleNode used a
nested {range}{range}...{end}{end} jsonpath that was never run against
real oc. Switch to the exact single-line form validated live on the
2026-08-25 spike cluster:
{.spec.template.spec.containers[0].env[?(@.name=="SINGLE_NODE")].value}
ocs-operator's deployment is single-container, so containers[0] is
safe. No test fixture change needed — the fake matches on the
"SINGLE_NODE" substring, which still appears in the new jsonpath.

Assisted-by: Claude Code/claude-fable-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Add the install-odf stage, which drives the four ODF phases
(InstallOperators, SetupLVM, EnableSingleNode, CreateStorageCluster)
against the converged cluster via interfaces.ODFInstaller, deriving
its spec (device path, OLM channel, PVC size) from the cluster config.
No-op unless the cluster opted in with --odf.

Wire installodf.New(d.ODF) into buildStages after merge-kubeconfig and
before finalize, and wire providers/odf.New(cmd) into the shared
production deps used by both Linux and Darwin.

Assisted-by: Claude Code/claude-sonnet-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Assisted-by: Claude Code/claude-sonnet-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Fixes a review finding on docs/dev/odf.md: the replica-1 capacity note
pointed at a "redundancy caveat below" that only exists in
docs/user/usage.md. Reworded in place instead of cross-linking a
fragile anchor. Also carries the spec's 20 GB+ VM -> 28 GB+ host
scaling corollary into the resource-floors section.

Assisted-by: Claude Code/claude-sonnet-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
Assisted-by: Claude Code/claude-fable-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
--odf-disk below 12 GB made DataPVCSizeGi (ODFDiskGB/3 - 3) zero or
negative, failing ~40 minutes into the install instead of at
validation; reject it in validateNew. Also: keep pollUntil's last
probe error in the timeout message instead of discarding it, correct
a comment claiming the ODF data disk uses APFS clonefile on macOS
(it's a truncate-sparse raw file), and use the existing
KubeconfigPath() helper instead of hand-building the path.

Assisted-by: Claude Code/claude-sonnet-5
Signed-off-by: Raghavendra Talur <raghavendra.talur@gmail.com>
@raghavendra-talur
raghavendra-talur merged commit 844baf7 into TheEasyShift:main Aug 25, 2026
2 checks passed
@raghavendra-talur
raghavendra-talur deleted the rtalur-odf branch August 25, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant