Skip to content

feat(bundler): add --storage-class flag for registry-driven injection#729

Merged
mchmarny merged 4 commits into
NVIDIA:mainfrom
dtzar:main
May 6, 2026
Merged

feat(bundler): add --storage-class flag for registry-driven injection#729
mchmarny merged 4 commits into
NVIDIA:mainfrom
dtzar:main

Conversation

@dtzar

@dtzar dtzar commented May 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Decouples storageClass from recipe overlays

Motivation / Context

Fixes: #503

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Build/CI/tooling

Component(s) Affected

  • CLI (cmd/aicr, pkg/cli)
  • API server (cmd/aicrd, pkg/api, pkg/server)
  • Recipe engine / data (pkg/recipe)
  • Bundlers (pkg/bundler, pkg/component/*)
  • Collectors / snapshotter (pkg/collector, pkg/snapshotter)
  • Validator (pkg/validator)
  • Core libraries (pkg/errors, pkg/k8s)
  • Docs/examples (docs/, examples/)
  • Other: ____________

Implementation Notes

Implemented with Claude Code assistance; all code reviewed and tested locally (make qualify passes).

Testing

# Commands run (prefer `make qualify` for non-trivial changes)
make qualify

Risk Assessment

  • Low — Isolated change, well-tested, easy to revert
  • Medium — Touches multiple components or has broader impact
  • High — Breaking change, affects critical paths, or complex rollout

The change is purely additive — a new optional flag that's a no-op when absent. No existing behavior changes: overlay defaults still apply, nothing is removed or reordered.

Checklist

  • Tests pass locally (make test with -race)
  • Linter passes (make lint)
  • I did not skip/disable tests to make CI green
  • I added/updated tests for new functionality
  • I updated docs if user-facing behavior changed
  • Changes follow existing patterns in the codebase
  • Commits are cryptographically signed (git commit -S) — GPG signing info

@dtzar dtzar requested review from a team as code owners May 2, 2026 02:07
@copy-pr-bot

copy-pr-bot Bot commented May 2, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions

github-actions Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Welcome to AICR, @dtzar! Thanks for your first pull request.

Before review, please ensure:

  • All commits are signed off per the DCO
  • CI checks pass (tests, lint, security scan)
  • The PR description explains the why behind your changes

A maintainer will review this soon.

@coderabbitai

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/user/cli-reference.md`:
- Around line 904-905: Update the CLI docs to explicitly state that an explicit
per-component --set <component>:<path>=... on the same storageClassPaths path
takes precedence over the global --storage-class; reference the implementation
behavior in pkg/bundler/bundler.go (the logic around Lines 651-672) to ensure
wording matches code, and apply the same clarification to the duplicate sentence
at the other instance (around lines 920-921) so both locations document that
--set overrides --storage-class.

In `@pkg/bundler/bundler_test.go`:
- Around line 909-911: The test currently uses t.Skip when the registry contract
for kube-prometheus-stack or storageClassPaths is missing (checks: comp == nil
|| len(comp.GetStorageClassPaths()) == 0); change these to fail fast using
t.Fatalf with a clear message (e.g., "registry missing kube-prometheus-stack or
storageClassPaths: cannot run storage class path test") so CI fails instead of
silently skipping; apply the same replacement for the second occurrence around
the 976-978 check to ensure missing registry entries cause test failures rather
than skips.
- Around line 901-1009: Replace the three separate tests
(TestApplyNodeSchedulingOverrides_StorageClass,
TestApplyNodeSchedulingOverrides_StorageClass_Empty,
TestApplyNodeSchedulingOverrides_StorageClass_ExplicitOverridePrecedence) with a
single table-driven test that enumerates the scenarios: (1) global storageClass
set -> inject into paths, (2) global storageClass empty -> do not override
existing values, and (3) explicit per-component --set override wins over global
storageClass. For each case set up inputs using config.NewConfig (with/without
WithStorageClass and WithValueOverrides), create the bundle via New(WithConfig),
prepare the values map (use component.ApplyMapOverrides when simulating CLI
--set), call b.applyNodeSchedulingOverrides("kube-prometheus-stack", values),
and assert with component.GetValueByPath that the final value matches the
expected result; use t.Run for subtests and include the registry/comp presence
check (recipe.GetComponentRegistry and registry.Get) to t.Skip when
storageClassPaths are unavailable.

In `@pkg/bundler/config/config.go`:
- Around line 134-136: Change the optional storageClass field from string to
*string in the config struct (symbol: storageClass) and apply the same
pointer-pattern to the other optional fields flagged (the ones around the same
declaration and usages). Update all uses of storageClass (e.g., any
StorageClass() accessor, defaulting/validation logic, NewConfig or unmarshalling
code that sets/reads storageClass, and places that compare it to "" or assign
"") to handle nil vs non-nil (check for nil, and dereference *storageClass when
you need the string value); adjust JSON/YAML marshal/unmarshal or helper getters
so nil means “unset” and &"" means “set to empty” per the repo convention.
Ensure call sites that previously relied on empty string to mean unset are
changed to nil checks or use a helper like GetStorageClass() that returns
(string, bool) or *string as appropriate.

In `@pkg/cli/bundle.go`:
- Around line 344-348: The new CLI flag "storage-class" added in the StringFlag
list is missing from the single-value flag enforcement, so update the validation
list in validateSingleValueFlags to include "storage-class" (the exact string
name) alongside the other single-value flag names; locate
validateSingleValueFlags and add "storage-class" to the slice/array of flag
names it checks so repeated --storage-class usages will be rejected like the
other single-value flags.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 48694af9-bd24-4570-a04a-2a0f21d7c169

📥 Commits

Reviewing files that changed from the base of the PR and between ab17917 and ce9f744.

📒 Files selected for processing (8)
  • docs/user/cli-reference.md
  • pkg/bundler/bundler.go
  • pkg/bundler/bundler_test.go
  • pkg/bundler/config/config.go
  • pkg/bundler/config/config_test.go
  • pkg/cli/bundle.go
  • pkg/recipe/components.go
  • recipes/registry.yaml

Comment thread docs/user/cli-reference.md Outdated
Comment thread pkg/bundler/bundler_test.go Outdated
Comment thread pkg/bundler/bundler_test.go
Comment on lines +134 to +136
// storageClass is the Kubernetes StorageClass name to inject into components at bundle time.
// When non-empty, it overrides the storageClassName at all registry-declared storageClassPaths.
storageClass string

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Use pointer semantics for optional storageClass config

Lines 134-136 and Lines 484-490 model an optional value as string, which conflates “unset” and “set to empty” and diverges from the repo’s optional-config convention.

♻️ Proposed refactor
-	storageClass string
+	storageClass *string
...
-func (c *Config) StorageClass() string {
-	return c.storageClass
+func (c *Config) StorageClass() *string {
+	if c.storageClass == nil {
+		return nil
+	}
+	v := *c.storageClass
+	return &v
 }
...
 func WithStorageClass(storageClass string) Option {
 	return func(c *Config) {
-		c.storageClass = storageClass
+		if storageClass == "" {
+			return
+		}
+		sc := storageClass
+		c.storageClass = &sc
 	}
 }

As per coding guidelines: “Use pointer pattern for optional config (nil = not set, &value = set).”

Also applies to: 292-295, 484-490

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/bundler/config/config.go` around lines 134 - 136, Change the optional
storageClass field from string to *string in the config struct (symbol:
storageClass) and apply the same pointer-pattern to the other optional fields
flagged (the ones around the same declaration and usages). Update all uses of
storageClass (e.g., any StorageClass() accessor, defaulting/validation logic,
NewConfig or unmarshalling code that sets/reads storageClass, and places that
compare it to "" or assign "") to handle nil vs non-nil (check for nil, and
dereference *storageClass when you need the string value); adjust JSON/YAML
marshal/unmarshal or helper getters so nil means “unset” and &"" means “set to
empty” per the repo convention. Ensure call sites that previously relied on
empty string to mean unset are changed to nil checks or use a helper like
GetStorageClass() that returns (string, bool) or *string as appropriate.

Comment thread pkg/cli/bundle.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
pkg/bundler/config/config.go (1)

134-136: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Model optional storageClass with pointer semantics (nil vs set).

Using string here collapses “unset” and “set empty,” which diverges from the repo’s optional-config pattern.

♻️ Proposed direction
- storageClass string
+ storageClass *string

- func (c *Config) StorageClass() string {
-   return c.storageClass
+ func (c *Config) StorageClass() *string {
+   if c.storageClass == nil {
+     return nil
+   }
+   v := *c.storageClass
+   return &v
 }

 func WithStorageClass(storageClass string) Option {
   return func(c *Config) {
-    c.storageClass = storageClass
+    if storageClass == "" {
+      return
+    }
+    sc := storageClass
+    c.storageClass = &sc
   }
 }

As per coding guidelines: “Use pointer pattern for optional config (nil = not set, &value = set). Avoid boolean flags to track whether option was set.”

Also applies to: 292-295, 484-490

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/bundler/config/config.go` around lines 134 - 136, Change the storageClass
field in the config struct from string to *string to model "unset" vs "set
(possibly empty)"; update all code that constructs, reads, compares or
serializes storageClass to handle pointer semantics (treat nil = not set, &"" =
explicitly set empty) including any setters/getters/validation and the code
paths that override storageClassName / registry-declared storageClassPaths where
storageClass is referenced; ensure assignments create a pointer (e.g., take
address of value) and all comparisons dereference safely (check nil before use)
and update JSON/YAML (un)marshalling or defaulting logic to preserve nil vs
set-empty semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@pkg/bundler/config/config.go`:
- Around line 134-136: Change the storageClass field in the config struct from
string to *string to model "unset" vs "set (possibly empty)"; update all code
that constructs, reads, compares or serializes storageClass to handle pointer
semantics (treat nil = not set, &"" = explicitly set empty) including any
setters/getters/validation and the code paths that override storageClassName /
registry-declared storageClassPaths where storageClass is referenced; ensure
assignments create a pointer (e.g., take address of value) and all comparisons
dereference safely (check nil before use) and update JSON/YAML (un)marshalling
or defaulting logic to preserve nil vs set-empty semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 1198ce93-b34a-4eb1-81ac-aef228c93e0d

📥 Commits

Reviewing files that changed from the base of the PR and between ce9f744 and 73b7b2e.

📒 Files selected for processing (8)
  • docs/user/cli-reference.md
  • pkg/bundler/bundler.go
  • pkg/bundler/bundler_test.go
  • pkg/bundler/config/config.go
  • pkg/bundler/config/config_test.go
  • pkg/cli/bundle.go
  • pkg/recipe/components.go
  • recipes/registry.yaml

@dtzar

dtzar commented May 2, 2026

Copy link
Copy Markdown
Contributor Author

The finding is not needed. Here's the verification:

Semantic soundness: "" == unset is correct for storage class. Kubernetes StorageClass names follow RFC 1123 DNS subdomain rules — an empty string is never a valid value, so there is no ambiguity between "unset" and "explicitly set to empty."

Codebase consistency: Every other optional string in this struct uses the same pattern:

repoURL string — "" means unset
targetRevision string — has a default of "main"
certificateIdentityRegexp string — "" means use default
estimatedNodeCount int — 0 means unset (documented inline)
The only pointer in the struct is workloadGateTaint *corev1.Taint — a struct type where nil is the natural zero value, a genuinely different situation.

CLAUDE.md anti-pattern: The rule says "Use pointer pattern instead of boolean flags to track options." That applies when you'd otherwise write storageClass string + storageClassSet bool side by side. The current code has no such boolean companion — the empty-string sentinel is the pattern used by every other field.

Call sites: bundler.go:654 correctly guards with sc != "". cli/bundle.go:473 passes cmd.String(...) which returns "" when unset — exactly right.

Changing to *string here would break the existing pattern without providing any semantic benefit. No change needed.

@mchmarny

mchmarny commented May 2, 2026

Copy link
Copy Markdown
Member

Welcome to AICR, and thanks for picking this up — exactly the kind of cleanup the project benefits from. One scope question to settle before a line-level pass.

PR scope vs. Issue #503

The mechanism here is the right shape — registry-driven storageClassPaths, modeled on nodeScheduling.nodeCountPaths, with --set precedence preserved. That matches existing patterns. 👍

What's missing is the actual decoupling. Issue #503 outlines the need for better decoupling of storageClass from recipe overlays (solving the hardcoded values scattered across overlay files). Ideally we would want not propagate that while making changes in this area:

  • recipes/overlays/eks.yaml:54gp2
  • recipes/overlays/aks.yaml:70managed-csi
  • recipes/overlays/gke-cos.yaml:53standard-rwo
  • recipes/overlays/oke.yaml:56oci-bv

As-is, the PR adds an override path but the overlays remain CSP-specific on this dimension, and the "blocks further mixin extraction" benefit the issue cites is unchanged. PTAL at issue #503 with this lens and extend the PR to remove those four entries. It's a small delta on top of what's already here and would let this PR actually close the issue end-to-end.

Smaller observations

  • The "Component(s) Affected" checklist doesn't include area/recipes, but the PR modifies pkg/recipe/components.go and recipes/registry.yaml. Auto-labeler caught it; worth ticking the box.
  • The head branch is main on your fork — works, but a feature branch makes follow-up pushes cleaner.
  • CI rollup shows only 3 checks (Label PRs, Auto-Merge, CodeRabbit) — the usual make qualify workflow doesn't appear to have run, likely fork-permission gating. Could you confirm make qualify passed locally?

Once scope is settled I'll do a proper line-level review. Thanks again.

coderabbitai[bot]

This comment was marked as resolved.

@mchmarny mchmarny left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/LGTM

@mchmarny

mchmarny commented May 4, 2026

Copy link
Copy Markdown
Member

@dtzar the PR looks good, you will have to amend the commit though with verified signature. See: https://github.com/NVIDIA/aicr/blob/main/CONTRIBUTING.md#developer-certificate-of-origin

@dtzar dtzar force-pushed the main branch 2 times, most recently from c1aeac7 to ef09b9f Compare May 5, 2026 00:26
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

@mchmarny mchmarny enabled auto-merge (squash) May 6, 2026 10:08
@mchmarny mchmarny merged commit 5125148 into NVIDIA:main May 6, 2026
85 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Decouple storageClass from recipe overlays — make it a deployer concern

2 participants