Skip to content
This repository was archived by the owner on Sep 9, 2026. It is now read-only.

MGMT-23977: Implement per-tier StorageClass resolution in Tenant controller - #199

Merged
openshift-merge-bot[bot] merged 3 commits into
osac-project:mainfrom
zszabo-rh:feature/MGMT-23977-per-tier-resolution
May 4, 2026
Merged

openshift-merge-bot[bot] merged 3 commits into
osac-project:mainfrom
zszabo-rh:feature/MGMT-23977-per-tier-resolution

Conversation

@zszabo-rh

@zszabo-rh zszabo-rh commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replace single-tier getTenantStorageClass() with getTenantStorageClasses() that resolves all storage tiers independently
  • For each distinct storage-tier label value, apply two-step fallback: tenant-specific SC first, then shared Default SC
  • StorageClasses without the osac.openshift.io/storage-tier label are ignored
  • Tier values normalized to lowercase
  • Tenant is Ready when at least one tier resolves; Progressing when none do
  • Per-tier duplicate detection emits DuplicateStorageClass warning events without blocking other tiers
  • Emit StorageClassNotReady warning event when no tier resolves
  • Adds groupByTier() helper that classifies StorageClasses by tier label
  • Deprecated status.storageClass field populated from the first resolved tier for backward compatibility (MGMT-24139)

Implements the resolution algorithm from EP #32.

Test plan

  • make test passes — 12-step integration test covering: no namespace, no SCs, SC without tier label ignored, shared Default fallback, tenant-specific priority, multi-tier resolution, per-tier duplicate isolation, mixed resolution, full degradation with NotReady event, all-tiers-duplicate
  • Unit tests for groupByTier() (including lowercase normalization) and joinStorageClassNames()
  • go build ./... compiles cleanly

Jira: MGMT-23977

Summary by CodeRabbit

  • New Features

    • Storage is resolved per storage-tier; tenant status lists all resolved storage classes by tier and still marks a primary choice.
    • Emits warning events when tiers are unresolvable; new distinct event reason for "not ready" cases.
  • Chores

    • Simplified tenant condition reasons (removed redundant constants).
  • Tests

    • Expanded coverage for multi-tier resolution, grouping/normalization, per-tier priority/fallback, and duplicate/misconfiguration cases.

@openshift-ci-robot

openshift-ci-robot commented Apr 24, 2026

Copy link
Copy Markdown

@zszabo-rh: This pull request references MGMT-23977 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the sub-task to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Summary

  • Replace single-tier getTenantStorageClass() with getTenantStorageClasses() that resolves all storage tiers independently
  • For each distinct storage-tier label value, apply two-step fallback: tenant-specific SC first, then shared Default SC
  • StorageClasses without the osac.openshift.io/storage-tier label are ignored
  • Tenant is Ready when at least one tier resolves; Progressing when none do
  • Per-tier duplicate detection emits DuplicateStorageClass warning events without blocking other tiers
  • Adds groupByTier() helper that classifies StorageClasses by tier label

Implements the resolution algorithm from EP #32.

Test plan

  • make test passes — 12-step integration test covering: no namespace, no SCs, SC without tier label ignored, shared Default fallback, tenant-specific priority, multi-tier resolution, per-tier duplicate isolation, mixed resolution, full degradation, all-tiers-duplicate
  • Unit tests for groupByTier() and joinStorageClassNames()
  • make lint passes
  • go build ./... compiles cleanly

Jira: MGMT-23977

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci
openshift-ci Bot requested review from larsks and trewest April 24, 2026 10:35
@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@zszabo-rh has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 42 minutes and 43 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ec5ce9a0-a2bc-45c8-9156-cf980d069743

📥 Commits

Reviewing files that changed from the base of the PR and between bd6d450 and f4c40ad.

📒 Files selected for processing (2)
  • internal/controller/tenant_controller.go
  • internal/controller/tenant_controller_test.go
📝 Walkthrough

Walkthrough

