Skip to content

⚠ Add spec.group to ClusterObjectSet revision tracking - #2969

Draft
fgiudici wants to merge 4 commits into
operator-framework:mainfrom
fgiudici:oprun-4739-spec-group
Draft

fgiudici wants to merge 4 commits into
operator-framework:mainfrom
fgiudici:oprun-4739-spec-group

Conversation

@fgiudici

@fgiudici fgiudici commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Description

Implement OPRUN-4739 by making ClusterObjectSet.spec.group the authoritative association between a ClusterExtension and its revisions. Revision lookup and event routing currently depend on owner labels or references; the new immutable group keeps that association explicit and gives sibling revisions a shared SSA field manager.

  • Require a group of 1–52 characters with the ClusterExtension name format, and expose it in the Group printer column.
  • Populate generated revisions with the ClusterExtension name, register a .spec.group cache index, and use group-scoped queries for migration, revision state, sibling reconciliation, and pruning.
  • Route ClusterObjectSet events to the ClusterExtension named by the group and use cos-group/<group> for SSA ownership while preserving the OLM metadata prefix.
  • Regenerate apply configurations, experimental CRDs, and manifests; add admission, cache, watch mapping, ownership, and experimental E2E coverage.

This changes the experimental ClusterObjectSet API: new objects must supply spec.group. Migration/backfill for already stored experimental objects and future parent API integration are outside this PR's scope.

Validation

Completed during implementation:

  • make generate manifests crd-ref-docs
  • make lint-api-diff, make lint, make test-unit (race detection and coverage), and make verify
  • All 14 affected experimental E2E scenarios passed across the corrected runs, covering generated groups, shared SSA ownership during rollout, group-isolated pruning, direct ClusterObjectSets, and uninstall cascade cleanup.
  • Live experiment verified unlabeled same-group revision handoff, distinct-group isolation, admission validation, and shared SSA ownership.

An optional standard build-tag compile check fails identically on unchanged main; that pre-existing failure is outside this change. All four commits have DCO sign-offs.

Reviewer Checklist

  • API Go Documentation
  • Tests: Unit Tests (and E2E Tests, if appropriate)
  • Comprehensive Commit Messages
  • Links to related GitHub Issue(s) (tracked by Jira OPRUN-4739)

Summary by CodeRabbit

  • New Features

    • Cluster object sets now include a required, immutable group name. Names must be 1–52 characters, start with a lowercase letter, and contain only lowercase letters, digits, or hyphens; they cannot end with a hyphen.
    • Group names appear in cluster object set listings.
  • Changes

    • Revisions are associated and managed by group, keeping separately grouped object sets distinct.
    • Objects applied by a revision use a group-specific server-side apply manager.

@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Oct 2, 2026
@openshift-ci

openshift-ci Bot commented Oct 2, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign pedjak for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found 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

@netlify

netlify Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for olmv1 ready!

Name Link
🔨 Latest commit b4e2c87
🔍 Latest deploy log https://app.netlify.com/projects/olmv1/deploys/6ac36f130fef0c0008da6345
😎 Deploy Preview https://deploy-preview-2969--olmv1.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (1)
test/e2e/README.md — auto-discovered

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1bf80262-4bb3-45bb-9458-c094a9f2848b
📥 Commits

Reviewing files that changed from the base of the PR and between 4cc1f56 and b4e2c87.

📒 Files selected for processing (34)
  • api/v1/clusterobjectset_types.go
  • api/v1/clusterobjectset_types_test.go
  • api/v1/validation_test.go
  • applyconfigurations/api/v1/clusterobjectsetspec.go
  • applyconfigurations/internal/internal.go
  • cmd/object-controller/main.go
  • cmd/object-controller/main_test.go
  • cmd/operator-controller/main.go
  • helm/olmv1/base/object-controller/crd/experimental/olm.operatorframework.io_clusterobjectsets.yaml
  • internal/object-controller/controllers/clusterobjectset_controller.go
  • internal/object-controller/controllers/clusterobjectset_controller_internal_test.go
  • internal/object-controller/controllers/clusterobjectset_controller_test.go
  • internal/object-controller/controllers/resolve_ref_test.go
  • internal/object-controller/controllers/revision_engine_factory.go
  • internal/object-controller/controllers/revision_engine_factory_test.go
  • internal/operator-controller/applier/boxcutter.go
  • internal/operator-controller/applier/boxcutter_test.go
  • internal/operator-controller/controllers/boxcutter_reconcile_steps.go
  • internal/operator-controller/controllers/boxcutter_reconcile_steps_internal_test.go
  • internal/operator-controller/controllers/boxcutter_reconcile_steps_test.go
  • internal/operator-controller/controllers/clusterextension_controller.go
  • internal/operator-controller/controllers/clusterextension_controller_internal_test.go
  • internal/shared/clusterobjectset/group.go
  • internal/shared/clusterobjectset/group_test.go
  • manifests/experimental-e2e.yaml
  • manifests/experimental.yaml
  • test/e2e/features/group.feature
  • test/e2e/features/install.feature
  • test/e2e/features/revision.feature
  • test/e2e/features/uninstall.feature
  • test/e2e/features/update.feature
  • test/e2e/steps/group_steps.go
  • test/e2e/steps/hooks.go
  • test/e2e/steps/steps.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.


