Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions pkg/controller/external_secrets/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,9 @@ func (r *Reconciler) createOrApplyCertificate(esc *operatorv1alpha1.ExternalSecr
r.log.V(4).Info("certificate resource already exists and is in expected state", "name", certificateName)
}
if !exist {
if err := r.Create(r.ctx, desired); err != nil {
return common.FromClientError(err, "failed to create %s certificate resource", certificateName)
if err := r.createWithFallback(desired, resourceMetadata, certificateName, esc); err != nil {
return err
}
r.eventRecorder.Eventf(esc, corev1.EventTypeNormal, "Reconciled", "certificate resource %s created", certificateName)
}

return nil
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/external_secrets/certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ func TestCreateOrApplyCertificates(t *testing.T) {
esc.Spec.ControllerConfig.CertProvider.CertManager.IssuerRef.Name = testIssuerName
},
recon: false,
wantErr: fmt.Sprintf("failed to create %s/%s certificate resource: %s", commontest.TestExternalSecretsNamespace, testValidateCertificateResourceName, commontest.ErrTestClient),
wantErr: fmt.Sprintf("failed to create Certificate %s/%s: %s", commontest.TestExternalSecretsNamespace, testValidateCertificateResourceName, commontest.ErrTestClient),
},
{
name: "successful webhook certificate creation",
Expand Down
31 changes: 19 additions & 12 deletions pkg/controller/external_secrets/configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"maps"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

Expand Down Expand Up @@ -57,25 +58,31 @@ func (r *Reconciler) ensureTrustedCABundleConfigMap(esc *operatorv1alpha1.Extern
}

if !exist {
// Create the ConfigMap
// NOTE: This ConfigMap cannot use the generic createWithFallback helper because
Comment thread
chiragkyal marked this conversation as resolved.
// the Data/BinaryData fields are managed by CNO (not by this operator). On
// AlreadyExists we use a MergePatch that touches only metadata, leaving
// CNO-injected CA certificates untouched.
if err := r.Create(r.ctx, desiredConfigMap); err != nil {
return common.FromClientError(err, "failed to create %s trusted CA bundle ConfigMap resource", configMapName)
if !apierrors.IsAlreadyExists(err) {
return common.FromClientError(err, "failed to create %s trusted CA bundle ConfigMap resource", configMapName)
}
r.log.V(1).Info("trusted CA bundle ConfigMap exists on API server but absent from label-filtered cache, patching metadata", "name", configMapName)
if patchErr := r.patchResourceMetadata(desiredConfigMap, resourceMetadata); patchErr != nil {
return common.FromClientError(patchErr, "failed to patch %s trusted CA bundle ConfigMap metadata", configMapName)
}
r.eventRecorder.Eventf(esc, corev1.EventTypeNormal, "Reconciled", "trusted CA bundle ConfigMap resource %s restored to desired state", configMapName)
return nil
}
r.eventRecorder.Eventf(esc, corev1.EventTypeNormal, "Reconciled", "trusted CA bundle ConfigMap resource %s created", configMapName)
return nil
}

// ConfigMap exists, ensure it has the correct labels and annotations.
// Do not update the data of the ConfigMap since it is managed by CNO.
// MergeFetchedAnnotations preserves external annotations (e.g., CNO's openshift.io/owning-component).
// ConfigMap exists in cache — ensure it has the correct labels and annotations.
// Use a metadata-only patch so CNO-managed Data/BinaryData are never touched.
if exist && common.ObjectMetadataModified(desiredConfigMap, existingConfigMap, &resourceMetadata) {
r.log.V(1).Info("trusted CA bundle ConfigMap has been modified, updating to desired state", "name", configMapName)
// Preserve data from existing ConfigMap (managed by CNO)
desiredConfigMap.Data = existingConfigMap.Data
desiredConfigMap.BinaryData = existingConfigMap.BinaryData
common.RemoveObsoleteAnnotations(desiredConfigMap, resourceMetadata)
if err := r.UpdateWithRetry(r.ctx, desiredConfigMap); err != nil {
return common.FromClientError(err, "failed to update %s trusted CA bundle ConfigMap resource", configMapName)
r.log.V(1).Info("trusted CA bundle ConfigMap has been modified, patching metadata to desired state", "name", configMapName)
if err := r.patchResourceMetadata(desiredConfigMap, resourceMetadata); err != nil {
return common.FromClientError(err, "failed to patch %s trusted CA bundle ConfigMap metadata", configMapName)
}
r.eventRecorder.Eventf(esc, corev1.EventTypeNormal, "Reconciled", "trusted CA bundle ConfigMap resource %s reconciled back to desired state", configMapName)
} else {
Expand Down
259 changes: 259 additions & 0 deletions pkg/controller/external_secrets/configmap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
package external_secrets

import (
"context"
"testing"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

operatorv1alpha1 "github.com/openshift/external-secrets-operator/api/v1alpha1"
"github.com/openshift/external-secrets-operator/pkg/controller/client/fakes"
"github.com/openshift/external-secrets-operator/pkg/controller/common"
"github.com/openshift/external-secrets-operator/pkg/controller/commontest"
)

// testESCWithProxy returns an ExternalSecretsConfig with a proxy configured so that
// ensureTrustedCABundleConfigMap proceeds past the proxy-nil guard.
func testESCWithProxy() *operatorv1alpha1.ExternalSecretsConfig {
esc := commontest.TestExternalSecretsConfig()
esc.Spec.ApplicationConfig.Proxy = &operatorv1alpha1.ProxyConfig{
HTTPProxy: "http://proxy.example.com:3128",
}
return esc
}

func TestEnsureTrustedCABundleConfigMap(t *testing.T) {
cnoData := map[string]string{"ca-bundle.crt": "cert-data"}

tests := []struct {
name string
resourceMetadata common.ResourceMetadata
preReq func(r *Reconciler, cached *fakes.FakeCtrlClient, uncached *fakes.FakeCtrlClient)
wantErr string
wantCreate bool
wantPatch bool
noProxy bool
}{
{
name: "ConfigMap created when it does not exist",
resourceMetadata: testResourceMetadata(commontest.TestExternalSecretsConfig()),
preReq: func(_ *Reconciler, cached *fakes.FakeCtrlClient, _ *fakes.FakeCtrlClient) {
cached.ExistsCalls(func(_ context.Context, _ types.NamespacedName, _ client.Object) (bool, error) {
return false, nil
})
cached.CreateCalls(func(_ context.Context, obj client.Object, _ ...client.CreateOption) error {
cm, ok := obj.(*corev1.ConfigMap)
if !ok {
t.Errorf("expected ConfigMap, got %T", obj)
}
if cm.Name != trustedCABundleConfigMapName {
t.Errorf("expected name %s, got %s", trustedCABundleConfigMapName, cm.Name)
}
if cm.Labels[trustedCABundleInjectLabel] != "true" {
t.Errorf("expected inject label to be 'true', got %q", cm.Labels[trustedCABundleInjectLabel])
}
return nil
})
},
wantCreate: true,
},
{
name: "ConfigMap exists in correct state, no update needed",
resourceMetadata: testResourceMetadata(commontest.TestExternalSecretsConfig()),
preReq: func(_ *Reconciler, cached *fakes.FakeCtrlClient, _ *fakes.FakeCtrlClient) {
cached.ExistsCalls(func(_ context.Context, _ types.NamespacedName, obj client.Object) (bool, error) {
existing := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: trustedCABundleConfigMapName,
Namespace: commontest.TestExternalSecretsNamespace,
Labels: getTrustedCABundleLabels(controllerDefaultResourceLabels),
},
Data: cnoData,
}
existing.DeepCopyInto(obj.(*corev1.ConfigMap))
return true, nil
})
},
},
{
name: "ConfigMap exists with wrong labels, patch triggered",
resourceMetadata: testResourceMetadata(commontest.TestExternalSecretsConfig()),
preReq: func(_ *Reconciler, cached *fakes.FakeCtrlClient, _ *fakes.FakeCtrlClient) {
cached.ExistsCalls(func(_ context.Context, _ types.NamespacedName, obj client.Object) (bool, error) {
existing := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: trustedCABundleConfigMapName,
Namespace: commontest.TestExternalSecretsNamespace,
Labels: map[string]string{"app": "something-else"},
},
Data: cnoData,
}
existing.DeepCopyInto(obj.(*corev1.ConfigMap))
return true, nil
})
cached.PatchCalls(func(_ context.Context, obj client.Object, patch client.Patch, _ ...client.PatchOption) error {
cm, ok := obj.(*corev1.ConfigMap)
if !ok {
t.Errorf("expected ConfigMap, got %T", obj)
}
if cm.Labels[trustedCABundleInjectLabel] != "true" {
t.Errorf("expected inject label in patch target, got %q", cm.Labels[trustedCABundleInjectLabel])
}
if patch.Type() != types.JSONPatchType {
t.Errorf("expected JSONPatch, got %s", patch.Type())
}
return nil
})
},
wantPatch: true,
},
{
// Regression test for: labels updating with app: external-secrets of cm
// external-secrets-trusted-ca-bundle will block the reconcile in the http_proxy env.
//
// When the managed label (app=external-secrets) is removed from the ConfigMap,
// the object falls out of the label-filtered cache. The cached Exists() returns
// false, Create() fails with AlreadyExists, and the controller must patch only
// the metadata (labels/annotations), leaving CNO-managed Data/BinaryData untouched.
name: "Create returns AlreadyExists (label-filtered cache miss): patches metadata to restore labels",
resourceMetadata: testResourceMetadata(commontest.TestExternalSecretsConfig()),
preReq: func(_ *Reconciler, cached *fakes.FakeCtrlClient, _ *fakes.FakeCtrlClient) {
cached.ExistsCalls(func(_ context.Context, _ types.NamespacedName, _ client.Object) (bool, error) {
return false, nil
})
cached.CreateCalls(func(_ context.Context, _ client.Object, _ ...client.CreateOption) error {
return apierrors.NewAlreadyExists(schema.GroupResource{Resource: "configmaps"}, trustedCABundleConfigMapName)
})
cached.PatchCalls(func(_ context.Context, obj client.Object, patch client.Patch, _ ...client.PatchOption) error {
cm, ok := obj.(*corev1.ConfigMap)
if !ok {
t.Errorf("expected ConfigMap, got %T", obj)
}
if cm.Labels["app"] != "external-secrets" {
t.Errorf("expected app=external-secrets label in patch target, got %q", cm.Labels["app"])
}
if cm.Labels[trustedCABundleInjectLabel] != "true" {
t.Errorf("expected inject label in patch target, got %q", cm.Labels[trustedCABundleInjectLabel])
}
if patch.Type() != types.JSONPatchType {
t.Errorf("expected JSONPatch, got %s", patch.Type())
}
return nil
})
},
wantPatch: true,
},
{
name: "proxy not configured: ConfigMap reconcile skipped",
resourceMetadata: testResourceMetadata(commontest.TestExternalSecretsConfig()),
noProxy: true,
},
{
name: "cached Exists check fails",
resourceMetadata: testResourceMetadata(commontest.TestExternalSecretsConfig()),
preReq: func(_ *Reconciler, cached *fakes.FakeCtrlClient, _ *fakes.FakeCtrlClient) {
cached.ExistsCalls(func(_ context.Context, _ types.NamespacedName, _ client.Object) (bool, error) {
return false, commontest.ErrTestClient
})
},
wantErr: "failed to check external-secrets/external-secrets-trusted-ca-bundle trusted CA bundle ConfigMap resource already exists: test client error",
},
{
name: "Create fails with non-AlreadyExists error",
resourceMetadata: testResourceMetadata(commontest.TestExternalSecretsConfig()),
preReq: func(_ *Reconciler, cached *fakes.FakeCtrlClient, _ *fakes.FakeCtrlClient) {
cached.ExistsCalls(func(_ context.Context, _ types.NamespacedName, _ client.Object) (bool, error) {
return false, nil
})
cached.CreateCalls(func(_ context.Context, _ client.Object, _ ...client.CreateOption) error {
return commontest.ErrTestClient
})
},
wantErr: "failed to create external-secrets/external-secrets-trusted-ca-bundle trusted CA bundle ConfigMap resource: test client error",
},
{
name: "Patch fails after AlreadyExists from Create",
resourceMetadata: testResourceMetadata(commontest.TestExternalSecretsConfig()),
preReq: func(_ *Reconciler, cached *fakes.FakeCtrlClient, _ *fakes.FakeCtrlClient) {
cached.ExistsCalls(func(_ context.Context, _ types.NamespacedName, _ client.Object) (bool, error) {
return false, nil
})
cached.CreateCalls(func(_ context.Context, _ client.Object, _ ...client.CreateOption) error {
return apierrors.NewAlreadyExists(schema.GroupResource{Resource: "configmaps"}, trustedCABundleConfigMapName)
})
cached.PatchCalls(func(_ context.Context, _ client.Object, _ client.Patch, _ ...client.PatchOption) error {
return commontest.ErrTestClient
})
},
wantErr: "failed to patch external-secrets/external-secrets-trusted-ca-bundle trusted CA bundle ConfigMap metadata: test client error",
},
{
name: "cached Patch fails when ConfigMap has wrong labels",
resourceMetadata: testResourceMetadata(commontest.TestExternalSecretsConfig()),
preReq: func(_ *Reconciler, cached *fakes.FakeCtrlClient, _ *fakes.FakeCtrlClient) {
cached.ExistsCalls(func(_ context.Context, _ types.NamespacedName, obj client.Object) (bool, error) {
existing := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: trustedCABundleConfigMapName,
Namespace: commontest.TestExternalSecretsNamespace,
Labels: nil,
},
}
existing.DeepCopyInto(obj.(*corev1.ConfigMap))
return true, nil
})
cached.PatchCalls(func(_ context.Context, _ client.Object, _ client.Patch, _ ...client.PatchOption) error {
return commontest.ErrTestClient
})
},
wantErr: "failed to patch external-secrets/external-secrets-trusted-ca-bundle trusted CA bundle ConfigMap metadata: test client error",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := testReconciler(t)
cached := &fakes.FakeCtrlClient{}
uncached := &fakes.FakeCtrlClient{}
if tt.preReq != nil {
tt.preReq(r, cached, uncached)
}
r.CtrlClient = cached
r.UncachedClient = uncached

var esc *operatorv1alpha1.ExternalSecretsConfig
if tt.noProxy {
esc = commontest.TestExternalSecretsConfig()
} else {
esc = testESCWithProxy()
}

err := r.ensureTrustedCABundleConfigMap(esc, tt.resourceMetadata)

if tt.wantErr != "" {
if err == nil || err.Error() != tt.wantErr {
t.Errorf("ensureTrustedCABundleConfigMap() err = %v, wantErr = %v", err, tt.wantErr)
}
return
}
if err != nil {
t.Errorf("ensureTrustedCABundleConfigMap() unexpected error: %v", err)
}

if tt.wantCreate && cached.CreateCallCount() == 0 {
t.Error("expected Create to be called, but it wasn't")
}
if tt.wantPatch && cached.PatchCallCount() == 0 {
t.Error("expected Patch to be called, but it wasn't")
}
if !tt.wantPatch && cached.PatchCallCount() > 0 {
t.Error("expected no Patch call, but one was made")
}
})
}
}
Loading