Controller storage-class resolution changed from selecting a single tenant/shared Default to resolving StorageClasses across all distinct osac.openshift.io/storage-tier labels. It now groups StorageClasses by tier, deterministically processes each tier (tenant-specific first, then shared Default), records per-tier resolved entries and duplicate/warning messages, updates Tenant conditions and instance.Status.StorageClasses with all resolved tiers, and preserves Status.StorageClass as the first resolved tier.

Changes

Cohort / File(s) Summary
Multi-tier Storage Resolution
internal/controller/tenant_controller.go
Replaces single-SC resolution with multi-tier flow: adds getTenantStorageClasses, tierResolutionResult, groupByTier, deterministic tier sorting, per-tier resolution (tenant-first, shared Default fallback), collects duplicate/error messages per tier, sets TenantConditionStorageClassReady with aggregated reason/message, writes instance.Status.StorageClasses (all resolved tiers) and derives instance.Status.StorageClass from the first resolved tier.
Tests and helpers
internal/controller/tenant_controller_test.go
Updates tests to validate multi-tier behavior: asserts tenant.Status.StorageClasses (resolved entries) and Status.StorageClass derived from first tier, covers tenant vs shared-default per tier, ignores unlabeled StorageClasses, checks duplicate-within-tier handling and aggregated warnings, and adds tests for groupByTier (filtering, grouping, lowercase normalization, nil/empty cases).
Event/Name constants
internal/controller/tenant_names.go
Adds eventReasonStorageClassNotReady = "StorageClassNotReady" to distinguish tier-resolution-failure events from duplicate/default events.
API constants cleanup
api/v1alpha1/tenant_types.go
Removes TenantReasonSharedDefault and TenantReasonMultipleDefaultsFound, leaving TenantReasonFound, TenantReasonNotFound, and TenantReasonMultipleFound (formatting adjusted).

Sequence Diagram(s)

sequenceDiagram
    participant Controller
    participant K8sAPI as "Kubernetes API\n(StorageClass list)"
    participant Events
    participant Status as "Tenant Status"

    Controller->>K8sAPI: List StorageClasses
    K8sAPI-->>Controller: StorageClass objects
    Controller->>Controller: groupByTier -> sort tiers
    alt For each tier
        Controller->>Controller: resolve tenant-specific SCs
        alt tenant-specific found (1)
            Controller->>Status: add ResolvedStorageClass(tier, tenantSC)
        else multiple tenant-specific
            Controller->>Events: emit duplicate warning (per-tier)
            Controller->>Status: set condition MultipleFound for tier
        else none
            Controller->>Controller: fallback -> find shared Default SCs
            alt single shared Default
                Controller->>Status: add ResolvedStorageClass(tier, defaultSC)
            else multiple defaults
                Controller->>Events: emit duplicate default warning
                Controller->>Status: set condition MultipleFound for tier
            else none
                Controller->>Events: emit StorageClassNotReady (tier aggregated)
                Controller->>Status: set StorageClassReady = False (NotFound)
            end
        end
    end
    Controller->>Status: set Status.StorageClasses (all resolved)
    Controller->>Status: set Status.StorageClass = first resolved tier (if any)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

lgtm, approved

Suggested reviewers

  • tzvatot
  • akshaynadkarni

Poem

🐰 I hop through tiers with careful cheer,
Tenant-first, then Default near.
I warn of duplicates, tidy each row,
Resolved classes gathered — off we go! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and specifically describes the main change: implementing per-tier StorageClass resolution in the Tenant controller, matching the core refactoring from single-tier to multi-tier resolution.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 42 minutes and 43 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@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: 1

🧹 Nitpick comments (3)
internal/controller/tenant_controller.go (2)

181-194: Nit: conditionMessage() can use a value receiver.

conditionMessage doesn't mutate state, and tierResolutionResult is small (four slice headers). A value receiver is more idiomatic for read-only methods on small structs. Optional.