📝 Walkthrough

Walkthrough

ClusterObjectSet now requires an immutable, validated spec.group. Object and operator controllers use that group to index and select revisions, route watches, and set server-side apply field managers. End-to-end scenarios cover group-scoped revisions, updates, pruning, and removal.

Changes

ClusterObjectSet group scoping

Layer / File(s) Summary
Define the ClusterObjectSet group contract
api/v1/clusterobjectset_types.go, api/v1/clusterobjectset_types_test.go, api/v1/validation_test.go, applyconfigurations/api/v1/clusterobjectsetspec.go, applyconfigurations/internal/internal.go, internal/shared/clusterobjectset/*, helm/olmv1/base/object-controller/crd/experimental/*, manifests/experimental*.yaml
The API and CRD schemas require an immutable group with lowercase-letter, digit, and hyphen constraints and a 52-character maximum. The apply configuration and shared group-index extractor support the field.
Scope object-controller revisions by group
cmd/object-controller/main.go, cmd/object-controller/main_test.go, internal/object-controller/controllers/clusterobjectset_controller.go, internal/object-controller/controllers/clusterobjectset_controller_*test.go, internal/object-controller/controllers/resolve_ref_test.go, internal/object-controller/controllers/revision_engine_factory.go, internal/object-controller/controllers/revision_engine_factory_test.go
The object controller indexes and selects revisions by group rather than owner-name label. The revision engine uses cos-group/<group> as the apply field manager.
Create and reconcile extension-group revisions
cmd/operator-controller/main.go, internal/operator-controller/applier/boxcutter.go, internal/operator-controller/applier/boxcutter_test.go, internal/operator-controller/controllers/boxcutter_reconcile_steps*.go, internal/operator-controller/controllers/clusterextension_controller.go, internal/operator-controller/controllers/clusterextension_controller_internal_test.go
Boxcutter assigns the extension name as the group and uses it to find revisions. Revision-state lookup and ClusterObjectSet watch mapping also use the group.
Verify group behavior in end-to-end scenarios
test/e2e/features/group.feature, test/e2e/features/install.feature, test/e2e/features/revision.feature, test/e2e/features/uninstall.feature, test/e2e/features/update.feature, test/e2e/steps/group_steps.go, test/e2e/steps/hooks.go, test/e2e/steps/steps.go
End-to-end coverage checks group assignment and selection, revision pruning, apply managers, ownership, and removal of referred Secrets.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant ClusterExtension
  participant Boxcutter
  participant KubernetesAPI
  participant RevisionEngine
  participant PhaseObjects
  ClusterExtension->>Boxcutter: Reconcile extension
  Boxcutter->>KubernetesAPI: Create ClusterObjectSet with group
  Boxcutter->>KubernetesAPI: List revisions by spec.group
  KubernetesAPI-->>Boxcutter: Return matching revisions
  Boxcutter->>RevisionEngine: Apply revision
  RevisionEngine->>PhaseObjects: Apply with cos-group/group manager
Loading

Suggested reviewers: perdasilva

Merge Risk: ⚪ Minimal · up to b4e2c

No concrete issue remains that should block merging after normal checks.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 50 functions across 26 files. (8 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the addition of spec.group and its role in ClusterObjectSet revision tracking. It is concise and uses the required warning icon.
Description check ✅ Passed The description explains the change, its motivation, API impact, validation, and scope. It includes the reviewer checklist and identifies Jira issue OPRUN-4739, though the checklist remains unchecked …
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 18.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 50 functions across 26 files. (8 skipped: 8 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@openshift-ci openshift-ci Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Oct 2, 2026
Require an immutable group identifier for revision tracking and display
it in the Group printer column.

Signed-off-by: Francesco Giudici <fgiudici@redhat.com>
Regenerate apply configurations and experimental CRDs and manifests from
the Group API specification.

Signed-off-by: Francesco Giudici <fgiudici@redhat.com>
Populate and index spec.group, use group-scoped revision queries and
watches, and share SSA field ownership across revisions while retaining
the OLM metadata prefix. Cover admission, cache isolation, event mapping,
migration ownership, and SSA behavior.

Signed-off-by: Francesco Giudici <fgiudici@redhat.com>
Check generated groups, shared SSA ownership during rollout, pruning
isolation, and cascade cleanup. Update direct COS fixtures and query
revision associations by spec.group.

Signed-off-by: Francesco Giudici <fgiudici@redhat.com>
@fgiudici
fgiudici force-pushed the oprun-4739-spec-group branch from c052700 to b4e2c87 Compare October 5, 2026 09:34
@openshift-ci openshift-ci Bot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Oct 5, 2026
Comment on lines +42 to +43
// group identifies the ClusterExtension whose revisions belong together.
// All revisions for the same ClusterExtension must use its name as their group.

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.

Do not mention ClusterExtension anywhere in the docs for the ClusterObjectDeployment or ClusterObjectSet APIs. These APIs are standalone and separate from ClusterExtension.

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.

// +kubebuilder:validation:MaxLength=52
// +kubebuilder:validation:XValidation:rule=`self.matches('^[a-z]([a-z0-9-]*[a-z0-9])?$')`,message="group must start with a lowercase letter, contain only lowercase letters, digits or hyphens, and end with a letter or digit"
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="group is immutable"
Group string `json:"group,omitempty"`

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.

Since this field is required, no need for omitempty:

Suggested change
Group string `json:"group,omitempty"`
Group string `json:"group"`

// +optional
Spec ClusterObjectSetSpec `json:"spec,omitempty"`
// +required
Spec ClusterObjectSetSpec `json:"spec,omitzero"`

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.

No need for omitzero since it is required.

Suggested change
Spec ClusterObjectSetSpec `json:"spec,omitzero"`
Spec ClusterObjectSetSpec `json:"spec"`

Comment on lines +27 to +29
LifecycleState: ClusterObjectSetLifecycleStateActive,
Revision: 1,
CollisionProtection: CollisionProtectionPrevent,

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.

Technically this is an invalid spec since group is required, right? We should test the real world scenarios of:

  1. groupA -> groupB fails
  2. groupA -> groupA succeeds
  3. groupA -> `` fails (immutable AND required)

Comment on lines +36 to +39
spec: ClusterObjectSetSpec{
LifecycleState: ClusterObjectSetLifecycleStateActive,
Revision: 1,
CollisionProtection: CollisionProtectionPrevent,

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.

Same here. We should set the group in the "from" spec.

Comment on lines +399 to +401
{name: "missing spec", omitSpec: true},
{name: "missing group"},
{name: "empty group", group: ptr.To("")},

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.

Include explicit valid: false to make the test cases more readable.

omitSpec bool
valid bool
}{
{name: "missing spec", omitSpec: true},

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.

This is a test of the spec itself, not of the group. Move this to a separate set of tests, e.g. in TestClusterObjectSetSpecValidation

},
{
name: "should return empty list when owner label missing",
name: "should include revisions when owner label is missing",

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.

Owner label is just not a thing anymore for the ClusterObjectSetReconciler, right? So no need for any tests that mention anything about an owner label?

},
{
name: "should only include revisions matching owner label",
name: "should only include revisions in the same group",

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.

What is the actual grouping key with this change? Is it spec.group + controller: true ownerRef? Or is it just spec.group?

I think we should use the former. That would mean that if two different COSes have the same group, but different controller owners, they would be in different groups. And if two different COSes have the same group and do not have a controller owner at all, they would be in the same group.

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.

We should include tests for all of those scenarios.

@joelanford joelanford Oct 5, 2026 •

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.

Here's what I landed on as a helper package/type/functions for building up the sibling list from the raw set of ClusterObjectSet objects sharing a spec.group: https://github.com/joelanford/orb-operator/blob/main/internal/revision/chain.go

}
// Keep the field owner prefix unchanged so existing objects can be reconciled after migration.
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &ocv1.ClusterObjectSet{},
clusterobjectset.GroupField, clusterobjectset.ExtractGroup); err != nil {

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.

Seems like the GroupField and ExtractGroup functionality are internal details of the setup for the COS field indexer.

To keep things a bit cleaner, WDYT about a SetupIndexes function defined in the COS controller code that inlines the GroupField and ExtractGroup functions?

That would eliminate the entire shared/clusterobjectset package and result in the entire index setup being handled in one place.

Comment on lines +429 to +432
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &ocv1.ClusterObjectSet{},
clusterobjectset.GroupField, clusterobjectset.ExtractGroup); err != nil {
return fmt.Errorf("indexing ClusterObjectSet group: %w", err)
}

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.

Is this just temporary until we have ClusterObjectDeployment?

If so, I'm slightly worried that we're accruing code we will end up not needing/wanting with no easy way to come back around and identify things we don't need after ClusterObjectDeployment is in place.

Any ideas on how to make sure we keep a lid on this kind of thing?


var ctrlBuilderOpts []controllers.ControllerBuilderOption
if features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) {
ctrlBuilderOpts = append(ctrlBuilderOpts, controllers.WithOwns(&ocv1.ClusterObjectSet{}))

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.

It feels like we should be able to eventually fully abstract this into a SetupWithManager functions in the COD/COS packages, rather than putting details like this in main.go.

I know we're dealing with both Helm and Boxcutter runtimes right now, but I do wonder if we could do something like:

if boxcutter {
  ceController.SetupWithManagerForBoxcutter(mgr)
} else {
  ceController.SetupWithManagerForHelm(mgr)
}

Then we wouldn't need more low-level details like WithClusterObjectSetWatch leaking out into main.go.

WDYT?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants