Skip to content

Commit 24cf078

Browse files
Merge pull request openshift#572 from openshift-bot/synchronize-upstream
NO-ISSUE: Synchronize From Upstream Repositories
2 parents 93bf7ab + ca1c967 commit 24cf078

File tree

9 files changed

+406
-26
lines changed

9 files changed

+406
-26
lines changed

cmd/catalogd/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import (
5959
"github.com/operator-framework/operator-controller/internal/catalogd/storage"
6060
"github.com/operator-framework/operator-controller/internal/catalogd/webhook"
6161
sharedcontrollers "github.com/operator-framework/operator-controller/internal/shared/controllers"
62+
cacheutil "github.com/operator-framework/operator-controller/internal/shared/util/cache"
6263
fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs"
6364
httputil "github.com/operator-framework/operator-controller/internal/shared/util/http"
6465
imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image"
@@ -254,6 +255,8 @@ func run(ctx context.Context) error {
254255

255256
cacheOptions := crcache.Options{
256257
ByObject: map[client.Object]crcache.ByObject{},
258+
// Memory optimization: strip managed fields and large annotations from cached objects
259+
DefaultTransform: cacheutil.StripManagedFieldsAndAnnotations(),
257260
}
258261

259262
saKey, err := sautil.GetServiceAccount()

cmd/operator-controller/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import (
7878
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render/registryv1"
7979
"github.com/operator-framework/operator-controller/internal/operator-controller/scheme"
8080
sharedcontrollers "github.com/operator-framework/operator-controller/internal/shared/controllers"
81+
cacheutil "github.com/operator-framework/operator-controller/internal/shared/util/cache"
8182
fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs"
8283
httputil "github.com/operator-framework/operator-controller/internal/shared/util/http"
8384
imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image"
@@ -257,6 +258,8 @@ func run() error {
257258
cfg.systemNamespace: {LabelSelector: k8slabels.Everything()},
258259
},
259260
DefaultLabelSelector: k8slabels.Nothing(),
261+
// Memory optimization: strip managed fields and large annotations from cached objects
262+
DefaultTransform: cacheutil.StripAnnotations(),
260263
}
261264

262265
if features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) {

commitchecker.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
expectedMergeBase: 34394ce0a7067e1f1622dc2c381a9a12439b10d2
1+
expectedMergeBase: 4e349e62c5314574f6194d64b1ff4508f2e9331f
22
upstreamBranch: main
33
upstreamOrg: operator-framework
44
upstreamRepo: operator-controller

internal/catalogd/controllers/core/clustercatalog_controller.go

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
ocv1 "github.com/operator-framework/operator-controller/api/v1"
4242
"github.com/operator-framework/operator-controller/internal/catalogd/storage"
4343
imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image"
44+
k8sutil "github.com/operator-framework/operator-controller/internal/shared/util/k8s"
4445
)
4546

4647
const (
@@ -107,16 +108,16 @@ func (r *ClusterCatalogReconciler) Reconcile(ctx context.Context, req ctrl.Reque
107108
// Do checks before any Update()s, as Update() may modify the resource structure!
108109
updateStatus := !equality.Semantic.DeepEqual(existingCatsrc.Status, reconciledCatsrc.Status)
109110
updateFinalizers := !equality.Semantic.DeepEqual(existingCatsrc.Finalizers, reconciledCatsrc.Finalizers)
110-
unexpectedFieldsChanged := checkForUnexpectedFieldChange(existingCatsrc, *reconciledCatsrc)
111+
unexpectedFieldsChanged := k8sutil.CheckForUnexpectedFieldChange(&existingCatsrc, reconciledCatsrc)
111112

112113
if unexpectedFieldsChanged {
113114
panic("spec or metadata changed by reconciler")
114115
}
115116

116117
// Save the finalizers off to the side. If we update the status, the reconciledCatsrc will be updated
117118
// to contain the new state of the ClusterCatalog, which contains the status update, but (critically)
118-
// does not contain the finalizers. After the status update, we need to re-add the finalizers to the
119-
// reconciledCatsrc before updating the object.
119+
// does not contain the finalizers. After the status update, we will use the saved finalizers in the
120+
// CreateOrPatch()
120121
finalizers := reconciledCatsrc.Finalizers
121122

122123
if updateStatus {
@@ -125,10 +126,12 @@ func (r *ClusterCatalogReconciler) Reconcile(ctx context.Context, req ctrl.Reque
125126
}
126127
}
127128

128-
reconciledCatsrc.Finalizers = finalizers
129-
130129
if updateFinalizers {
131-
if err := r.Update(ctx, reconciledCatsrc); err != nil {
130+
// Use CreateOrPatch to update finalizers on the server
131+
if _, err := controllerutil.CreateOrPatch(ctx, r.Client, reconciledCatsrc, func() error {
132+
reconciledCatsrc.Finalizers = finalizers
133+
return nil
134+
}); err != nil {
132135
reconcileErr = errors.Join(reconcileErr, fmt.Errorf("error updating finalizers: %v", err))
133136
}
134137
}
@@ -415,13 +418,6 @@ func (r *ClusterCatalogReconciler) needsPoll(lastSuccessfulPoll time.Time, catal
415418
return nextPoll.Before(time.Now())
416419
}
417420

418-
// Compare resources - ignoring status & metadata.finalizers
419-
func checkForUnexpectedFieldChange(a, b ocv1.ClusterCatalog) bool {
420-
a.Status, b.Status = ocv1.ClusterCatalogStatus{}, ocv1.ClusterCatalogStatus{}
421-
a.Finalizers, b.Finalizers = []string{}, []string{}
422-
return !equality.Semantic.DeepEqual(a, b)
423-
}
424-
425421
type finalizerFunc func(ctx context.Context, obj client.Object) (crfinalizer.Result, error)
426422

427423
func (f finalizerFunc) Finalize(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {

internal/operator-controller/applier/boxcutter.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
ocv1 "github.com/operator-framework/operator-controller/api/v1"
2929
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
30+
"github.com/operator-framework/operator-controller/internal/shared/util/cache"
3031
)
3132

3233
const (
@@ -66,6 +67,9 @@ func (r *SimpleRevisionGenerator) GenerateRevisionFromHelmRelease(
6667
maps.Copy(labels, objectLabels)
6768
obj.SetLabels(labels)
6869

70+
// Memory optimization: strip large annotations
71+
// Note: ApplyStripTransform never returns an error in practice
72+
_ = cache.ApplyStripAnnotationsTransform(&obj)
6973
sanitizedUnstructured(ctx, &obj)
7074

7175
objs = append(objs, ocv1.ClusterExtensionRevisionObject{
@@ -117,6 +121,10 @@ func (r *SimpleRevisionGenerator) GenerateRevision(
117121
unstr := unstructured.Unstructured{Object: unstrObj}
118122
unstr.SetGroupVersionKind(gvk)
119123

124+
// Memory optimization: strip large annotations
125+
if err := cache.ApplyStripAnnotationsTransform(&unstr); err != nil {
126+
return nil, err
127+
}
120128
sanitizedUnstructured(ctx, &unstr)
121129

122130
objs = append(objs, ocv1.ClusterExtensionRevisionObject{

internal/operator-controller/controllers/clusterextension_controller.go

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"sigs.k8s.io/controller-runtime/pkg/builder"
3636
"sigs.k8s.io/controller-runtime/pkg/client"
3737
crcontroller "sigs.k8s.io/controller-runtime/pkg/controller"
38+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3839
"sigs.k8s.io/controller-runtime/pkg/event"
3940
crhandler "sigs.k8s.io/controller-runtime/pkg/handler"
4041
"sigs.k8s.io/controller-runtime/pkg/log"
@@ -48,6 +49,7 @@ import (
4849
ocv1 "github.com/operator-framework/operator-controller/api/v1"
4950
"github.com/operator-framework/operator-controller/internal/operator-controller/conditionsets"
5051
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
52+
k8sutil "github.com/operator-framework/operator-controller/internal/shared/util/k8s"
5153
)
5254

5355
const (
@@ -135,25 +137,28 @@ func (r *ClusterExtensionReconciler) Reconcile(ctx context.Context, req ctrl.Req
135137
updateFinalizers := !equality.Semantic.DeepEqual(existingExt.Finalizers, reconciledExt.Finalizers)
136138

137139
// If any unexpected fields have changed, panic before updating the resource
138-
unexpectedFieldsChanged := checkForUnexpectedClusterExtensionFieldChange(*existingExt, *reconciledExt)
140+
unexpectedFieldsChanged := k8sutil.CheckForUnexpectedFieldChange(existingExt, reconciledExt)
139141
if unexpectedFieldsChanged {
140142
panic("spec or metadata changed by reconciler")
141143
}
142144

143145
// Save the finalizers off to the side. If we update the status, the reconciledExt will be updated
144146
// to contain the new state of the ClusterExtension, which contains the status update, but (critically)
145-
// does not contain the finalizers. After the status update, we need to re-add the finalizers to the
146-
// reconciledExt before updating the object.
147+
// does not contain the finalizers. After the status update, we will use the saved finalizers in the
148+
// CreateOrPatch()
147149
finalizers := reconciledExt.Finalizers
148150
if updateStatus {
149151
if err := r.Client.Status().Update(ctx, reconciledExt); err != nil {
150152
reconcileErr = errors.Join(reconcileErr, fmt.Errorf("error updating status: %v", err))
151153
}
152154
}
153-
reconciledExt.Finalizers = finalizers
154155

155156
if updateFinalizers {
156-
if err := r.Update(ctx, reconciledExt); err != nil {
157+
// Use CreateOrPatch to update finalizers on the server
158+
if _, err := controllerutil.CreateOrPatch(ctx, r.Client, reconciledExt, func() error {
159+
reconciledExt.Finalizers = finalizers
160+
return nil
161+
}); err != nil {
157162
reconcileErr = errors.Join(reconcileErr, fmt.Errorf("error updating finalizers: %v", err))
158163
}
159164
}
@@ -179,13 +184,6 @@ func ensureAllConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.C
179184
}
180185
}
181186

182-
// Compare resources - ignoring status & metadata.finalizers
183-
func checkForUnexpectedClusterExtensionFieldChange(a, b ocv1.ClusterExtension) bool {
184-
a.Status, b.Status = ocv1.ClusterExtensionStatus{}, ocv1.ClusterExtensionStatus{}
185-
a.Finalizers, b.Finalizers = []string{}, []string{}
186-
return !equality.Semantic.DeepEqual(a, b)
187-
}
188-
189187
// SetDeprecationStatus will set the appropriate deprecation statuses for a ClusterExtension
190188
// based on the provided bundle
191189
func SetDeprecationStatus(ext *ocv1.ClusterExtension, bundleName string, deprecation *declcfg.Deprecation) {
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package cache
2+
3+
import (
4+
"maps"
5+
6+
toolscache "k8s.io/client-go/tools/cache"
7+
crcache "sigs.k8s.io/controller-runtime/pkg/cache"
8+
"sigs.k8s.io/controller-runtime/pkg/client"
9+
)
10+
11+
// stripAnnotations removes memory-heavy annotations that aren't needed for controller operations.
12+
func stripAnnotations(obj interface{}) (interface{}, error) {
13+
if metaObj, ok := obj.(client.Object); ok {
14+
// Remove the last-applied-configuration annotation which can be very large
15+
// Clone the annotations map to avoid modifying shared references
16+
annotations := metaObj.GetAnnotations()
17+
if annotations != nil {
18+
annotations = maps.Clone(annotations)
19+
delete(annotations, "kubectl.kubernetes.io/last-applied-configuration")
20+
if len(annotations) == 0 {
21+
metaObj.SetAnnotations(nil)
22+
} else {
23+
metaObj.SetAnnotations(annotations)
24+
}
25+
}
26+
}
27+
return obj, nil
28+
}
29+
30+
// StripManagedFieldsAndAnnotations returns a cache transform function that removes
31+
// memory-heavy fields that aren't needed for controller operations.
32+
// This significantly reduces memory usage in informer caches by removing:
33+
// - Managed fields (can be several KB per object)
34+
// - kubectl.kubernetes.io/last-applied-configuration annotation (can be very large)
35+
//
36+
// Use this function as a DefaultTransform in controller-runtime cache.Options
37+
// to reduce memory overhead across all cached objects.
38+
//
39+
// Example:
40+
//
41+
// cacheOptions := cache.Options{
42+
// DefaultTransform: cacheutil.StripManagedFieldsAndAnnotations(),
43+
// }
44+
func StripManagedFieldsAndAnnotations() toolscache.TransformFunc {
45+
// Use controller-runtime's built-in TransformStripManagedFields and compose it
46+
// with our custom annotation stripping transform
47+
managedFieldsTransform := crcache.TransformStripManagedFields()
48+
49+
return func(obj interface{}) (interface{}, error) {
50+
// First strip managed fields using controller-runtime's transform
51+
obj, err := managedFieldsTransform(obj)
52+
if err != nil {
53+
return obj, err
54+
}
55+
56+
// Then strip the large annotations
57+
return stripAnnotations(obj)
58+
}
59+
}
60+
61+
// StripAnnotations returns a cache transform function that removes
62+
// memory-heavy annotation fields that aren't needed for controller operations.
63+
// This significantly reduces memory usage in informer caches by removing:
64+
// - kubectl.kubernetes.io/last-applied-configuration annotation (can be very large)
65+
//
66+
// Use this function as a DefaultTransform in controller-runtime cache.Options
67+
// to reduce memory overhead across all cached objects.
68+
//
69+
// Example:
70+
//
71+
// cacheOptions := cache.Options{
72+
// DefaultTransform: cacheutil.StripAnnotations(),
73+
// }
74+
func StripAnnotations() toolscache.TransformFunc {
75+
return func(obj interface{}) (interface{}, error) {
76+
// Strip the large annotations
77+
return stripAnnotations(obj)
78+
}
79+
}
80+
81+
// ApplyStripAnnotationsTransform applies the strip transform directly to an object.
82+
// This is a convenience function for cases where you need to strip fields
83+
// from an object outside of the cache transform context.
84+
//
85+
// Note: This function never returns an error in practice, but returns error
86+
// to satisfy the TransformFunc interface.
87+
func ApplyStripAnnotationsTransform(obj client.Object) error {
88+
transform := StripAnnotations()
89+
_, err := transform(obj)
90+
return err
91+
}

internal/shared/util/k8s/k8s.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package k8s
2+
3+
import (
4+
"reflect"
5+
6+
"k8s.io/apimachinery/pkg/api/equality"
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
)
9+
10+
// CheckForUnexpectedFieldChange compares two Kubernetes objects and returns true
11+
// if their annotations, labels, or spec have changed. This is useful for detecting
12+
// unexpected modifications during reconciliation.
13+
//
14+
// The function compares:
15+
// - Annotations (via GetAnnotations)
16+
// - Labels (via GetLabels)
17+
// - Spec (using reflection to access the Spec field, with semantic equality)
18+
//
19+
// Status and finalizers are intentionally not compared, as these are expected
20+
// to change during reconciliation.
21+
//
22+
// This function uses reflection to access the Spec field, so no explicit GetSpec()
23+
// method is required. The objects must have a field named "Spec".
24+
func CheckForUnexpectedFieldChange(a, b metav1.Object) bool {
25+
if !equality.Semantic.DeepEqual(a.GetAnnotations(), b.GetAnnotations()) {
26+
return true
27+
}
28+
if !equality.Semantic.DeepEqual(a.GetLabels(), b.GetLabels()) {
29+
return true
30+
}
31+
32+
// Use reflection to access the Spec field
33+
aVal := reflect.ValueOf(a)
34+
bVal := reflect.ValueOf(b)
35+
36+
// Handle pointer types
37+
if aVal.Kind() == reflect.Ptr {
38+
aVal = aVal.Elem()
39+
}
40+
if bVal.Kind() == reflect.Ptr {
41+
bVal = bVal.Elem()
42+
}
43+
44+
// Get the Spec field from both objects
45+
aSpec := aVal.FieldByName("Spec")
46+
bSpec := bVal.FieldByName("Spec")
47+
48+
// If either Spec field is invalid, return false (no change detected)
49+
if !aSpec.IsValid() || !bSpec.IsValid() {
50+
return false
51+
}
52+
53+
return !equality.Semantic.DeepEqual(aSpec.Interface(), bSpec.Interface())
54+
}

0 commit comments

Comments
 (0)