Skip to content

Conversation

@tmshort
Copy link
Contributor

@tmshort tmshort commented Nov 12, 2025

Refactor all controllers to use server-side-apply instead of Update() when adding or removing finalizers to improve performance, and to avoid removing non-cached fields erroneously. Create shared finalizer utilities to eliminate code duplication across controllers.

This is necesary because we no longer cache thelast-applied-configuration annotation, so when we add/remove the finalizers, we are removing that field from the metadata. This causes issues with clients when they don't see that annotation (e.g. apply the same ClusterExtension twice).

  • Add shared finalizer.EnsureFinalizer() and RemoveFinalizer() utilities
  • Update ClusterCatalog, ClusterExtension, and ClusterExtensionRevision
    controllers to use Patch-based finalizer management
  • Maintain early return behavior after adding finalizers on create
  • Remove unused internal/operator-controller/finalizers package
  • Update all unit tests to match new behavior

🤖 Generated with Claude Code

Co-Authored-By: Claude [email protected]

Description

Reviewer Checklist

  • API Go Documentation
  • Tests: Unit Tests (and E2E Tests, if appropriate)
  • Comprehensive Commit Messages
  • Links to related GitHub Issue(s)

@tmshort tmshort requested a review from a team as a code owner November 12, 2025 15:54
@openshift-ci
Copy link

openshift-ci bot commented Nov 12, 2025

