Skip to content

Add annotation-based resource adoption policy#3798

Open
dislbenn wants to merge 7 commits intostolostron:mainfrom
dislbenn:feature/resource-adoption-policy
Open

Add annotation-based resource adoption policy#3798
dislbenn wants to merge 7 commits intostolostron:mainfrom
dislbenn:feature/resource-adoption-policy

Conversation

@dislbenn
Copy link
Copy Markdown
Contributor

@dislbenn dislbenn commented Mar 31, 2026

Description

Introduce a configurable resource adoption policy controlled via the installer.open-cluster-management.io/resource-adoption-policy annotation on the MultiClusterHub resource.

This allows operators to control how MCH handles existing resources that lack installer labels:

  • "Strict" (default): Only manage resources with installer.name and installer.namespace labels
  • "Adopt": Automatically adopt unlabeled resources by adding installer labels during reconciliation

This enables seamless migration from manually created resources and provides flexibility for different deployment scenarios while maintaining safe defaults.

Related Issue

If applicable, please reference the issue(s) that this pull request addresses.

Changes Made

  • Validates adoption policy and logs warnings for invalid values
  • Prevents adoption of resources with partial/corrupted installer labels
  • Adds labels to template (desired state) rather than existing resource
  • Comprehensive unit test coverage for all scenarios

Screenshots (if applicable)

Add screenshots or GIFs that demonstrate the changes visually, if relevant.

Checklist

  • I have tested the changes locally and they are functioning as expected.
  • I have updated the documentation (if necessary) to reflect the changes.
  • I have added/updated relevant unit tests (if applicable).
  • I have ensured that my code follows the project's coding standards.
  • I have checked for any potential security issues and addressed them.
  • I have added necessary comments to the code, especially in complex or unclear sections.
  • I have rebased my branch on top of the latest main/master branch.

Additional Notes

Add any additional notes, context, or information that might be helpful for reviewers.

Reviewers

Tag the appropriate reviewers who should review this pull request. To add reviewers, please add the following line: /cc @reviewer1 @reviewer2

/cc @cameronmwall @ngraham20

Definition of Done

  • Code is reviewed.
  • Code is tested.
  • Documentation is updated.
  • All checks and tests pass.
  • Approved by at least one reviewer.
  • Merged into the main/master branch.

Summary by CodeRabbit

  • New Features

    • Added resource adoption policy support to control which resources a hub instance may manage or adopt.
    • Added ownership checks to avoid updating or deleting resources not owned by the current instance.
  • Bug Fixes

    • Clarified hub status handling during pruning to reflect when old resources have been fully removed.
  • Tests

    • Added tests covering adoption policy behavior and ownership validation.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a3a1fada-0cdf-45ae-81d5-78ef88e9599f

📥 Commits

Reviewing files that changed from the base of the PR and between 9f2effa and b544c38.

📒 Files selected for processing (1)
  • pkg/utils/annotations.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/utils/annotations.go

📝 Walkthrough

Walkthrough

Adds policy-driven resource adoption and ownership checks to template apply/delete paths, expands annotation matching to include a resource-adoption-policy key, and adjusts hub status handling to mark pruning-complete as Progressing when appropriate.

Changes

Cohort / File(s) Summary
Adoption Policy & Ownership Controls
controllers/templates.go
Adds ownership gate checks to applyTemplate and deleteTemplate. Introduces getAdoptionPolicy (reads/validates adoption annotation, defaults to "Strict") and ensureResourceOwnership (validates installer labels, rejects partial labels, injects installer labels into desired template when adopting).
Adoption Tests
controllers/templates_test.go
New unit tests covering getAdoptionPolicy (defaulting, valid/invalid values, case sensitivity) and ensureResourceOwnership (label presence/partial presence, adoption injection, nil-label safety, ensures template mutated not existing resource).
Annotation Tracking
pkg/utils/annotations.go
Adds AnnotationResourceAdoptionPolicy constant and updates AnnotationsMatch to explicitly check the new adoption annotation and require non-important annotation key sets to match (added/removed keys now treated as mismatches).
Hub Pruning Status
controllers/status.go
Changes hub status logic: when pruning completed and hub is not paused, set Progressing=True with AllOldComponentsRemovedReason (and message) instead of setting Complete=True; adds related log message.

Sequence Diagram

sequenceDiagram
    participant R as Reconciler
    participant M as MultiClusterHub
    participant E as ensureResourceOwnership
    participant Res as ExistingResource

    R->>M: Read adoption annotation
    M-->>R: Annotation (value or missing)
    R->>R: getAdoptionPolicy(annotation)
    R->>Res: Fetch existing resource
    Res-->>R: Return resource (labels)
    R->>E: ensureResourceOwnership(existing, desired, policy)
    alt Policy = Strict
        E-->>R: false (reject if installer labels missing/partial)
        R->>R: Skip update/delete
    else Policy = Adopt
        E->>R: true (inject installer labels into desired)
        R->>R: Proceed with update/delete
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nudge templates with gentle paws tonight,
Labels stitched in moonbeam, adoption done right,
Pruning cleared, progress flagged in sight,
A rabbit's hop—order set, tidy and light. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and specifically describes the main feature introduced in the PR: a configurable resource adoption policy controlled via annotation.
Description check ✅ Passed The description covers the purpose, related changes, and key implementation details, and includes specific reviewers and a completed checklist; though some sections use template placeholders, the core information is substantive and complete.