♻️ Proposed change
-func (r *tierResolutionResult) conditionMessage() string {
+func (r tierResolutionResult) conditionMessage() string {
 	var parts []string
 	parts = append(parts, r.resolvedMessages...)
 	parts = append(parts, r.errorMessages...)
 	return strings.Join(parts, "; ")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/tenant_controller.go` around lines 181 - 194, Change the
method receiver of conditionMessage from a pointer to a value receiver: replace
func (r *tierResolutionResult) conditionMessage() string with a value receiver
version (func (r tierResolutionResult) conditionMessage() string) leaving the
body unchanged; this targets the tierResolutionResult type and the
conditionMessage method only—no other call sites need modification since value
and pointer receivers are interchangeable for method calls here.

240-252: Optional: collapse tier-set collection using maps.Keys / slices.Sort.

The union-then-sort can be expressed more compactly with stdlib maps/slices helpers. The project uses Go 1.25.0, which fully supports these functions (available since Go 1.21).

♻️ Proposed change
-	allTiers := make(map[string]struct{})
-	for t := range tenantByTier {
-		allTiers[t] = struct{}{}
-	}
-	for t := range defaultByTier {
-		allTiers[t] = struct{}{}
-	}
-
-	sortedTiers := make([]string, 0, len(allTiers))
-	for t := range allTiers {
-		sortedTiers = append(sortedTiers, t)
-	}
-	sort.Strings(sortedTiers)
+	tierSet := make(map[string]struct{}, len(tenantByTier)+len(defaultByTier))
+	for t := range tenantByTier {
+		tierSet[t] = struct{}{}
+	}
+	for t := range defaultByTier {
+		tierSet[t] = struct{}{}
+	}
+	sortedTiers := slices.Sorted(maps.Keys(tierSet))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/tenant_controller.go` around lines 240 - 252, Replace the
manual key-collection and sort in tenant_controller.go by using Go stdlib
helpers: after merging tenantByTier and defaultByTier into allTiers
(map[string]struct{}), obtain the slice of tier names with maps.Keys(allTiers)
and then sort in-place with slices.Sort; update references to sortedTiers to use
that result. This touches the variables allTiers, tenantByTier, defaultByTier
and sortedTiers.
internal/controller/tenant_controller_test.go (1)

360-388: LGTM — groupByTier unit tests.

Covers the three meaningful cases: label-missing is ignored, multi-SC grouping across distinct tiers, and nil input. One optional addition worth considering: an SC with an empty-string tier label (osacStorageTierLabel="") to lock in the tier == "" branch of groupByTier. Not blocking.

➕ Optional additional case
 	It("ignores StorageClasses without storage-tier label", func() {
 		scs := []storagev1.StorageClass{
 			*makeSC("sc-no-tier", "tenant-a", ""),
+			// Explicit empty-string tier value (label present but empty) should also be ignored.
+			func() storagev1.StorageClass {
+				sc := makeSC("sc-empty-tier", "tenant-a", "")
+				sc.Labels[osacStorageTierLabel] = ""
+				return *sc
+			}(),
 			*makeSC("sc-with-tier", "tenant-a", "fast"),
 		}
 		groups := groupByTier(scs)
 		Expect(groups).To(HaveLen(1))
 		Expect(groups).To(HaveKey("fast"))
 		Expect(groups["fast"]).To(HaveLen(1))
 	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/tenant_controller_test.go` around lines 360 - 388, Add a
unit test that covers the case where a StorageClass has the storage-tier label
present but set to the empty string to exercise the tier == "" branch in
groupByTier; create an SC via makeSC with osacStorageTierLabel set to ""
(distinct from nil/no-label case), call groupByTier with that SC and assert the
resulting map contains a key "" with the expected slice length (or that
empty-string keys are handled as your implementation expects) to lock in
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/controller/tenant_controller.go`:
- Around line 206-218: The groupByTier function currently accepts any non-empty
osac.openshift.io/storage-tier label; update groupByTier to validate each tier
value against the CRD regex used by ResolvedStorageClass.Tier
(^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$) and skip any label values that do not match
so invalid (e.g. uppercase) tiers are not propagated to
instance.Status.StorageClasses; include an optional processLogger/EventRecorder
call (or similar) to record skipped invalid tier values for observability and
reference groupByTier and ResolvedStorageClass.Tier when locating the code to
change.

---

Nitpick comments:
In `@internal/controller/tenant_controller_test.go`:
- Around line 360-388: Add a unit test that covers the case where a StorageClass
has the storage-tier label present but set to the empty string to exercise the
tier == "" branch in groupByTier; create an SC via makeSC with
osacStorageTierLabel set to "" (distinct from nil/no-label case), call
groupByTier with that SC and assert the resulting map contains a key "" with the
expected slice length (or that empty-string keys are handled as your
implementation expects) to lock in behavior.

In `@internal/controller/tenant_controller.go`:
- Around line 181-194: Change the method receiver of conditionMessage from a
pointer to a value receiver: replace func (r *tierResolutionResult)
conditionMessage() string with a value receiver version (func (r
tierResolutionResult) conditionMessage() string) leaving the body unchanged;
this targets the tierResolutionResult type and the conditionMessage method
only—no other call sites need modification since value and pointer receivers are
interchangeable for method calls here.
- Around line 240-252: Replace the manual key-collection and sort in
tenant_controller.go by using Go stdlib helpers: after merging tenantByTier and
defaultByTier into allTiers (map[string]struct{}), obtain the slice of tier
names with maps.Keys(allTiers) and then sort in-place with slices.Sort; update
references to sortedTiers to use that result. This touches the variables
allTiers, tenantByTier, defaultByTier and sortedTiers.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3eae8515-ecc7-4d3c-80a2-e0adef310740

📥 Commits

Reviewing files that changed from the base of the PR and between 525facb and b242337.

📒 Files selected for processing (2)
  • internal/controller/tenant_controller.go
  • internal/controller/tenant_controller_test.go

Comment thread internal/controller/tenant_controller.go
@zszabo-rh
zszabo-rh force-pushed the feature/MGMT-23977-per-tier-resolution branch from 32ca009 to 34f43b8 Compare April 29, 2026 07:40

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/controller/tenant_controller_test.go (1)

116-118: ⚠️ Potential issue | 🟠 Major

Assert reconcile success instead of discarding errors

On Line 117, _ = doReconcile() can mask reconciliation failures and let assertions pass against stale status. Please assert success in the polling loop.

Proposed fix
 				Eventually(func(g Gomega) {
-					_ = doReconcile()
+					g.Expect(doReconcile()).To(Succeed())
 					g.Expect(k8sClient.Get(ctx, typeNamespacedName, tenant)).To(Succeed())
 					g.Expect(tenant.Status.Phase).To(Equal(expectedPhase))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/controller/tenant_controller_test.go` around lines 116 - 118, The
test currently discards errors from doReconcile() which can hide failures;
inside the Eventually(func(g Gomega) { ... }) replace `_ = doReconcile()` with
an assertion that it succeeds (e.g. `g.Expect(doReconcile()).To(Succeed())`) so
reconcile errors fail the poll immediately, then continue with
`g.Expect(k8sClient.Get(ctx, typeNamespacedName, tenant)).To(Succeed())` to
assert the fetched resource is present.
🧹 Nitpick comments (1)
internal/controller/tenant_controller_test.go (1)

40-45: Use the tenant label constant in makeSC for clarity

makeSC populates ObjectMeta.Labels, but Line 41 uses osacTenantAnnotation. Even if values currently match, this couples tests to cross-constant equivalence and is easy to regress. Prefer the label constant here.

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

In `@internal/controller/tenant_controller_test.go` around lines 40 - 45, In
makeSC, change the key used when populating ObjectMeta.Labels from
osacTenantAnnotation to the tenant label constant (the constant used for tenant
labels, e.g., osacTenantLabel) so the test uses the label constant rather than
the annotation constant; locate makeSC and replace the
labels[osacTenantAnnotation] assignment with labels[<tenant-label-constant>] =
tenant (keeping the tier logic using osacStorageTierLabel unchanged) to avoid
coupling the test to annotation vs label constant equality.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@internal/controller/tenant_controller_test.go`:
- Around line 116-118: The test currently discards errors from doReconcile()
which can hide failures; inside the Eventually(func(g Gomega) { ... }) replace
`_ = doReconcile()` with an assertion that it succeeds (e.g.
`g.Expect(doReconcile()).To(Succeed())`) so reconcile errors fail the poll
immediately, then continue with `g.Expect(k8sClient.Get(ctx, typeNamespacedName,
tenant)).To(Succeed())` to assert the fetched resource is present.

---

Nitpick comments:
In `@internal/controller/tenant_controller_test.go`:
- Around line 40-45: In makeSC, change the key used when populating
ObjectMeta.Labels from osacTenantAnnotation to the tenant label constant (the
constant used for tenant labels, e.g., osacTenantLabel) so the test uses the
label constant rather than the annotation constant; locate makeSC and replace
the labels[osacTenantAnnotation] assignment with labels[<tenant-label-constant>]
= tenant (keeping the tier logic using osacStorageTierLabel unchanged) to avoid
coupling the test to annotation vs label constant equality.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d25785ed-56b6-47f8-ba7a-f27670190c37

📥 Commits

Reviewing files that changed from the base of the PR and between b242337 and 34f43b8.

📒 Files selected for processing (3)
  • internal/controller/tenant_controller.go
  • internal/controller/tenant_controller_test.go
  • internal/controller/tenant_names.go
✅ Files skipped from review due to trivial changes (1)
  • internal/controller/tenant_names.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/controller/tenant_controller.go

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I guess you should remove this for the CI to pass.

g.Expect(tenant.Status.Phase).To(Equal(expectedPhase))
g.Expect(tenant.Status.StorageClass).To(Equal(expectedSC))
if expectedSC == "" {
if expectedSCs == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we check status.StorageClass here too? It's populated for backward compatibility but not asserted anywhere. Something like:

if expectedSCs == nil {
    g.Expect(tenant.Status.StorageClass).To(BeEmpty())
    g.Expect(tenant.Status.StorageClasses).To(BeNil())
} else {
    g.Expect(tenant.Status.StorageClasses).To(ConsistOf(expectedSCs))
    g.Expect(tenant.Status.StorageClass).To(Equal(expectedSCs[0].Name))
}

@akshaynadkarni

Copy link
Copy Markdown
Contributor

TenantReasonSharedDefault and TenantReasonMultipleDefaultsFound (tenant_types.go#L54-L56) are no longer set by the controller after the per-tier refactor. Since this is pre-release v1alpha1, should we remove them to avoid dead code?

}
reason,
condMsg)
r.Recorder.Eventf(instance, nil, corev1.EventTypeWarning, eventReasonStorageClassNotReady, "StorageClassResolution", "%s", condMsg)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: When should a Tenant be considered not ready due to StorageClass issues?

Currently StorageClassNotReady is emitted only when zero tiers resolve. If some tiers resolve and others have duplicates, the Tenant is Ready and the only signal is a DuplicateStorageClass event.

This raises a question: should a Tenant be Ready when a tier that was explicitly configured for it doesn't resolve? For example, if an admin creates a tenant-specific "fast" StorageClass, then accidentally introduces a duplicate, the Tenant stays Ready. The misconfiguration is only visible in events, not in the phase.

Before deciding on the event model (StorageClassNotReady vs TenantNotReady vs TenantTierNotReady), it's worth clarifying the readiness semantics:

Option 1: At least one tier resolves = Ready (current, per EP #32)

  • StorageClassNotReady only fires when all tiers fail
  • Pro: lenient, unrelated tier misconfigurations don't block the tenant
  • Con: hides misconfigurations in tiers explicitly set up for the tenant

Option 2: All discovered tiers must resolve = Ready

  • Any tier failure blocks readiness
  • Pro: catches all misconfigurations
  • Con: a duplicate in a shared Default tier the tenant doesn't need would block it

Option 3: Spec-driven required tiers

  • Add requiredTiers to TenantSpec, controller checks exactly those
  • Pro: explicit contract, cleanest model
  • Con: CRD change, broader scope

This PR implements Option 1 per the EP. Raising it here since the answer shapes what events we emit and when. Happy to discuss offline.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think option 1 is the right starting point for v1, a misconfiguration in one tier shouldn't block the entire tenant when other tiers are fine.

The DuplicateStorageClass event already signals per-tier misconfigurations. If we find that operators need stronger guarantees, option 3 would be the cleanest future evolution, it makes the contract explicit without overloading the resolution logic.

@akshaynadkarni akshaynadkarni Apr 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thinking about the event hierarchy a bit more. Even with Option 1, we could separate cause from effect in the event naming:

Tenant-level (all tiers failed, tenant is Progressing):

  • TenantNotReady: emitted when no tier resolves at all

Tier-level (specific tier failed, tenant may still be Ready):

  • TenantStorageTierNotReady: emitted per failing tier, with reason in the message:
    • DuplicateStorageClass: multiple SCs matched for the tier
    • StorageClassMissing: no SC found for the tier (no tenant-specific, no Default)

This separates what happened (cause) from the impact (effect), and also future-proofs for new failure reasons without changing the event structure. Today DuplicateStorageClass serves as both the cause and the event name, which works but conflates the two.

One thing to consider: for TenantStorageTierNotReady to be meaningful, we'd need to know which tiers are expected for a tenant. Otherwise we'd fire events for tiers that aren't relevant to the tenant (e.g., a shared Default "archive" tier that the tenant doesn't use). This might circle back to tracking enabled tiers per tenant, either in the spec or derived from discovered SCs. This can be tackled in post v1.0, if we identify the events are noisy or just as part of event hardening.

WDYT?

…roller

Replace single-tier getTenantStorageClass() with getTenantStorageClasses()
that resolves all storage tiers independently. For each tier, applies
tenant-specific first, then shared Default fallback. StorageClasses
without the storage-tier label are ignored. Tier values are normalized
to lowercase.

Tenant is Ready when at least one tier resolves. Per-tier duplicate
detection emits DuplicateStorageClass warning events without blocking
other tiers. Emit StorageClassNotReady warning event when no tier
resolves.

The deprecated status.storageClass field is populated from the first
resolved tier for backward compatibility (MGMT-24139).

Signed-off-by: Zoltan Szabo <zszabo@redhat.com>
Generated-By: Claude Code (Anthropic)
- Remove unused TenantReasonSharedDefault and
  TenantReasonMultipleDefaultsFound constants (dead code after
  per-tier refactor)
- Assert deprecated status.storageClass field in multi-tier tests
  to verify backward compatibility

Signed-off-by: Zoltan Szabo <zszabo@redhat.com>
Generated-By: Claude Code (Anthropic)
@zszabo-rh
zszabo-rh force-pushed the feature/MGMT-23977-per-tier-resolution branch from 34f43b8 to bd6d450 Compare April 30, 2026 06:05
Remove unused storageTierFromLabel function. Add tier label validation
in groupByTier: values that don't match the CRD pattern after lowercase
normalization are silently skipped, preventing status update rejections
and reconciliation loops from malformed labels.

Signed-off-by: Zoltan Szabo <zszabo@redhat.com>
Generated-By: Claude Code (Anthropic)

@akshaynadkarni akshaynadkarni left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@zszabo-rh Approving this PR. Let's carry forward this discussion offline.

It will good to get an agreement on the events to generate and add a ticket to the story to address it properly.

@openshift-ci

openshift-ci Bot commented May 4, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: akshaynadkarni, zszabo-rh

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants