From 73db0757359e4cb8189008a054d5c18acb37a26d Mon Sep 17 00:00:00 2001 From: Jan Fajerski Date: Tue, 10 Mar 2026 12:06:55 +0000 Subject: [PATCH] chore: migrate to controller-runtime client.Apply over client.Patch Signed-off-by: Jan Fajerski --- .../observability/reconcilers_test.go | 25 ++-------- pkg/reconciler/reconciler.go | 46 ++++++++++++++++++- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/pkg/controllers/observability/reconcilers_test.go b/pkg/controllers/observability/reconcilers_test.go index 14c2372a1..c03336462 100644 --- a/pkg/controllers/observability/reconcilers_test.go +++ b/pkg/controllers/observability/reconcilers_test.go @@ -37,13 +37,7 @@ func TestGetReconcilers(t *testing.T) { mockClient: func() *MockClient { mockClient := &MockClient{} mockClient.On("Get", context.Background(), mock.Anything, mock.IsType(&olmv1alpha1.Subscription{}), mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&corev1.Namespace{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&otelv1beta1.OpenTelemetryCollector{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&rbacv1.ClusterRole{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&rbacv1.ClusterRoleBinding{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&tempov1alpha1.TempoStack{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&corev1.Secret{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&uiv1alpha1.UIPlugin{}), mock.Anything, mock.Anything).Return(nil) + mockClient.On("Apply", context.Background(), mock.Anything, mock.Anything).Return(nil) return mockClient }, instance: &obsv1alpha1.ObservabilityInstaller{ @@ -70,14 +64,7 @@ func TestGetReconcilers(t *testing.T) { mockClient.On("Get", context.Background(), mock.Anything, mock.IsType(&olmv1alpha1.Subscription{}), mock.Anything).Return(nil) mockClient.On("Get", context.Background(), mock.Anything, mock.IsType(&corev1.Secret{}), mock.Anything).Return(nil) mockClient.On("Get", context.Background(), mock.Anything, mock.IsType(&corev1.ConfigMap{}), mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&corev1.Namespace{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&otelv1beta1.OpenTelemetryCollector{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&rbacv1.ClusterRole{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&rbacv1.ClusterRoleBinding{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&tempov1alpha1.TempoStack{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&corev1.Secret{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&corev1.ConfigMap{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&uiv1alpha1.UIPlugin{}), mock.Anything, mock.Anything).Return(nil) + mockClient.On("Apply", context.Background(), mock.Anything, mock.Anything).Return(nil) return mockClient }, instance: &obsv1alpha1.ObservabilityInstaller{ @@ -184,13 +171,7 @@ func TestGetReconcilers(t *testing.T) { name: "tracing capability enabled, subscription already installed", mockClient: func() *MockClient { mockClient := &MockClient{} - mockClient.On("Patch", context.Background(), mock.IsType(&corev1.Namespace{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&otelv1beta1.OpenTelemetryCollector{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&rbacv1.ClusterRole{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&rbacv1.ClusterRoleBinding{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&tempov1alpha1.TempoStack{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&corev1.Secret{}), mock.Anything, mock.Anything).Return(nil) - mockClient.On("Patch", context.Background(), mock.IsType(&uiv1alpha1.UIPlugin{}), mock.Anything, mock.Anything).Return(nil) + mockClient.On("Apply", context.Background(), mock.Anything, mock.Anything).Return(nil) return mockClient }, instance: &obsv1alpha1.ObservabilityInstaller{ diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index 3cd996292..bcd6c3899 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -2,6 +2,7 @@ package reconciler import ( "context" + "encoding/json" "fmt" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -48,8 +49,8 @@ func (r Updater) Reconcile(ctx context.Context, c client.Client, scheme *runtime } } - if err := c.Patch(ctx, r.resource, client.Apply, client.ForceOwnership, client.FieldOwner("observability-operator")); err != nil { - return fmt.Errorf("%s/%s (%s): updater failed to patch: %w", + if err := c.Apply(ctx, &clientObjectApplyConfig{obj: r.resource}, client.ForceOwnership, client.FieldOwner("observability-operator")); err != nil { + return fmt.Errorf("%s/%s (%s): updater failed to apply: %w", r.resource.GetNamespace(), r.resource.GetName(), r.resource.GetObjectKind().GroupVersionKind().String(), err) } @@ -123,3 +124,44 @@ func NewOptionalUnmanagedUpdater(r client.Object, c metav1.Object, cond bool) Re } return NewDeleter(r) } + +// clientObjectApplyConfig wraps a client.Object so it satisfies runtime.ApplyConfiguration, +// allowing Updater to use client.Client.Apply() instead of the deprecated +// client.Client.Patch(..., client.Apply, ...) path. +// +// The object is held as a plain field (not embedded) so the wrapper does NOT +// satisfy runtime.Object. Without this, the typed client's type-switch would +// hit the runtime.Object branch and try to look up *clientObjectApplyConfig +// in the scheme, causing "no kind is registered" errors at runtime. +// +// Serialisation is identical to the old applyPatch path: apply.NewRequest +// calls json.Marshal on this wrapper, which delegates to the underlying object. +type clientObjectApplyConfig struct { + obj client.Object +} + +func (a *clientObjectApplyConfig) IsApplyConfiguration() {} + +func (a *clientObjectApplyConfig) GetName() *string { + n := a.obj.GetName() + return &n +} + +func (a *clientObjectApplyConfig) GetNamespace() *string { + ns := a.obj.GetNamespace() + return &ns +} + +func (a *clientObjectApplyConfig) GetKind() *string { + k := a.obj.GetObjectKind().GroupVersionKind().Kind + return &k +} + +func (a *clientObjectApplyConfig) GetAPIVersion() *string { + av := a.obj.GetObjectKind().GroupVersionKind().GroupVersion().String() + return &av +} + +func (a *clientObjectApplyConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(a.obj) +}