Skip to content

Commit 14110ae

Browse files
committed
Fix Secret metadata reconciliation to preserve cert-controller-managed TLS data
Signed-off-by: chiragkyal <ckyal@redhat.com>
1 parent 5525eaf commit 14110ae

2 files changed

Lines changed: 63 additions & 10 deletions

File tree

pkg/controller/external_secrets/secret.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55

66
corev1 "k8s.io/api/core/v1"
7+
apierrors "k8s.io/apimachinery/pkg/api/errors"
78
"sigs.k8s.io/controller-runtime/pkg/client"
89

910
operatorv1alpha1 "github.com/openshift/external-secrets-operator/api/v1alpha1"
@@ -33,20 +34,32 @@ func (r *Reconciler) createOrApplySecret(esc *operatorv1alpha1.ExternalSecretsCo
3334
}
3435

3536
if exist && common.ObjectMetadataModified(desired, fetched, &resourceMetadata) {
36-
r.log.V(1).Info("secret has been modified, updating to desired state", "name", secretName)
37-
common.RemoveObsoleteAnnotations(desired, resourceMetadata)
38-
if err := r.UpdateWithRetry(r.ctx, desired); err != nil {
39-
return common.FromClientError(err, "failed to update %s secret resource", secretName)
37+
r.log.V(1).Info("secret has been modified, patching metadata to desired state", "name", secretName)
38+
if err := r.patchResourceMetadata(desired, resourceMetadata); err != nil {
39+
return common.FromClientError(err, "failed to patch %s secret resource metadata", secretName)
4040
}
4141
r.eventRecorder.Eventf(esc, corev1.EventTypeNormal, "Reconciled", "secret resource %s reconciled back to desired state", secretName)
4242
} else {
4343
r.log.V(4).Info("secret resource already exists and is in expected state", "name", secretName)
4444
}
4545

4646
if !exist {
47-
if err := r.createWithFallback(desired, resourceMetadata, secretName, esc); err != nil {
48-
return err
47+
// NOTE: This Secret cannot use the generic createWithFallback helper because
48+
// its Data field is managed by the external-secrets cert-controller, which injects
49+
// TLS content at runtime. On AlreadyExists we use a MergePatch that touches only
50+
// metadata, leaving cert-controller-managed TLS certificates untouched.
51+
if err := r.Create(r.ctx, desired); err != nil {
52+
if !apierrors.IsAlreadyExists(err) {
53+
return common.FromClientError(err, "failed to create %s secret resource", secretName)
54+
}
55+
r.log.V(1).Info("secret exists on API server but absent from label-filtered cache, patching metadata", "name", secretName)
56+
if patchErr := r.patchResourceMetadata(desired, resourceMetadata); patchErr != nil {
57+
return common.FromClientError(patchErr, "failed to patch %s secret resource metadata", secretName)
58+
}
59+
r.eventRecorder.Eventf(esc, corev1.EventTypeNormal, "Reconciled", "secret resource %s restored to desired state", secretName)
60+
return nil
4961
}
62+
r.eventRecorder.Eventf(esc, corev1.EventTypeNormal, "Reconciled", "secret resource %s created", secretName)
5063
}
5164
return nil
5265
}

pkg/controller/external_secrets/secret_test.go

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"testing"
77

88
corev1 "k8s.io/api/core/v1"
9+
apierrors "k8s.io/apimachinery/pkg/api/errors"
10+
"k8s.io/apimachinery/pkg/runtime/schema"
911
"k8s.io/apimachinery/pkg/types"
1012
"sigs.k8s.io/controller-runtime/pkg/client"
1113

@@ -87,7 +89,7 @@ func TestCreateOrApplySecret(t *testing.T) {
8789
wantErr: fmt.Sprintf("failed to check %s/%s secret resource already exists: %s", commontest.TestExternalSecretsNamespace, testValidateSecretResourceName, commontest.ErrTestClient),
8890
},
8991
{
90-
name: "reconciliation of secret fails while restoring to expected state",
92+
name: "reconciliation of secret fails while patching metadata to expected state",
9193
preReq: func(r *Reconciler, m *fakes.FakeCtrlClient) {
9294
m.GetCalls(func(ctx context.Context, ns types.NamespacedName, obj client.Object) error {
9395
if o, ok := obj.(*corev1.Secret); ok {
@@ -104,14 +106,52 @@ func TestCreateOrApplySecret(t *testing.T) {
104106
}
105107
return true, nil
106108
})
107-
m.UpdateWithRetryCalls(func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
109+
m.PatchCalls(func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
108110
if _, ok := obj.(*corev1.Secret); ok {
109111
return commontest.ErrTestClient
110112
}
111113
return nil
112114
})
113115
},
114-
wantErr: fmt.Sprintf("failed to update %s/%s secret resource: %s", commontest.TestExternalSecretsNamespace, testValidateSecretResourceName, commontest.ErrTestClient),
116+
wantErr: fmt.Sprintf("failed to patch %s/%s secret resource metadata: %s", commontest.TestExternalSecretsNamespace, testValidateSecretResourceName, commontest.ErrTestClient),
117+
},
118+
{
119+
// Regression test: when the managed label is removed from the Secret, the object
120+
// falls out of the label-filtered cache. cached Exists() returns false, Create()
121+
// fails with AlreadyExists, and the controller must patch only the metadata,
122+
// leaving cert-controller-injected TLS data untouched.
123+
name: "Create returns AlreadyExists (label-filtered cache miss): patches metadata to restore labels",
124+
preReq: func(r *Reconciler, m *fakes.FakeCtrlClient) {
125+
m.ExistsCalls(func(ctx context.Context, ns types.NamespacedName, obj client.Object) (bool, error) {
126+
return false, nil
127+
})
128+
m.CreateCalls(func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
129+
return apierrors.NewAlreadyExists(schema.GroupResource{Resource: "secrets"}, testValidateSecretResourceName)
130+
})
131+
m.PatchCalls(func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
132+
if _, ok := obj.(*corev1.Secret); ok {
133+
if obj.GetLabels()["app"] != "external-secrets" {
134+
t.Errorf("expected app=external-secrets label in patch target, got %v", obj.GetLabels())
135+
}
136+
}
137+
return nil
138+
})
139+
},
140+
},
141+
{
142+
name: "Patch fails after AlreadyExists from Create",
143+
preReq: func(r *Reconciler, m *fakes.FakeCtrlClient) {
144+
m.ExistsCalls(func(ctx context.Context, ns types.NamespacedName, obj client.Object) (bool, error) {
145+
return false, nil
146+
})
147+
m.CreateCalls(func(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
148+
return apierrors.NewAlreadyExists(schema.GroupResource{Resource: "secrets"}, testValidateSecretResourceName)
149+
})
150+
m.PatchCalls(func(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
151+
return commontest.ErrTestClient
152+
})
153+
},
154+
wantErr: fmt.Sprintf("failed to patch %s/%s secret resource metadata: %s", commontest.TestExternalSecretsNamespace, testValidateSecretResourceName, commontest.ErrTestClient),
115155
},
116156
{
117157
name: "reconciliation of secret which already exists in expected state",
@@ -155,7 +195,7 @@ func TestCreateOrApplySecret(t *testing.T) {
155195
return nil
156196
})
157197
},
158-
wantErr: fmt.Sprintf("failed to create Secret %s/%s: %s", commontest.TestExternalSecretsNamespace, testValidateSecretResourceName, commontest.ErrTestClient),
198+
wantErr: fmt.Sprintf("failed to create %s/%s secret resource: %s", commontest.TestExternalSecretsNamespace, testValidateSecretResourceName, commontest.ErrTestClient),
159199
},
160200
{
161201
name: "successful secret creation",

0 commit comments

Comments
 (0)