✏️ 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

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

@openshift-ci
Copy link
Copy Markdown

openshift-ci bot commented Mar 31, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: dislbenn

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

Introduce a configurable resource adoption policy controlled via the
installer.open-cluster-management.io/resource-adoption-policy annotation
on the MultiClusterHub resource.

This allows operators to control how MCH handles existing resources that
lack installer labels:
- "Strict" (default): Only manage resources with installer.name and
  installer.namespace labels
- "Adopt": Automatically adopt unlabeled resources by adding installer
  labels during reconciliation

Key features:
- Validates adoption policy and logs warnings for invalid values
- Prevents adoption of resources with partial/corrupted installer labels
- Adds labels to template (desired state) rather than existing resource
- Comprehensive unit test coverage for all scenarios

This enables seamless migration from manually created resources and
provides flexibility for different deployment scenarios while maintaining
safe defaults.

Signed-off-by: dislbenn <dbennett@redhat.com>
@dislbenn dislbenn force-pushed the feature/resource-adoption-policy branch from 90ffa31 to 4fe7f7a Compare March 31, 2026 13:23
@dislbenn
Copy link
Copy Markdown
Contributor Author

/test sonar-pre-submit

Reintroduce condition-clearing logic that was accidentally removed in
PR stolostron#3688 when ensureRemovalsGone() and ensureAppsubsGone() were deleted.

When all components are successful but a pruning condition exists
(OldComponentRemovedReason or OldComponentNotRemovedReason), the hub
is stuck in Pending status even though all resources were successfully
deleted. This fix detects this state and sets AllOldComponentsRemovedReason,
allowing the next reconcile to clear the condition and transition to Running.

The AllOldComponentsRemovedReason constant (line 52) has been orphaned
since March 2026 but is now reactivated.

Fixes issue where MCH remains Pending with 'Not all components successfully
pruned' message after disabling components like cluster-backup.

Signed-off-by: dislbenn <dbennett@redhat.com>
Fix AnnotationsMatch() to trigger reconciles when resource-adoption-policy
annotation is added or changed. Previously, only a whitelist of specific
annotations would trigger reconciles, and resource-adoption-policy was not
included.

This change uses a combined approach:
1. Explicitly tracks important annotations including resource-adoption-policy
2. Detects when ANY annotation is added or removed (enables manual triggering)
3. Ignores value changes to non-important annotations (reduces noise)

Now when users add/change the resource-adoption-policy annotation on MCH,
the controller will immediately start a new reconcile loop. Also allows
forcing reconciles by adding any temporary annotation.

Signed-off-by: dislbenn <dbennett@redhat.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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 (2)
controllers/templates.go (2)

246-247: Consider defining constants for installer label keys.

The label keys "installer.name" and "installer.namespace" are hardcoded here and also in pkg/utils/utils.go:AddInstallerLabel. If these label names ever need to change, multiple locations must be updated.

♻️ Suggested refactor to use constants

Add constants in pkg/utils/labels.go or similar:

const (
    InstallerNameLabel      = "installer.name"
    InstallerNamespaceLabel = "installer.namespace"
)

Then update both locations to use these constants.

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

In `@controllers/templates.go` around lines 246 - 247, Define shared constants for
the installer label keys (e.g., InstallerNameLabel = "installer.name" and
InstallerNamespaceLabel = "installer.namespace") in a central utils/labels.go
and replace the hardcoded strings in controllers.templates.go (where
existingLabels is checked) and in pkg/utils/utils.go:AddInstallerLabel to
reference those constants; update any other occurrences to use these constants
so changes to the label names are made in one place.

87-94: Ownership check failures are logged but may appear as silent success to callers.

When ensureResourceOwnership returns false, both applyTemplate and deleteTemplate return ctrl.Result{}, nil without error. Per the calling code in controllers/components.go, the loop continues without any indication that the resource was skipped.

While the log messages provide observability, consider whether callers should be notified that resources were skipped (e.g., via a custom result type or metric) to help operators understand why certain resources aren't being managed.

Also applies to: 214-222

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