[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 kevinrizza for approval. For more information see the Code Review Process.

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

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
Copy link

netlify bot commented Nov 12, 2025

Deploy Preview for olmv1 ready!

Name Link
🔨 Latest commit bfb7a16
🔍 Latest deploy log https://app.netlify.com/projects/olmv1/deploys/691609e8f8891c0008b252ac
😎 Deploy Preview https://deploy-preview-2328--olmv1.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

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

@tmshort
Copy link
Contributor Author

tmshort commented Nov 12, 2025

The ClusterExtension and ClusterCatalog finalizer code used Update, the ClusterExtensionRevision used Apply, so the CE and CE finalizer code was rewritten to be like CER. There is common finalizer code between them now.

@tmshort
Copy link
Contributor Author

tmshort commented Nov 12, 2025

Ping @joelanford

@tmshort
Copy link
Contributor Author

tmshort commented Nov 12, 2025

catalogd pod is panicing... it only fails due to the summary generated, it's not "noticeable" during a regular run!

@pedjak
Copy link
Contributor

pedjak commented Nov 12, 2025

This is necesary because we no longer cache thelast-applied-configuration annotation, so when we add/remove the finalizers, we are removing that field from the metadata. This causes issues with clients when they don't see that annotation (e.g. apply the same ClusterExtension twice).

If we want to remove conflicts, we should use patch, but adding/removing finalizer we do not touch removed fields (including the annotation). Controller-runtime cache is write-through cache, so an update or patch operation does not perform any direct modification on the cache - all changes are made by the informer.

In what situations did we see conflicts, given that only single controller is responsible for a resource type?

@tmshort
Copy link
Contributor Author

tmshort commented Nov 12, 2025

If we want to remove conflicts, we should use patch, but adding/removing finalizer we do not touch removed fields (including the annotation). Controller-runtime cache is write-through cache, so an update or patch operation does not perform any direct modification on the cache - all changes are made by the informer.

In what situations did we see conflicts, given that only single controller is responsible for a resource type?

This is not a conflict issue. The problem was that we are using Update(), which uses the cached version of the resource to make the changes. The cached version no longer includes the last-applied-configuration annotation, so that annotation is removed on the Update().

This causes a problem with e.g. kubectl applying a ClusterExtension a second time. If you apply a CE twice without this code (i.e. current main branch), you will see the following:

tshort@cube:~/.../config/samples (main %=)$ kubectl apply -f ce.yaml
clusterextension.olm.operatorframework.io/argocd created
tshort@cube:~/.../config/samples (main %=)$ oc get clusterextension -o yaml
apiVersion: v1
items:
- apiVersion: olm.operatorframework.io/v1
  kind: ClusterExtension
  metadata:
    creationTimestamp: "2025-11-12T15:22:25Z"
    finalizers:
    - olm.operatorframework.io/cleanup-unpack-cache
    - olm.operatorframework.io/cleanup-contentmanager-cache
    generation: 1
    name: argocd
    resourceVersion: "1296"
    uid: b096377d-577e-4d25-8f3d-b0e0b12fe894
...
tshort@cube:~/.../config/samples (main %=)$ kubectl apply -f ce.yaml
Warning: resource clusterextensions/argocd is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
clusterextension.olm.operatorframework.io/argocd configured
tshort@cube:~/.../config/samples (main %=)$

The problem is that the last-applied-configuration annotation is not present. Thus, causing the second kubectl apply command to complain. The expected result is that the second kubectl apply states clusterextension.olm.operatorframework.io/argocd unchanged

@codecov
Copy link

codecov bot commented Nov 12, 2025

Codecov Report

❌ Patch coverage is 72.09302% with 36 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.18%. Comparing base (35e38aa) to head (bfb7a16).

Files with missing lines Patch % Lines
...troller/controllers/clusterextension_controller.go 57.14% 7 Missing and 5 partials ⚠️
internal/shared/util/finalizer/finalizer.go 78.18% 6 Missing and 6 partials ⚠️
...logd/controllers/core/clustercatalog_controller.go 60.00% 5 Missing and 5 partials ⚠️
...controllers/clusterextensionrevision_controller.go 66.66% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2328      +/-   ##
==========================================
- Coverage   74.30%   74.18%   -0.13%     
==========================================
  Files          91       91              
  Lines        7083     7108      +25     
==========================================
+ Hits         5263     5273      +10     
- Misses       1405     1412       +7     
- Partials      415      423       +8     
Flag Coverage Δ
e2e 45.98% <56.58%> (+0.28%) ⬆️
experimental-e2e 48.41% <61.24%> (+0.01%) ⬆️
unit 58.08% <20.15%> (-0.52%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

// If the finalizer already exists, this is a no-op and returns (false, nil).
// Returns (true, nil) if the finalizer was added.
// Note: This function will update the passed object with the server response.
func EnsureFinalizer(ctx context.Context, c client.Client, obj client.Object, finalizer string) (bool, error) {
Copy link
Member

Choose a reason for hiding this comment

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

If we want to add multiple finalizers, it will mean multiple Patch calls to the apiserver as this is written. Perhaps we should have this function accept multiple finalizers so that we can limit the number of patch calls (and watch events produced/received by apiserver/informers)?

currentFinalizers := obj.GetFinalizers()
newFinalizers := make([]string, len(currentFinalizers), len(currentFinalizers)+1)
copy(newFinalizers, currentFinalizers)
newFinalizers = append(newFinalizers, finalizer)
Copy link
Member

Choose a reason for hiding this comment

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

Sort the new finalizers to ensure deterministic ordering?

return false, fmt.Errorf("marshalling patch to add finalizer: %w", err)
}
// Note: Patch will update obj with the server response, including ResourceVersion
if err := c.Patch(ctx, obj, client.RawPatch(types.MergePatchType, patchJSON)); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

Should we use SSA here?

Copy link
Contributor

Choose a reason for hiding this comment

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

that could be a good improvement, that would simplify the reconcile logic a lot, i.e. we do not need to care about appending/sorting finalizers at all.


// RemoveFinalizer removes a finalizer from the object using Patch.
// If the finalizer doesn't exist, this is a no-op.
func RemoveFinalizer(ctx context.Context, c client.Client, obj client.Object, finalizer string) error {
Copy link
Member

Choose a reason for hiding this comment

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

Same comment here: re accept multiple finalizers to avoid multiple patch calls?

Comment on lines 70 to 75
newFinalizers := make([]string, 0, len(currentFinalizers))
for _, f := range currentFinalizers {
if f != finalizer {
newFinalizers = append(newFinalizers, f)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

newFinalizers := slices.Delete(slices.Clone(currentFinalizers), finalizer)?

Copy link
Member

Choose a reason for hiding this comment

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

Or if we reorganize to accept multiple finalizers, then slices.DeleteFunc could be more appropriate (e.g. by building a set of finalizers to remove and then checking set presence in the predicate function).

@pedjak
Copy link
Contributor

pedjak commented Nov 13, 2025

This is not a conflict issue. The problem was that we are using Update(), which uses the cached version of the resource to make the changes. The cached version no longer includes the last-applied-configuration annotation, so that annotation is removed on the Update().

Thanks for explaining it, make sense. Given that I think we should update PR description to reflect that, currently we have:

Refactor all controllers to use client.Patch() instead of Update() when adding or removing finalizers to reduce conflicts and improve performance.

Given the explanation above, we are not reducing conflict or improving performances - we are ensuring correctness, i.e. not removing the annotation.

Refactor all controllers to use server-side-apply instead of Update()
when adding or removing finalizers to improve performance, and to avoid
removing non-cached fields erroneously. Create shared finalizer utilities
to eliminate code duplication across controllers.

This is necesary because we no longer cache the`last-applied-configuration`
annotation, so when we add/remove the finalizers, we are removing that field
from the metadata. This causes issues with clients when they don't see that
annotation (e.g. apply the same ClusterExtension twice).

- Add shared finalizer.EnsureFinalizer() and RemoveFinalizer() utilities
- Update ClusterCatalog, ClusterExtension, and ClusterExtensionRevision
  controllers to use Patch-based finalizer management
- Maintain early return behavior after adding finalizers on create
- Remove unused internal/operator-controller/finalizers package
- Update all unit tests to match new behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Signed-off-by: Todd Short <[email protected]>
@tmshort tmshort changed the title 🐛 Use Patch instead of Update for finalizer operations 🐛 Use SSA instead of Update for finalizer operations Nov 13, 2025
@tmshort tmshort requested review from joelanford and pedjak November 13, 2025 16:41
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.

3 participants