In `@controllers/templates.go` around lines 87 - 94, When
ensureResourceOwnership(existing, template, m) returns false, currently
applyTemplate and deleteTemplate return ctrl.Result{}, nil which hides skips
from callers; change both functions to return a distinguishable error (e.g., a
sentinel ErrResourceNotOwned) instead of nil so callers (such as the
reconciliation loop in controllers/components.go) can detect skips and act
(metrics, logging, or special handling). Update the code paths at the two sites
where ensureResourceOwnership is checked (the block around
ensureResourceOwnership(...) returning false at lines shown and the similar
block at 214-222) to return ErrResourceNotOwned, declare that sentinel error
near the top of the file, and adjust callers to treat ErrResourceNotOwned as a
non-fatal, observable condition.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/utils/annotations.go`:
- Around line 177-186: The current key-set comparison in AnnotationsMatch causes
false negatives when system-managed annotations change; update AnnotationsMatch
to first filter out known system annotation keys/prefixes (e.g. keys starting
with "kubectl.kubernetes.io/", "kubernetes.io/", and known exact keys like
"controller-revision-hash" or "deployment.kubernetes.io/revision") from both old
and new annotation maps, then compare lengths and key sets of the filtered maps;
keep the existing behavior of allowing manual triggers by only excluding
system-managed annotations (not user annotations). Reference: function
AnnotationsMatch and its use in GenerationChangedPredicate.Update to locate and
modify the comparison logic.

---

Nitpick comments:
In `@controllers/templates.go`:
- Around line 246-247: Define shared constants for the installer label keys
(e.g., InstallerNameLabel = "installer.name" and InstallerNamespaceLabel =
"installer.namespace") in a central utils/labels.go and replace the hardcoded
strings in controllers.templates.go (where existingLabels is checked) and in
pkg/utils/utils.go:AddInstallerLabel to reference those constants; update any
other occurrences to use these constants so changes to the label names are made
in one place.
- Around line 87-94: When ensureResourceOwnership(existing, template, m) returns
false, currently applyTemplate and deleteTemplate return ctrl.Result{}, nil
which hides skips from callers; change both functions to return a
distinguishable error (e.g., a sentinel ErrResourceNotOwned) instead of nil so
callers (such as the reconciliation loop in controllers/components.go) can
detect skips and act (metrics, logging, or special handling). Update the code
paths at the two sites where ensureResourceOwnership is checked (the block
around ensureResourceOwnership(...) returning false at lines shown and the
similar block at 214-222) to return ErrResourceNotOwned, declare that sentinel
error near the top of the file, and adjust callers to treat ErrResourceNotOwned
as a non-fatal, observable condition.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d0f2c6d9-e6da-4d55-a33c-c5f9edfae024

📥 Commits

Reviewing files that changed from the base of the PR and between cc50373 and 9f2effa.

📒 Files selected for processing (4)
  • controllers/status.go
  • controllers/templates.go
  • controllers/templates_test.go
  • pkg/utils/annotations.go

Comment on lines +177 to +186
// Also check if any annotation was added or removed (allows manual triggering)
// Don't check values, just keys - value changes to non-important annotations are ignored
if len(old) != len(new) {
return false
}
for k := range old {
if _, exists := new[k]; !exists {
return false
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Key-set comparison may trigger unnecessary reconciles from system annotations.

The key-set check causes AnnotationsMatch to return false when any annotation key is added or removed, including system-managed annotations like kubectl.kubernetes.io/last-applied-configuration. This could trigger reconciles even when no meaningful annotation has changed.

Per the context snippet in pkg/predicate/predicate.go, this function is used in GenerationChangedPredicate.Update() to decide whether to trigger reconciliation. While the comment indicates this enables "manual triggering," it may also increase reconcile frequency from unrelated annotation changes.

Consider whether this trade-off is acceptable, or if you want to filter out known system annotation prefixes.

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

In `@pkg/utils/annotations.go` around lines 177 - 186, The current key-set
comparison in AnnotationsMatch causes false negatives when system-managed
annotations change; update AnnotationsMatch to first filter out known system
annotation keys/prefixes (e.g. keys starting with "kubectl.kubernetes.io/",
"kubernetes.io/", and known exact keys like "controller-revision-hash" or
"deployment.kubernetes.io/revision") from both old and new annotation maps, then
compare lengths and key sets of the filtered maps; keep the existing behavior of
allowing manual triggers by only excluding system-managed annotations (not user
annotations). Reference: function AnnotationsMatch and its use in
GenerationChangedPredicate.Update to locate and modify the comparison logic.

The key-set comparison was incorrectly treating deprecated annotation keys
as different from their current equivalents, causing test failures when old
maps used deprecated keys (e.g. "mch-pause") and new maps used current keys
(e.g. "installer.open-cluster-management.io/pause").

Now we filter out already-checked annotation keys (including deprecated ones)
before comparing the remaining key sets. This allows the semantic checks to
handle deprecated fallbacks while still detecting additions/removals of
unchecked annotations for manual triggering.

Fixes test: Test_AnnotationMatch/Annotations_should_match

Signed-off-by: dislbenn <dbennett@redhat.com>
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.

1 participant