Skip to content

Conversation

chiragkyal
Copy link
Member

No description provided.

@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Sep 19, 2025
@openshift-merge-robot openshift-merge-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Sep 19, 2025
Copy link

openshift-ci bot commented Sep 19, 2025

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: chiragkyal
Once this PR has been reviewed and has the lgtm label, please assign trilokgeer 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

@emmajiafan
Copy link

@CodeRabbit review

Copy link

coderabbitai bot commented Sep 23, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

coderabbitai bot commented Sep 23, 2025

Walkthrough

Enable proxy-awareness and add trusted CA bundle support: controller now computes proxy configuration, injects proxy environment variables, manages a trusted CA ConfigMap, mounts the bundle into deployments, adds ConfigMap watches, updates CSVs/manifests, and adds unit tests and metrics cert mounts.

Changes

Cohort / File(s) Summary of changes
Controller: proxy & trusted CA logic
pkg/controller/external_secrets/constants.go, pkg/controller/external_secrets/controller.go, pkg/controller/external_secrets/utils.go, pkg/controller/external_secrets/configmap.go, pkg/controller/external_secrets/deployments.go, pkg/controller/external_secrets/install_external_secrets.go, pkg/controller/external_secrets/deployments_test.go
Added proxy configuration resolution (ESC → Manager → OLM env), set proxy env vars on containers (including lowercase variants), added trusted CA bundle volume/mount handling, ensured/managed trusted CA ConfigMap, added ConfigMap watches/predicates, integrated into reconcile flow, and added unit tests.
Bundle & CSV updates
bundle/manifests/external-secrets-operator.clusterserviceversion.yaml, config/manifests/bases/external-secrets-operator.clusterserviceversion.yaml
Toggle features.operators.openshift.io/proxy-aware to "true" and update CSV createdAt timestamp.
Trusted CA ConfigMap manifest
bundle/manifests/external-secrets-operator-trusted-ca-bundle_v1_configmap.yaml
Add new ConfigMap manifest external-secrets-operator-trusted-ca-bundle with labels including config.openshift.io/inject-trusted-cabundle: "true".
Manager kustomize and patch
config/manager/kustomization.yaml, config/manager/trusted-ca-patch.yaml
Add ConfigMap generator for trusted CA bundle and patch manager Deployment to mount the trusted CA bundle at /etc/pki/tls/certs (readOnly); disable name suffix hash.
Metrics serving cert patch
config/default/manager_metrics_patch.yaml
Replace no-op arrays with concrete volumeMounts and volumes to mount metrics-serving-cert secret at /etc/metrics-certs and keep existing metrics arg.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning No pull request description was provided, so there is no summary of the changes or context for reviewers to understand the intent and scope of the updates. Please add a concise description that outlines the purpose of the pull request and summarizes the key changes made to support HTTP proxy awareness.
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly summarizes the primary change of making the operator HTTP proxy aware, reflecting the core update introduced by the pull request despite the presence of the work-in-progress prefix.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
pkg/controller/external_secrets/rbacs_test.go (1)

172-183: Wrong asset helper used for ClusterRole name check.

Comparing against a ClusterRoleBinding name will mask failures.

-					case *rbacv1.ClusterRole:
-						if obj.GetName() == testClusterRoleBinding(certControllerClusterRoleBindingAssetName).GetName() {
+					case *rbacv1.ClusterRole:
+						if obj.GetName() == testClusterRole(certControllerClusterRoleAssetName).GetName() {
pkg/controller/common/utils.go (4)

62-68: Potential nil-map panic in UpdateResourceLabels.

obj.GetLabels() can be nil; assigning into it panics.

 func UpdateResourceLabels(obj client.Object, labels map[string]string) {
-    l := obj.GetLabels()
+    l := obj.GetLabels()
+    if l == nil {
+        l = map[string]string{}
+    }
     for k, v := range labels {
         l[k] = v
     }
     obj.SetLabels(l)
 }

284-286: Possible nil dereference on SecurityContext compare.

Dereferences *fetchedContainer.SecurityContext without nil check when desired is non‑nil and fetched is nil.

-    // SecurityContext nil check
-    if desiredContainer.SecurityContext != nil && !reflect.DeepEqual(*desiredContainer.SecurityContext, *fetchedContainer.SecurityContext) {
-        return true
-    }
+    // SecurityContext nil checks
+    if (desiredContainer.SecurityContext == nil) != (fetchedContainer.SecurityContext == nil) {
+        return true
+    }
+    if desiredContainer.SecurityContext != nil && fetchedContainer.SecurityContext != nil &&
+        !reflect.DeepEqual(*desiredContainer.SecurityContext, *fetchedContainer.SecurityContext) {
+        return true
+    }

292-295: Resource comparison logic likely inverted.

Currently flags a change when desired resources are empty and fetched aren’t. Usually we want to update only when desired explicitly differs.

-    if reflect.DeepEqual(desiredContainer.Resources, corev1.ResourceRequirements{}) &&
-        !reflect.DeepEqual(desiredContainer.Resources, fetchedContainer.Resources) {
-        return true
-    }
+    if !reflect.DeepEqual(desiredContainer.Resources, fetchedContainer.Resources) &&
+        !reflect.DeepEqual(desiredContainer.Resources, corev1.ResourceRequirements{}) {
+        return true
+    }

If the intended behavior was to clear resources when desired is empty, please confirm and we can gate that behind an explicit flag.


385-387: Function name vs behavior mismatch: IsESMSpecEmpty returns true when spec is non‑empty.

This can propagate logic errors.

-func IsESMSpecEmpty(esm *operatorv1alpha1.ExternalSecretsManager) bool {
-    return esm != nil && !reflect.DeepEqual(esm.Spec, operatorv1alpha1.ExternalSecretsManagerSpec{})
-}
+func IsESMSpecEmpty(esm *operatorv1alpha1.ExternalSecretsManager) bool {
+    if esm == nil {
+        return true
+    }
+    return reflect.DeepEqual(esm.Spec, operatorv1alpha1.ExternalSecretsManagerSpec{})
+}

If callers rely on the old (inverted) semantics, consider introducing IsESMSpecNonEmpty and migrating call sites.

pkg/controller/external_secrets/controller.go (1)

412-414: Wrong Reason for Ready=false fatal case.

ReasonReady on a non‑ready condition is misleading. Use ReasonFailed.

- readyCond.Status = metav1.ConditionFalse
- readyCond.Reason = operatorv1alpha1.ReasonReady
+ readyCond.Status = metav1.ConditionFalse
+ readyCond.Reason = operatorv1alpha1.ReasonFailed
pkg/controller/external_secrets/install_external_secrets.go (2)

30-46: Guard against nil ControllerConfig before accessing Labels

Potential NPE: esc.Spec.ControllerConfig may be nil.

Apply:

- if len(esc.Spec.ControllerConfig.Labels) != 0 {
-   for k, v := range esc.Spec.ControllerConfig.Labels {
+ if esc.Spec.ControllerConfig != nil && len(esc.Spec.ControllerConfig.Labels) != 0 {
+   for k, v := range esc.Spec.ControllerConfig.Labels {

106-121: createOrApplyNamespace only creates; it never applies labels on existing namespaces

This regresses "apply" semantics; label drift won’t be corrected.

Apply:

   if err := r.Create(r.ctx, obj); err != nil {
     if errors.IsAlreadyExists(err) {
-      r.log.V(4).Info("namespace already exists", "namespace", namespace)
-      return nil
+      // Ensure labels are up to date on existing namespace
+      existing := &corev1.Namespace{}
+      if getErr := r.Get(r.ctx, types.NamespacedName{Name: namespace}, existing); getErr != nil {
+        return getErr
+      }
+      common.UpdateResourceLabels(existing, resourceLabels)
+      if updateErr := r.UpdateWithRetry(r.ctx, existing); updateErr != nil {
+        return updateErr
+      }
+      r.log.V(4).Info("namespace already exists; labels ensured", "namespace", namespace)
+      return nil
     }
     return err
   }

Add missing import:

k8s.io/apimachinery/pkg/types
pkg/controller/external_secrets/certificate.go (1)

164-183: Handle unsupported Issuer kind to avoid passing a nil object to Exists()

Currently, an unknown kind yields a nil client.Object and risks a panic/pathological behavior.

   var object client.Object
   switch issuerRef.Kind {
   case clusterIssuerKind:
     object = &certmanagerv1.ClusterIssuer{}
   case issuerKind:
     object = &certmanagerv1.Issuer{}
+  default:
+    return false, fmt.Errorf("unsupported issuer kind %q", issuerRef.Kind)
   }
 
- ifExists, err := r.UncachedClient.Exists(r.ctx, namespacedName, object)
+ ifExists, err := r.UncachedClient.Exists(r.ctx, namespacedName, object)
pkg/controller/external_secrets_manager/controller.go (1)

67-70: Missing RBAC for ExternalSecretsConfig

The controller reads and watches externalsecretsconfigs; add permissions or manager will fail with RBAC errors.

Add:

// +kubebuilder:rbac:groups=operator.openshift.io,resources=externalsecretsconfigs,verbs=get;list;watch
🧹 Nitpick comments (56)
test/apis/README.md (1)

1-1: Fix bare URL and stabilize branch reference.

Use a Markdown link and avoid hardcoding the branch name to reduce link rot.

-Refer to https://github.com/openshift/api/tree/master/tests for more details.
+Refer to the OpenShift API tests for more details: https://github.com/openshift/api/tree/main/tests
pkg/controller/external_secrets/constants.go (1)

48-51: Correct comment to reflect actual field path.

Spec.Namespace doesn’t exist; Namespace lives under ControllerConfig.

-	// will be created, when ExternalSecretsConfig.Spec.Namespace is not set.
+	// will be created when ExternalSecretsConfig.Spec.ControllerConfig.Namespace is not set.
api/v1alpha1/tests/externalsecretsmanager.operator.openshift.io/externalsecretsmanager.testsuite.yaml (2)

210-219: Add acceptance test for noProxy at boundary length.

You test “too long”; add a case that accepts exactly 4096 bytes to lock the boundary.

+    - name: Should accept no proxy list at maximum length boundary
+      resourceName: cluster
+      initial: |
+        apiVersion: operator.openshift.io/v1alpha1
+        kind: ExternalSecretsManager
+        spec:
+          globalConfig:
+            logLevel: 1
+            proxy:
+              noProxy: "<string of exactly 4096 bytes>"
+      expected: |
+        apiVersion: operator.openshift.io/v1alpha1
+        kind: ExternalSecretsManager
+        spec:
+          globalConfig:
+            logLevel: 1
+            proxy:
+              noProxy: "<string of exactly 4096 bytes>"

92-99: Consider invalid-scheme tests for proxy URLs.

Add negative cases for schemes other than http/https (e.g., socks5://) if the CRD enforces scheme.

Also applies to: 244-253

pkg/controller/external_secrets/serviceaccounts_test.go (1)

124-124: Duplicate test name; rename for clarity.

Two tests share the same title. Rename this one to reflect the deletion path.

-			name: "cert-controller serviceaccount skipped when cert-manager enabled",
+			name: "cert-controller serviceaccount deletion fails when cert-manager enabled",
config/samples/operator_v1alpha1_externalsecrets.yaml (1)

2-8: Align sample with rename and proxy feature

  • Consider renaming the file to operator_v1alpha1_externalsecretsconfig.yaml for consistency.
  • Add a minimal Proxy configuration example in spec to showcase the PR’s HTTP proxy support.
test/e2e/testdata/external_secret.yaml (1)

2-8: E2E testdata naming and coverage

  • Consider renaming to external_secrets_config.yaml for clarity.
  • Add a case exercising Proxy settings to validate the new HTTP proxy awareness end‑to‑end.
README.md (1)

28-31: Fix typo in controller bullet

“external-scerets” → “external-secrets”.

-  * reconciling the `externalsecretsmanagers.openshift.operator.io` resource for the global configurations and updates the `external-scerets` deployment accordingly.
+  * reconciling the `externalsecretsmanagers.openshift.operator.io` resource for the global configurations and updates the `external-secrets` deployment accordingly.
hack/test-apis.sh (1)

16-19: Shell robustness: quote HOME and comparison

Avoid unbound/word-splitting issues; quote HOME and use : default.

-export HOME=${HOME:=/tmp/kubebuilder-testing}
-if [ $HOME == "/" ]; then
+export HOME="${HOME:-/tmp/kubebuilder-testing}"
+if [ "$HOME" = "/" ]; then
   export HOME=/tmp/kubebuilder-testing
 fi
test/apis/suite_test.go (1)

46-50: Register project APIs with the scheme.

Typed client encodes/decodes your CRs only if the scheme knows them.

-    testScheme = scheme.Scheme
+    testScheme = scheme.Scheme
+    // register operator APIs
+    Expect(operatorv1alpha1.AddToScheme(testScheme)).To(Succeed())

And add imports:

@@
-    "testing"
+    "testing"
+    "context"
@@
-    "k8s.io/client-go/discovery"
+    "k8s.io/client-go/discovery"
+    operatorv1alpha1 "github.com/openshift/external-secrets-operator/api/v1alpha1"

If these vars aren’t declared elsewhere, also declare at file scope:

+var (
+    cfg       *rest.Config
+    k8sClient client.Client
+    testEnv   *envtest.Environment
+    testScheme = scheme.Scheme
+)

With import:

+    "k8s.io/client-go/rest"
pkg/controller/external_secrets/serviceaccounts.go (2)

40-42: Fix misleading delete error message.

Always mentions “cert-controller” even when deleting another SA.

-            return fmt.Errorf("failed to delete cert-controller serviceaccount: %w", err)
+            return fmt.Errorf("failed to delete serviceaccount %q: %w", serviceAccount.assetName, err)

15-15: Optional: rename boolean param for clarity.

externalsecretsCreateRecon now mismatches type naming. Consider createReconcile or escCreateRecon.

api/v1alpha1/meta.go (3)

58-63: Enforce immutability of Resources via CEL (comment says “Cannot be updated”).

Add an XValidation rule so the API enforces it.

-    // Cannot be updated.
+    // Cannot be updated.
+    // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="resources is immutable"

91-111: Proxy config shape is fine; consider follow-ups.

  • NoProxy as a string is acceptable, but a string list (slice) avoids comma-splitting issues later.
  • Optionally add URL pattern checks for http(s)Proxy.

Would you like a follow-up PR to add patterns and a helper to translate ProxyConfig to env vars (HTTP_PROXY/HTTPS_PROXY/NO_PROXY and lowercase variants)?


72-84: Hard caps on Tolerations/NodeSelector may be too restrictive.

MaxItems/MaxProperties of 10 can block legitimate configurations. Consider removing or raising.

pkg/controller/common/utils.go (1)

432-437: Generic RemoveFinalizer error messages hardcode resource kind.

Errors always mention externalsecretsconfig... even when removing from other types.

-            return fmt.Errorf("failed to create %q externalsecretsconfig.openshift.operator.io object with finalizers removed", namespacedName)
+            return fmt.Errorf("failed to update %q with finalizers removed", namespacedName)
@@
-            return fmt.Errorf("failed to remove finalizers on %q externalsecretsconfig.openshift.operator.io with %w", namespacedName, err)
+            return fmt.Errorf("failed to remove finalizers on %q: %w", namespacedName, err)

Optionally specialize by inspecting the object’s GVK.

Makefile (1)

266-269: Optional: add a simple clean target (static checkmake warning).

Helps CI hygiene.

 .PHONY: ginkgo
 ginkgo: $(LOCALBIN) ## Download ginkgo locally if necessary.
 	$(call go-install-tool,$(GINKGO),github.com/onsi/ginkgo/v2/ginkgo)
 
+.PHONY: clean
+clean: ## Remove local build artifacts.
+	rm -rf $(LOCALBIN) cover.out dist
pkg/controller/crd_annotator/controller_test.go (2)

171-177: Use updated resource name in NotFound error.

Reflect the CRD rename for accuracy.

-                        return errors.NewNotFound(schema.GroupResource{
-                            Group:    operatorv1alpha1.GroupVersion.Group,
-                            Resource: "externalsecrets",
-                        }, commontest.TestExternalSecretsConfigResourceName)
+                        return errors.NewNotFound(schema.GroupResource{
+                            Group:    operatorv1alpha1.GroupVersion.Group,
+                            Resource: "externalsecretsconfigs",
+                        }, commontest.TestExternalSecretsConfigResourceName)

418-423: Consider asserting Get() error in status checks.

Ignoring the error can hide regressions when the resource isn’t created.

-            r.CtrlClient.Get(r.ctx, key, esc)
+            if err := r.CtrlClient.Get(r.ctx, key, esc); err != nil && tt.wantErr == "" {
+                t.Fatalf("failed to fetch ESC for status assertions: %v", err)
+            }
pkg/controller/external_secrets/services.go (1)

42-79: Param name nit: keep naming consistent with ExternalSecretsConfig.

Rename externalsecretsCreateRecon → externalSecretsConfigCreateRecon for clarity.

-func (r *Reconciler) createOrApplyServiceFromAsset(esc *operatorv1alpha1.ExternalSecretsConfig, assetName string, resourceLabels map[string]string, externalsecretsCreateRecon bool) error {
+func (r *Reconciler) createOrApplyServiceFromAsset(esc *operatorv1alpha1.ExternalSecretsConfig, assetName string, resourceLabels map[string]string, externalSecretsConfigCreateRecon bool) error {
@@
-  if exists {
-    if externalsecretsCreateRecon {
+  if exists {
+    if externalSecretsConfigCreateRecon {
api/v1alpha1/tests/externalsecretsconfig.operator.openshift.io/externalsecretsconfig.testsuite.yaml (1)

104-130: Duplicate test case—consolidate to one.

“addInjectorAnnotations true when enabled is false” appears twice with the same expectation. Please drop one to reduce noise.

pkg/controller/external_secrets/secret_test.go (2)

181-185: Test doesn’t enable cert‑manager but claims creation is skipped.

This case never toggles esc.Spec.ApplicationConfig.CertManagerConfig.Enabled to "true", and no client behavior is stubbed. Make intent explicit and stub a successful delete.

-    {
-      name: "secret creation skipped when cert-manager config is enabled",
-      preReq: func(r *Reconciler, m *fakes.FakeCtrlClient) {
-      },
-    },
+    {
+      name: "secret creation skipped when cert-manager config is enabled",
+      preReq: func(r *Reconciler, m *fakes.FakeCtrlClient) {
+        m.DeleteCalls(func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error {
+          return nil
+        })
+      },
+      esc: func(esc *v1alpha1.ExternalSecretsConfig) {
+        esc.Spec.ApplicationConfig = v1alpha1.ApplicationConfig{
+          CertManagerConfig: &v1alpha1.CertManagerConfig{ Enabled: "true" },
+        }
+      },
+    },

224-227: Consider adding a drift test if Secret comparison switches to full object.

If you adopt HasObjectChanged in production, add a test where Secret data differs and ensure update path triggers.

pkg/controller/external_secrets/controller.go (3)

327-365: Resource strings use the wrong API group order.

Logs/comments reference externalsecretsconfig.openshift.operator.io; the group is operator.openshift.io (plural form for resources).

- // deployment to reflect desired state configured in `externalsecretsconfig.openshift.operator.io`.
+ // deployment to reflect desired state configured in `externalsecretsconfigs.operator.openshift.io`.
@@
- // Fetch the externalsecretsconfig.openshift.operator.io CR
+ // Fetch the externalsecretsconfigs.operator.openshift.io CR
@@
- r.log.V(1).Info("externalsecretsconfig.openshift.operator.io object not found, skipping reconciliation", "request", req)
+ r.log.V(1).Info("externalsecretsconfigs.operator.openshift.io object not found, skipping reconciliation", "request", req)
@@
- return ctrl.Result{}, fmt.Errorf("failed to fetch externalsecretsconfig.openshift.operator.io %q during reconciliation: %w", req.NamespacedName, err)
+ return ctrl.Result{}, fmt.Errorf("failed to fetch externalsecretsconfigs.operator.openshift.io %q during reconciliation: %w", req.NamespacedName, err)
@@
- r.log.V(1).Info("externalsecretsconfig.openshift.operator.io is marked for deletion", "namespace", req.NamespacedName)
+ r.log.V(1).Info("externalsecretsconfigs.operator.openshift.io is marked for deletion", "resource", req.NamespacedName)
@@
- return ctrl.Result{}, fmt.Errorf("clean up failed for %q externalsecretsconfig.openshift.operator.io instance deletion: %w", req.NamespacedName, err)
+ return ctrl.Result{}, fmt.Errorf("clean up failed for %q externalsecretsconfigs.operator.openshift.io instance deletion: %w", req.NamespacedName, err)
@@
- return ctrl.Result{}, fmt.Errorf("failed to update %q externalsecretsconfig.openshift.operator.io with finalizers: %w", req.NamespacedName, err)
+ return ctrl.Result{}, fmt.Errorf("failed to update %q externalsecretsconfigs.operator.openshift.io with finalizers: %w", req.NamespacedName, err)

367-379: Same group order issue for ExternalSecretsManager.

Use externalsecretsmanagers.operator.openshift.io.

- // Fetch the externalsecretsmanager.openshift.operator.io CR
+ // Fetch the externalsecretsmanagers.operator.openshift.io CR
@@
- r.log.V(1).Info("externalsecretsmanager.openshift.operator.io object not found, continuing without it")
+ r.log.V(1).Info("externalsecretsmanagers.operator.openshift.io object not found, continuing without it")
@@
- return ctrl.Result{}, fmt.Errorf("failed to fetch externalsecretsmanager.openshift.operator.io %q during reconciliation: %w", esmNamespacedName, err)
+ return ctrl.Result{}, fmt.Errorf("failed to fetch externalsecretsmanagers.operator.openshift.io %q during reconciliation: %w", esmNamespacedName, err)

458-463: Event message uses wrong group order.

Minor copy fix.

- r.eventRecorder.Eventf(esc, corev1.EventTypeWarning, "RemoveDeployment", "%s/%s externalsecretsconfig.openshift.operator.io marked for deletion, remove reference in deployment and remove all resources created for deployment", esc.GetNamespace(), esc.GetName())
+ r.eventRecorder.Eventf(esc, corev1.EventTypeWarning, "RemoveDeployment", "%s/%s externalsecretsconfigs.operator.openshift.io marked for deletion, remove reference in deployment and remove all resources created for deployment", esc.GetNamespace(), esc.GetName())
config/manifests/bases/external-secrets-operator.clusterserviceversion.yaml (2)

133-138: Tighten CSV description grammar.

Minor copy fixes for clarity.

-  description: External Secrets Operator for Red Hat OpenShift deploys and manages
-    `external-secrets` application in OpenShift clusters. `external-secrets` provides
-    an uniformed interface to fetch secrets stored in external providers like  AWS
-    Secrets Manager, HashiCorp Vault, Google Secrets Manager, Azure Key Vault, IBM
-    Cloud Secrets Manager to name a few, stores them as secrets in OpenShift. It provides
-    APIs to define authentication and the details of the secret to fetch.
+  description: External Secrets Operator for Red Hat OpenShift deploys and manages
+    the `external-secrets` application in OpenShift clusters. `external-secrets` provides
+    a uniform interface to fetch secrets stored in external providers such as AWS
+    Secrets Manager, HashiCorp Vault, Google Secret Manager, Azure Key Vault, and IBM
+    Cloud Secrets Manager, and stores them as Secrets in OpenShift. It provides
+    APIs to define authentication and the details of the secret to fetch.

10-10: Align createdAt with bundle CSV.

Base CSV has createdAt: 2023-03-03; bundle CSV uses 2025-09-18. Consider syncing for consistency.

pkg/controller/external_secrets/validatingwebhook.go (1)

57-73: Error message phrasing and potential nil-guard.

Message says “for %s external secrets”; suggest neutral wording and avoid esc.GetName() in the error path in case esc is nil in future edits.

-        if err := updateValidatingWebhookAnnotation(esc, validatingWebhook); err != nil {
-            return nil, fmt.Errorf("failed to update validatingWebhook resource for %s external secrets: %s", esc.GetName(), err.Error())
-        }
+        if err := updateValidatingWebhookAnnotation(esc, validatingWebhook); err != nil {
+            return nil, fmt.Errorf("failed to update validatingWebhook annotations: %w", err)
+        }
test/apis/generator.go (5)

47-58: Deterministic suite order.

Map iteration randomizes test order. Sort file paths before loading to stabilize runs.

   var out []SuiteSpec
-  for path := range suiteFiles {
+  paths := make([]string, 0, len(suiteFiles))
+  for p := range suiteFiles {
+    paths = append(paths, p)
+  }
+  sort.Strings(paths)
+  for _, path := range paths {
     suite, err := loadSuiteFile(path)
     if err != nil {
       return nil, fmt.Errorf("could not set up test suite: %w", err)
     }
     out = append(out, suite)
   }

Add import:

 import (
   "bytes"
   "fmt"
+  "sort"
   "os"
   "path/filepath"
   "strings"

396-404: GenerateName ergonomics.

Kubernetes commonly expects generateName prefixes to end with “-”. Add it if missing for readability.

-  if useGenerateName {
-    u.SetGenerateName(resourceName)
+  if useGenerateName {
+    gen := resourceName
+    if !strings.HasSuffix(gen, "-") {
+      gen += "-"
+    }
+    u.SetGenerateName(gen)
   } else {
     u.SetName(resourceName)
   }

256-260: Comment typo.

“sential” → “sentinel”.

-    // Use an eventually here, so that we retry until the sential correctly applies.
+    // Use Eventually here so that we retry until the sentinel correctly applies.

555-585: Redundant continue.

The continue after the if/break is dead code; remove for clarity.

     if currCRD.Name == crdName {
       crdFilesToCheck = append(crdFilesToCheck, filename)
       break
     }
-    continue

14-15: Prefer maintained YAML lib.

Switch from github.com/ghodss/yaml (archived) to sigs.k8s.io/yaml.

-  "github.com/ghodss/yaml"
+  sigsYaml "sigs.k8s.io/yaml"
-  if err := yaml.Unmarshal(raw, &s); err != nil {
+  if err := sigsYaml.Unmarshal(raw, &s); err != nil {
-  if err := yaml.Unmarshal(raw, crd); err != nil {
+  if err := sigsYaml.Unmarshal(raw, crd); err != nil {
-  if err := yaml.Unmarshal(patchedData.Bytes(), crd); err != nil {
+  if err := sigsYaml.Unmarshal(patchedData.Bytes(), crd); err != nil {

Also applies to: 431-434, 548-550

pkg/controller/external_secrets/certificate_test.go (1)

467-481: Helper reuse suggestion.

Small DRY win: factor a helper to set Enabled/IssuerRef to cut repetition in table entries.

bundle/manifests/external-secrets-operator.clusterserviceversion.yaml (2)

331-336: Fix bundle CSV description grammar.

Same copy edits as base CSV.

-  description: External Secrets Operator for Red Hat OpenShift deploys and manages
-    `external-secrets` application in OpenShift clusters. `external-secrets` provides
-    an uniformed interface to fetch secrets stored in external providers like  AWS
-    Secrets Manager, HashiCorp Vault, Google Secrets Manager, Azure Key Vault, IBM
-    Cloud Secrets Manager to name a few, stores them as secrets in OpenShift. It provides
-    APIs to define authentication and the details of the secret to fetch.
+  description: External Secrets Operator for Red Hat OpenShift deploys and manages
+    the `external-secrets` application in OpenShift clusters. `external-secrets` provides
+    a uniform interface to fetch secrets stored in external providers such as AWS
+    Secrets Manager, HashiCorp Vault, Google Secret Manager, Azure Key Vault, and IBM
+    Cloud Secrets Manager, and stores them as Secrets in OpenShift. It provides
+    APIs to define authentication and the details of the secret to fetch.

500-523: RBAC for Proxy object (if read at runtime).

If the operator reads cluster proxy config (config.openshift.io/v1, Resource=proxies), add get/list/watch to clusterPermissions. If not, ignore.

I can propose the RBAC stanza once confirmed.

pkg/controller/external_secrets/install_external_secrets.go (1)

100-100: Align log message with new API name

Use "externalsecretsconfig" for clarity.

- r.log.V(4).Info("finished reconciliation of external-secrets", "namespace", esc.GetNamespace(), "name", esc.GetName())
+ r.log.V(4).Info("finished reconciliation of externalsecretsconfig", "namespace", esc.GetNamespace(), "name", esc.GetName())
pkg/controller/external_secrets/certificate.go (1)

5-5: Avoid reflect; use explicit nil/field checks

Reflect here adds complexity and a false‑positive path. Prefer direct checks.

-import "reflect"
pkg/controller/external_secrets_manager/controller.go (3)

91-96: Defensive type assertions in predicate

Hard type assertions can panic; guard them.

- oldObj := e.ObjectOld.(*operatorv1alpha1.ExternalSecretsConfig)
- newObj := e.ObjectNew.(*operatorv1alpha1.ExternalSecretsConfig)
+ oldObj, ok1 := e.ObjectOld.(*operatorv1alpha1.ExternalSecretsConfig)
+ newObj, ok2 := e.ObjectNew.(*operatorv1alpha1.ExternalSecretsConfig)
+ if !ok1 || !ok2 {
+   return false
+ }

49-50: Fix finalizer comment to match resource

Comment references the wrong group/resource.

- // finalizer name for external-secrets.openshift.operator.io resource.
+ // finalizer name for externalsecretsmanager.openshift.operator.io resource.

102-103: Optional: Map ESC updates directly to the singleton ESM request

Reduces chance of accidental enqueueing with unexpected names.

Use EnqueueRequestsFromMapFunc to always enqueue NamespacedName{Name: common.ExternalSecretsManagerObjectName}.

pkg/controller/external_secrets/deployments.go (2)

247-249: Fix duplicated error field paths (“spec.affinity.affinity…”, “spec.tolerations.tolerations…”)

Call sites already include the field segment; the validators append it again, duplicating path components.

- if err := validateAffinityRules(affinity, field.NewPath("spec", "affinity")); err != nil {
+ if err := validateAffinityRules(affinity, field.NewPath("spec")); err != nil {
     return err
 }
- if err := validateTolerationsConfig(tolerations, field.NewPath("spec", "tolerations")); err != nil {
+ if err := validateTolerationsConfig(tolerations, field.NewPath("spec")); err != nil {
     return err
 }

Then update the unit test expectations to reflect correct paths (see related comment).

Also applies to: 269-271


290-294: Align tolerations validator with caller to avoid double “tolerations” in path

If you keep the callers as-is, remove the extra Child here. Prefer aligning with the pattern used by nodeSelector.

 func validateTolerationsConfig(tolerations []corev1.Toleration, fldPath *field.Path) error {
   // convert corev1.Tolerations to core.Tolerations, required for validation.
   convTolerations := *(*[]core.Toleration)(unsafe.Pointer(&tolerations))
-  return corevalidation.ValidateTolerations(convTolerations, fldPath.Child("tolerations")).ToAggregate()
+  return corevalidation.ValidateTolerations(convTolerations, fldPath.Child("tolerations")).ToAggregate()
 }

If you applied the call-site fix, change this to fldPath.Child("tolerations") -> fldPath instead.

test/apis/vars.go (1)

68-71: Fix typo in comment (“resourec” → “resource”)

Small doc nit.

- // ExpectedError defines the error string that should be returned when the initial resourec is invalid.
+ // ExpectedError defines the error string that should be returned when the initial resource is invalid.
pkg/controller/external_secrets/deployments_test.go (3)

414-418: Remove duplicated conditional in status image assertion

Nested if tt.wantErr == "" is redundant.

- if tt.wantErr == "" {
-   if tt.wantErr == "" {
-     if externalsecrets.Status.ExternalSecretsImage != commontest.TestExternalSecretsImageName {
-       t.Errorf(...)
-     }
-   }
- }
+ if tt.wantErr == "" && externalsecrets.Status.ExternalSecretsImage != commontest.TestExternalSecretsImageName {
+   t.Errorf("createOrApplyDeployments() got image in status: %v, want: %v", externalsecrets.Status.ExternalSecretsImage, "test-image")
+ }

424-722: Add tests: proxy removal and initContainers coverage

  • Add a case where proxies were previously present, then ESC/ESM unset and OLM envs are absent; expect env vars removed from all containers.
  • Add a case with initContainers to assert proxy env propagation/removal there too.

I can draft these test cases to align with the proposed code changes—want me to push them?


231-231: Error path field duplication is codified in expectations

Tests expect spec.tolerations.tolerations[...] and spec.affinity.affinity.... If you adopt the field-path fix in code, update these expected strings accordingly.

Also applies to: 312-313

bundle/manifests/operator.openshift.io_externalsecretsconfigs.yaml (2)

1018-1021: Validation inconsistency for TLS secret reference field

The comment mentions that the TLS key pair secret is required, but the field is marked as optional without any validation to ensure it's provided when Bitwarden is enabled.

Consider adding a validation rule to ensure the secret reference is provided when Bitwarden is enabled:

x-kubernetes-validations:
- message: secretRef must be provided when Bitwarden is enabled
  rule: self.enabled != 'true' || has(self.secretRef)

1108-1113: Consider simplifying the validation expression

The validation expression is complex and could be simplified for better readability.

-                    - message: addInjectorAnnotations can only be set when enabled
-                        is set to 'true'.
-                      rule: 'has(self.addInjectorAnnotations) && self.addInjectorAnnotations
-                        != ''false'' ? self.enabled != ''false'' : true'
+                    - message: addInjectorAnnotations can only be set when enabled is set to 'true'.
+                      rule: '!has(self.addInjectorAnnotations) || self.addInjectorAnnotations == ''false'' || self.enabled == ''true'''
pkg/controller/external_secrets/utils.go (1)

102-110: Consider handling additional log levels explicitly

The log level mapping only handles specific cases (0-2, 4-5) but doesn't handle level 3. This might cause unexpected behavior.

 func getLogLevel(config operatorv1alpha1.ExternalSecretsConfigSpec) string {
 	switch config.ApplicationConfig.LogLevel {
 	case 0, 1, 2:
 		return zapcore.Level(config.ApplicationConfig.LogLevel).String()
+	case 3:
+		return zapcore.WarnLevel.String()
 	case 4, 5:
 		return zapcore.DebugLevel.String()
 	}
 	return zapcore.InfoLevel.String()
 }
docs/api_reference.md (1)

37-40: Fix bare URLs in markdown documentation

The documentation contains bare URLs that should be properly formatted as markdown links for better readability.

-| `resources` _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcerequirements-v1-core)_ | resources is for defining the resource requirements.<br />Cannot be updated.<br />ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ |  | Optional: \{\} <br /> |
-| `affinity` _[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#affinity-v1-core)_ | affinity is for setting scheduling affinity rules.<br />ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/ |  | Optional: \{\} <br /> |
-| `tolerations` _[Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#toleration-v1-core) array_ | tolerations is for setting the pod tolerations.<br />ref: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ |  | MaxItems: 10 <br />MinItems: 0 <br />Optional: \{\} <br /> |
-| `nodeSelector` _object (keys:string, values:string)_ | nodeSelector is for defining the scheduling criteria using node labels.<br />ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ |  | MaxProperties: 10 <br />MinProperties: 0 <br />Optional: \{\} <br /> |
+| `resources` _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcerequirements-v1-core)_ | resources is for defining the resource requirements.<br />Cannot be updated.<br />ref: [Managing Resources for Containers](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) |  | Optional: \{\} <br /> |
+| `affinity` _[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#affinity-v1-core)_ | affinity is for setting scheduling affinity rules.<br />ref: [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/) |  | Optional: \{\} <br /> |
+| `tolerations` _[Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#toleration-v1-core) array_ | tolerations is for setting the pod tolerations.<br />ref: [Taints and Tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) |  | MaxItems: 10 <br />MinItems: 0 <br />Optional: \{\} <br /> |
+| `nodeSelector` _object (keys:string, values:string)_ | nodeSelector is for defining the scheduling criteria using node labels.<br />ref: [Assigning Pods to Nodes](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/) |  | MaxProperties: 10 <br />MinProperties: 0 <br />Optional: \{\} <br /> |
api/v1alpha1/external_secrets_config_types.go (1)

182-183: Consider using constants for cert-manager validation

The validation rules use hardcoded strings for 'issuer', 'clusterissuer', and 'cert-manager.io'. Consider defining these as constants for maintainability.

While the current validation works, you might want to define constants in the code for these values to ensure consistency across the codebase and make future updates easier.

pkg/controller/crd_annotator/controller.go (3)

215-235: Thread the request context; avoid r.ctx inside reconciliation path

Using a background context bypasses cancellation/shutdown and deadlines. Pass ctx through to all client calls.

Apply this diff in this function:

-func (r *Reconciler) processReconcileRequest(esc *operatorv1alpha1.ExternalSecretsConfig, req types.NamespacedName) (ctrl.Result, error) {
+func (r *Reconciler) processReconcileRequest(ctx context.Context, esc *operatorv1alpha1.ExternalSecretsConfig, req types.NamespacedName) (ctrl.Result, error) {
@@
-        if err := r.Get(r.ctx, req, crd); err != nil {
+        if err := r.Get(ctx, req, crd); err != nil {

And update the call site in Reconcile:

return r.processReconcileRequest(ctx, esc, req.NamespacedName)

223-231: Preserve status updates on NotFound by avoiding early return

Currently, a NotFound CRD short-circuits before updateCondition, so Status isn’t updated. Update the condition before returning.

Apply this diff:

-            if errors.IsNotFound(err) {
-                r.log.V(1).Info("crd managed by OLM is not found, skipping reconciliation", "crd", req)
-                return ctrl.Result{}, nil
-            }
+            if errors.IsNotFound(err) {
+                r.log.V(1).Info("crd managed by OLM is not found, skipping reconciliation", "crd", req)
+                if uerr := r.updateCondition(esc, nil); uerr != nil {
+                    return ctrl.Result{}, uerr
+                }
+                return ctrl.Result{}, nil
+            }

272-276: Avoid taking the address of the range loop variable

Passing &crd uses the address of the loop variable; prefer indexing to take the address of the slice element.

-    for _, crd := range managedCRDList.Items {
-        if err := r.updateAnnotations(&crd); err != nil {
+    for i := range managedCRDList.Items {
+        if err := r.updateAnnotations(&managedCRDList.Items[i]); err != nil {
             return fmt.Errorf("failed to update annotations in %q: %w", crd.GetName(), err)
         }
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 9c19a74 and cac8b3f.

⛔ Files ignored due to path filters (125)
  • go.sum is excluded by !**/*.sum
  • vendor/github.com/ghodss/yaml/.gitignore is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/ghodss/yaml/.travis.yml is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/ghodss/yaml/LICENSE is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/ghodss/yaml/README.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/ghodss/yaml/fields.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/ghodss/yaml/yaml.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/CHANGELOG.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/core_dsl.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/decorator_dsl.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/build/build_command.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo/watch/dependencies.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/ginkgo_t_dsl.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/around_node.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/focus.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/group.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/node.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/progress_report.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/spec_context.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/suite.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/internal/testingtproxy/testing_t_proxy.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/reporters/default_reporter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/reporters/junit_report.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/reporters/teamcity_report.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/around_node.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/config.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/errors.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/semver_filter.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/types.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/ginkgo/v2/types/version.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/gomega/CHANGELOG.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/gomega/gomega_dsl.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/gomega/internal/async_assertion.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/gomega/matchers/be_comparable_to_matcher.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/onsi/gomega/matchers/match_yaml_matcher.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/vmware-archive/yaml-patch/LICENSE is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/vmware-archive/yaml-patch/Makefile is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/vmware-archive/yaml-patch/README.md is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/vmware-archive/yaml-patch/container.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/vmware-archive/yaml-patch/node.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/vmware-archive/yaml-patch/operation.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/vmware-archive/yaml-patch/patch.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/vmware-archive/yaml-patch/pathfinder.go is excluded by !vendor/**, !**/vendor/**
  • vendor/github.com/vmware-archive/yaml-patch/placeholder_wrapper.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/net/http2/http2.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/mkerrors.sh is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/syscall_darwin.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_386.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_arm.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_mips.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_386.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_loong64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_ppc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_386.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_amd64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_arm.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_arm64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_loong64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_mips.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_mips64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_mips64le.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_mipsle.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_ppc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_ppc64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_ppc64le.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_riscv64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_s390x.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/sys/unix/ztypes_linux_sparc64.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/term/term_windows.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/term/terminal.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/ast/astutil/enclosing.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/ast/inspector/inspector.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/ast/inspector/typeof.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/packages/doc.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/go/ssa/builder.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/imports/source_modindex.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/modindex/directories.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/modindex/index.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/modindex/modindex.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/modindex/symbols.go is excluded by !vendor/**, !**/vendor/**
  • vendor/golang.org/x/tools/internal/modindex/types.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/encoding/protowire/wire.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/editiondefaults/editions_defaults.binpb is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/filedesc/editions.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/filedesc/presence.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/genid/descriptor_gen.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/impl/codec_message_opaque.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/impl/message_opaque.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/impl/presence.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/internal/version/version.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/reflect/protoreflect/source_gen.go is excluded by !vendor/**, !**/vendor/**
  • vendor/google.golang.org/protobuf/types/descriptorpb/descriptor.pb.go is excluded by !**/*.pb.go, !vendor/**, !**/vendor/**
  • vendor/modules.txt is excluded by !vendor/**, !**/vendor/**
  • vendor/sigs.k8s.io/controller-runtime/pkg/envtest/komega/OWNERS is excluded by !vendor/**, !**/vendor/**
  • vendor/sigs.k8s.io/controller-runtime/pkg/envtest/komega/default.go is excluded by !vendor/**, !**/vendor/**
  • vendor/sigs.k8s.io/controller-runtime/pkg/envtest/komega/equalobject.go is excluded by !vendor/**, !**/vendor/**
  • vendor/sigs.k8s.io/controller-runtime/pkg/envtest/komega/interfaces.go is excluded by !vendor/**, !**/vendor/**
  • vendor/sigs.k8s.io/controller-runtime/pkg/envtest/komega/komega.go is excluded by !vendor/**, !**/vendor/**
  • vendor/sigs.k8s.io/controller-runtime/pkg/webhook/admission/multi.go is excluded by !vendor/**, !**/vendor/**
📒 Files selected for processing (53)
  • Makefile (5 hunks)
  • PROJECT (1 hunks)
  • README.md (1 hunks)
  • api/v1alpha1/external_secrets_config_types.go (4 hunks)
  • api/v1alpha1/external_secrets_manager_types.go (4 hunks)
  • api/v1alpha1/meta.go (3 hunks)
  • api/v1alpha1/tests/externalsecretsconfig.operator.openshift.io/externalsecretsconfig.testsuite.yaml (1 hunks)
  • api/v1alpha1/tests/externalsecretsmanager.operator.openshift.io/externalsecretsmanager.testsuite.yaml (1 hunks)
  • api/v1alpha1/zz_generated.deepcopy.go (8 hunks)
  • bundle/manifests/external-secrets-operator.clusterserviceversion.yaml (6 hunks)
  • bundle/manifests/operator.openshift.io_externalsecretsconfigs.yaml (9 hunks)
  • bundle/manifests/operator.openshift.io_externalsecretsmanagers.yaml (5 hunks)
  • config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml (9 hunks)
  • config/crd/bases/operator.openshift.io_externalsecretsmanagers.yaml (5 hunks)
  • config/crd/kustomization.yaml (1 hunks)
  • config/manifests/bases/external-secrets-operator.clusterserviceversion.yaml (2 hunks)
  • config/rbac/role.yaml (2 hunks)
  • config/samples/operator_v1alpha1_externalsecrets.yaml (1 hunks)
  • docs/api_reference.md (10 hunks)
  • go.mod (4 hunks)
  • hack/test-apis.sh (1 hunks)
  • pkg/controller/common/constants.go (1 hunks)
  • pkg/controller/common/utils.go (4 hunks)
  • pkg/controller/commontest/utils.go (3 hunks)
  • pkg/controller/crd_annotator/controller.go (8 hunks)
  • pkg/controller/crd_annotator/controller_test.go (16 hunks)
  • pkg/controller/external_secrets/certificate.go (5 hunks)
  • pkg/controller/external_secrets/certificate_test.go (13 hunks)
  • pkg/controller/external_secrets/constants.go (2 hunks)
  • pkg/controller/external_secrets/controller.go (9 hunks)
  • pkg/controller/external_secrets/deployments.go (15 hunks)
  • pkg/controller/external_secrets/deployments_test.go (14 hunks)
  • pkg/controller/external_secrets/install_external_secrets.go (3 hunks)
  • pkg/controller/external_secrets/rbacs.go (11 hunks)
  • pkg/controller/external_secrets/rbacs_test.go (6 hunks)
  • pkg/controller/external_secrets/secret.go (3 hunks)
  • pkg/controller/external_secrets/secret_test.go (3 hunks)
  • pkg/controller/external_secrets/service_test.go (3 hunks)
  • pkg/controller/external_secrets/serviceaccounts.go (4 hunks)
  • pkg/controller/external_secrets/serviceaccounts_test.go (5 hunks)
  • pkg/controller/external_secrets/services.go (4 hunks)
  • pkg/controller/external_secrets/utils.go (3 hunks)
  • pkg/controller/external_secrets/validatingwebhook.go (4 hunks)
  • pkg/controller/external_secrets/validatingwebhook_test.go (1 hunks)
  • pkg/controller/external_secrets_manager/controller.go (4 hunks)
  • pkg/controller/external_secrets_manager/controller_test.go (7 hunks)
  • test/apis/README.md (1 hunks)
  • test/apis/generator.go (1 hunks)
  • test/apis/suite_test.go (1 hunks)
  • test/apis/vars.go (1 hunks)
  • test/e2e/e2e_test.go (1 hunks)
  • test/e2e/testdata/external_secret.yaml (1 hunks)
  • tools/tools.go (1 hunks)
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
test/apis/README.md

1-1: Bare URL used

(MD034, no-bare-urls)

docs/api_reference.md

37-37: Bare URL used

(MD034, no-bare-urls)


38-38: Bare URL used

(MD034, no-bare-urls)


39-39: Bare URL used

(MD034, no-bare-urls)


40-40: Bare URL used

(MD034, no-bare-urls)


98-98: Bare URL used

(MD034, no-bare-urls)


99-99: Bare URL used

(MD034, no-bare-urls)


100-100: Bare URL used

(MD034, no-bare-urls)


101-101: Bare URL used

(MD034, no-bare-urls)


364-364: Bare URL used

(MD034, no-bare-urls)


365-365: Bare URL used

(MD034, no-bare-urls)


366-366: Bare URL used

(MD034, no-bare-urls)


367-367: Bare URL used

(MD034, no-bare-urls)

🪛 checkmake (0.2.2)
Makefile

[warning] 395-395: Missing required phony target "clean"

(minphony)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/controller/external_secrets/deployments.go (1)

117-121: Do not require Bitwarden image unless reconciling Bitwarden deployment.

Today, missing RELATED_IMAGE_BITWARDEN_SDK_SERVER fails all deployment reconciliations even if Bitwarden is disabled.

-	bitwardenImage := os.Getenv(bitwardenImageEnvVarName)
-	if bitwardenImage == "" {
-		return nil, common.NewIrrecoverableError(fmt.Errorf("%s environment variable with bitwarden-sdk-server image not set", bitwardenImageEnvVarName), "failed to update image in %s deployment object", deployment.GetName())
-	}
@@
 	case bitwardenDeploymentAssetName:
-		deployment.Labels["app.kubernetes.io/version"] = bitwardenImageVersionEnvVarName
-		updateBitwardenServerContainerSpec(deployment, bitwardenImage)
+		bitwardenImage := os.Getenv(bitwardenImageEnvVarName)
+		if bitwardenImage == "" {
+			return nil, common.NewIrrecoverableError(fmt.Errorf("%s environment variable with bitwarden-sdk-server image not set", bitwardenImageEnvVarName), "failed to update image in %s deployment object", deployment.GetName())
+		}
+		deployment.Labels["app.kubernetes.io/version"] = bitwardenImageVersionEnvVarName
+		updateBitwardenServerContainerSpec(deployment, bitwardenImage)

Also applies to: 129-133

🧹 Nitpick comments (11)
pkg/controller/external_secrets/configmap.go (2)

46-47: Use reconciler context instead of context.TODO().

Leverage r.ctx for cancellation, tracing, and consistency with the rest of the controller.

-	err := r.Get(context.TODO(), types.NamespacedName{Name: trustedCABundleConfigMapName, Namespace: namespace}, existingConfigMap)
+	err := r.Get(r.ctx, types.NamespacedName{Name: trustedCABundleConfigMapName, Namespace: namespace}, existingConfigMap)
@@
-			if err := r.Create(context.TODO(), configMap); err != nil {
+			if err := r.Create(r.ctx, configMap); err != nil {
@@
-		if err := r.Update(context.TODO(), existingConfigMap); err != nil {
+		if err := r.Update(r.ctx, existingConfigMap); err != nil {

Also applies to: 51-52, 76-77


65-72: Minor: avoid recomputing expectedLabels.

You already compute expectedLabels at Line 30. Reuse it to reduce noise.

-	expectedLabels = getTrustedCABundleLabels(resourceLabels)
 	needsUpdate := false
 	for k, expectedValue := range expectedLabels {
pkg/controller/external_secrets/install_external_secrets.go (1)

111-127: createOrApplyNamespace doesn't “apply” when Namespace exists.

If the Namespace pre-exists, labels are never reconciled. Apply label changes when AlreadyExists.

 func (r *Reconciler) createOrApplyNamespace(esc *operatorv1alpha1.ExternalSecretsConfig, resourceLabels map[string]string) error {
   namespace := getNamespace(esc)
   obj := &corev1.Namespace{
     ObjectMeta: metav1.ObjectMeta{
       Name:   namespace,
       Labels: resourceLabels,
     },
   }
   if err := r.Create(r.ctx, obj); err != nil {
     if errors.IsAlreadyExists(err) {
-      r.log.V(4).Info("namespace already exists", "namespace", namespace)
-      return nil
+      // Fetch and patch labels if they differ
+      existing := &corev1.Namespace{}
+      if getErr := r.Get(r.ctx, types.NamespacedName{Name: namespace}, existing); getErr != nil {
+        return getErr
+      }
+      if !reflect.DeepEqual(existing.Labels, resourceLabels) {
+        if existing.Labels == nil {
+          existing.Labels = map[string]string{}
+        }
+        for k, v := range resourceLabels {
+          existing.Labels[k] = v
+        }
+        if updErr := r.Update(r.ctx, existing); updErr != nil {
+          return updErr
+        }
+      }
+      return nil
     }
     return err
   }
   return nil
 }

Note: add imports if missing:

  • "reflect"
  • "k8s.io/apimachinery/pkg/types"
pkg/controller/external_secrets/controller.go (2)

225-228: Fix wrong object in error message.

Uses ownObject in the esm informer error; should log esmObject.

-	if err != nil {
-		return nil, fmt.Errorf("failed to add informer for %s resource: %w", ownObject.GetObjectKind().GroupVersionKind().String(), err)
-	}
+	if err != nil {
+		return nil, fmt.Errorf("failed to add informer for %s resource: %w", esmObject.GetObjectKind().GroupVersionKind().String(), err)
+	}

270-277: Tighten ConfigMap predicate to avoid cross‑namespace noise.

Name-only match can enqueue on similarly named CMs in other namespaces. Prefer filtering by the injection label you control.

-	// predicate function for ConfigMaps to include both managed resources and trusted CA bundle ConfigMap
-	configMapPredicate := predicate.NewPredicateFuncs(func(object client.Object) bool {
-		if object.GetLabels() != nil && object.GetLabels()[requestEnqueueLabelKey] == requestEnqueueLabelValue {
-			return true
-		}
-		return object.GetName() == trustedCABundleConfigMapName
-	})
+	// Predicate for ConfigMaps: include controller-managed resources and any CM with the injection label
+	configMapPredicate := predicate.NewPredicateFuncs(func(object client.Object) bool {
+		labels := object.GetLabels()
+		if labels != nil && labels[requestEnqueueLabelKey] == requestEnqueueLabelValue {
+			return true
+		}
+		return labels != nil && labels[trustedCABundleInjectLabel] == "true"
+	})
pkg/controller/external_secrets/deployments_test.go (2)

425-759: Add coverage: initContainers, lowercase envs, and removal of stale proxy vars.

Current tests validate only containers and uppercase envs. Please add cases to assert:

  • initContainers receive the same proxy env and mounts
  • lowercase http_proxy/https_proxy/no_proxy are set
  • when no proxy is configured from ESC/ESM/OLM, any existing proxy envs are removed

761-774: Make env var comparison order-insensitive.

Container.Env ordering is not guaranteed. Sort by Name before comparing to reduce test flakiness.

 func validateEnvironmentVariables(t *testing.T, deployment *appsv1.Deployment, expectedContainerEnvVars map[string][]corev1.EnvVar) {
 	for containerName, expectedEnvVars := range expectedContainerEnvVars {
 		container := findContainer(deployment, containerName)
 		if container == nil {
 			t.Errorf("Container %s not found in deployment", containerName)
 			return
 		}
-		if !reflect.DeepEqual(container.Env, expectedEnvVars) {
+		sort.Slice(container.Env, func(i, j int) bool { return container.Env[i].Name < container.Env[j].Name })
+		sort.Slice(expectedEnvVars, func(i, j int) bool { return expectedEnvVars[i].Name < expectedEnvVars[j].Name })
+		if !reflect.DeepEqual(container.Env, expectedEnvVars) {
 			t.Errorf("Container %s environment variables mismatch.\nExpected: %+v\nActual: %+v",
 				containerName, expectedEnvVars, container.Env)
 		}
 	}
 }

Note: add import "sort" at top.

pkg/controller/external_secrets/deployments.go (3)

185-187: Guard against nil r.esm in fallbacks.

Avoid potential NPEs by checking r.esm != nil before dereferencing in resource/affinity/toleration/nodeSelector fallbacks.

-	} else if r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Resources != nil {
+	} else if r.esm != nil && r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Resources != nil {
@@
-	} else if r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.NodeSelector != nil {
+	} else if r.esm != nil && r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.NodeSelector != nil {
@@
-	} else if r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Affinity != nil {
+	} else if r.esm != nil && r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Affinity != nil {
@@
-	} else if r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Tolerations != nil {
+	} else if r.esm != nil && r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Tolerations != nil {

Also applies to: 217-219, 239-241, 262-263


411-424: Proxies: support removal, initContainers, and lowercase envs.

  • Remove stale proxy envs when no proxy config resolves from ESC/ESM/OLM
  • Apply to initContainers as well
  • Set lowercase variants for broader image compatibility
 func (r *Reconciler) updateProxyEnvironmentVariables(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig) error {
 	proxyConfig := r.getProxyConfiguration(esc)
-	if proxyConfig == nil {
-		return nil
-	}
-
-	// Apply proxy environment variables to all containers
-	for i := range deployment.Spec.Template.Spec.Containers {
-		container := &deployment.Spec.Template.Spec.Containers[i]
-		r.setProxyEnvVars(container, proxyConfig)
-	}
+	// Helper to process both containers and initContainers
+	process := func(envSetter func(c *corev1.Container)) {
+		for i := range deployment.Spec.Template.Spec.Containers {
+			envSetter(&deployment.Spec.Template.Spec.Containers[i])
+		}
+		for i := range deployment.Spec.Template.Spec.InitContainers {
+			envSetter(&deployment.Spec.Template.Spec.InitContainers[i])
+		}
+	}
+	if proxyConfig == nil {
+		process(func(c *corev1.Container) { r.removeProxyEnvVars(c) })
+		return nil
+	}
+	process(func(c *corev1.Container) { r.setProxyEnvVars(c, proxyConfig) })
 
 	return nil
 }
@@
 	// Set proxy environment variables
 	setEnvVar("HTTP_PROXY", proxyConfig.HTTPProxy)
 	setEnvVar("HTTPS_PROXY", proxyConfig.HTTPSProxy)
 	setEnvVar("NO_PROXY", proxyConfig.NoProxy)
+	// Lowercase variants
+	setEnvVar("http_proxy", proxyConfig.HTTPProxy)
+	setEnvVar("https_proxy", proxyConfig.HTTPSProxy)
+	setEnvVar("no_proxy", proxyConfig.NoProxy)

Add helper outside the shown ranges:

// removeProxyEnvVars deletes all proxy-related env vars from a container.
func (r *Reconciler) removeProxyEnvVars(container *corev1.Container) {
	if len(container.Env) == 0 {
		return
	}
	toDrop := map[string]struct{}{
		"HTTP_PROXY": {}, "HTTPS_PROXY": {}, "NO_PROXY": {},
		"http_proxy": {}, "https_proxy": {}, "no_proxy": {},
	}
	dst := container.Env[:0]
	for _, e := range container.Env {
		if _, drop := toDrop[e.Name]; !drop {
			dst = append(dst, e)
		}
	}
	container.Env = dst
}

Also applies to: 426-459


509-524: Mount trusted CA bundle in initContainers too.

Parity with main containers ensures any init work honors the proxy CA.

-	for i := range deployment.Spec.Template.Spec.Containers {
+	for i := range deployment.Spec.Template.Spec.Containers {
 		container := &deployment.Spec.Template.Spec.Containers[i]
@@
 	}
+
+	// Add volume mounts to all init containers
+	for i := range deployment.Spec.Template.Spec.InitContainers {
+		container := &deployment.Spec.Template.Spec.InitContainers[i]
+		volumeMountExists := false
+		for j, volumeMount := range container.VolumeMounts {
+			if volumeMount.Name == trustedCABundleVolumeName {
+				container.VolumeMounts[j] = trustedCAVolumeMount
+				volumeMountExists = true
+				break
+			}
+		}
+		if !volumeMountExists {
+			container.VolumeMounts = append(container.VolumeMounts, trustedCAVolumeMount)
+		}
+	}
pkg/controller/external_secrets/constants.go (1)

52-66: Add derived file path constant for the injected CA bundle

Confirmed usage of ConfigMapName, InjectLabel, VolumeName, MountPath and KeyName; no direct full-path references exist—derive once instead of recomputing.

 	// trustedCABundleKeyName is the key in the ConfigMap that contains the CA bundle
 	trustedCABundleKeyName = "ca-bundle.crt"
+	// trustedCABundleFilePath is the full path to the injected CA bundle file
+	trustedCABundleFilePath = trustedCABundleMountPath + "/" + trustedCABundleKeyName
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between cac8b3f and c0c39bb.

📒 Files selected for processing (7)
  • pkg/controller/external_secrets/configmap.go (1 hunks)
  • pkg/controller/external_secrets/constants.go (2 hunks)
  • pkg/controller/external_secrets/controller.go (9 hunks)
  • pkg/controller/external_secrets/deployments.go (15 hunks)
  • pkg/controller/external_secrets/deployments_test.go (15 hunks)
  • pkg/controller/external_secrets/install_external_secrets.go (3 hunks)
  • pkg/controller/external_secrets/utils.go (4 hunks)
🔇 Additional comments (6)
pkg/controller/external_secrets/deployments.go (1)

410-415: Guard getProxyConfiguration against nil r.esm.

If not already fixed, ensure r.esm is checked inside getProxyConfiguration to avoid panics. This was flagged earlier.

Additional snippet (outside range):

func (r *Reconciler) getProxyConfiguration(esc *operatorv1alpha1.ExternalSecretsConfig) *operatorv1alpha1.ProxyConfig {
	// ESC overrides
	if esc != nil && esc.Spec.ApplicationConfig.CommonConfigs.Proxy != nil {
		return esc.Spec.ApplicationConfig.CommonConfigs.Proxy
	}
	// ESM fallback
	if r != nil && r.esm != nil && r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.CommonConfigs.Proxy != nil {
		return r.esm.Spec.GlobalConfig.CommonConfigs.Proxy
	}
	// OLM env fallback...
	// ...
	return nil
}
pkg/controller/external_secrets/install_external_secrets.go (1)

55-57: Don't swallow namespace reconcile errors.

Propagate the error so the loop can requeue and status can reflect failure. This was flagged earlier.

 if err := r.createOrApplyNamespace(esc, resourceLabels); err != nil {
-	r.log.Error(err, "failed to create namespace")
+	r.log.Error(err, "failed to reconcile namespace resource")
+	return err
 }
pkg/controller/external_secrets/controller.go (2)

429-433: Status updates short‑circuited; Ready may not update.

Evaluate both SetStatusCondition calls, then update once. Same issue was previously noted.

-		if apimeta.SetStatusCondition(&esc.Status.Conditions, degradedCond) ||
-			apimeta.SetStatusCondition(&esc.Status.Conditions, readyCond) {
+		changed := apimeta.SetStatusCondition(&esc.Status.Conditions, degradedCond)
+		changed = apimeta.SetStatusCondition(&esc.Status.Conditions, readyCond) || changed
+		if changed {
 			errUpdate = r.updateCondition(esc, err)
 			err = utilerrors.NewAggregate([]error{err, errUpdate})
 		}

456-459: Same short‑circuit bug on success path.

-	if apimeta.SetStatusCondition(&esc.Status.Conditions, degradedCond) ||
-		apimeta.SetStatusCondition(&esc.Status.Conditions, readyCond) {
-		errUpdate = r.updateCondition(esc, nil)
-	}
+	changed := apimeta.SetStatusCondition(&esc.Status.Conditions, degradedCond)
+	changed = apimeta.SetStatusCondition(&esc.Status.Conditions, readyCond) || changed
+	if changed {
+		errUpdate = r.updateCondition(esc, nil)
+	}
pkg/controller/external_secrets/constants.go (2)

48-51: LGTM: default namespace comment clarified.

Comment correctly reflects ExternalSecretsConfig.Spec.Namespace fallback.


19-21: Fix finalizer domain and keep a legacy finalizer for upgrade paths.

The string uses openshift.operator.io (reversed). Use operator.openshift.io and add a legacy constant so existing CRs don’t get stranded on upgrade. Also update the stale comment.

Apply:

-	// finalizer name for external-secrets.openshift.operator.io resource.
-	finalizer = "externalsecretsconfig.openshift.operator.io/" + ControllerName
+	// finalizer name for ExternalSecretsConfig resources.
+	// NOTE: keep legacy finalizer for upgrade cleanup of existing CRs.
+	finalizer       = "externalsecretsconfig.operator.openshift.io/" + ControllerName
+	legacyFinalizer = "externalsecrets.openshift.operator.io/" + ControllerName

Run to find and update add/remove checks to handle both finalizers (prefer new; remove both):

#!/bin/bash
# Find finalizer string literals and controller-runtime helpers usage
rg -nC2 -P 'externalsecrets.*openshift\.operator\.io/|-finalizer|-Finalizer|controllerutil\.(AddFinalizer|RemoveFinalizer|ContainsFinalizer)\('

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
bundle/manifests/external-secrets-operator.clusterserviceversion.yaml (2)

331-337: Fix user‑facing grammar and clarity in description

  • “an uniformed interface” → “a unified interface”
  • Remove double space before “AWS”
  • Clarify that secrets are stored as Kubernetes Secrets.

Apply:

-  description: External Secrets Operator for Red Hat OpenShift deploys and manages
-    `external-secrets` application in OpenShift clusters. `external-secrets` provides
-    an uniformed interface to fetch secrets stored in external providers like  AWS
-    Secrets Manager, HashiCorp Vault, Google Secrets Manager, Azure Key Vault, IBM
-    Cloud Secrets Manager to name a few, stores them as secrets in OpenShift. It provides
-    APIs to define authentication and the details of the secret to fetch.
+  description: External Secrets Operator for Red Hat OpenShift deploys and manages
+    the `external-secrets` application in OpenShift clusters. `external-secrets` provides
+    a unified interface to fetch secrets stored in external providers like AWS
+    Secrets Manager, HashiCorp Vault, Google Secrets Manager, Azure Key Vault, and IBM
+    Cloud Secrets Manager, and stores them as Kubernetes Secrets in OpenShift. It provides
+    APIs to define authentication and the details of the secret to fetch.

500-523: Add patch verb to externalsecretsconfigs ClusterRole
Controller patches ExternalSecretsConfig (finalizers); add patch to the main resource’s verbs. No patch needed on the status subresource.

--- bundle/manifests/external-secrets-operator.clusterserviceversion.yaml
@@ -502,7 +502,8 @@
           - operator.openshift.io
           resources:
           - externalsecretsconfigs
-          verbs:
-          - create
-          - get
-          - list
-          - update
-          - watch
+          verbs:
+          - create
+          - get
+          - list
+          - patch
+          - update
+          - watch
🧹 Nitpick comments (4)
config/manager/trusted-ca-patch.yaml (2)

11-15: Explicitly set SSL_CERT_FILE for portability.

To ensure all base images/apps pick up the injected CA, set SSL_CERT_FILE to the mounted bundle.

Apply this diff:

         - name: manager
+          env:
+            - name: SSL_CERT_FILE
+              value: /etc/pki/tls/certs/ca-bundle.crt
           volumeMounts:
             - name: trusted-ca-bundle
               mountPath: /etc/pki/tls/certs
               readOnly: true

Please confirm the controller image base (UBI/distroless/etc.). If it’s not UBI/RHEL‑like, we may need SSL_CERT_DIR instead or a different path.


10-14: Harden container security context (addresses Checkov hints).

You can set restrictive defaults here without affecting functionality.

Apply this diff:

         - name: manager
+          securityContext:
+            allowPrivilegeEscalation: false
+            readOnlyRootFilesystem: true
+            runAsNonRoot: true
+            seccompProfile:
+              type: RuntimeDefault
bundle/manifests/external-secrets-operator.clusterserviceversion.yaml (2)

179-189: alm-example: ExternalSecretsConfig entry looks correct

Kind/name align with singleton pattern; empty spec is fine for a minimal example.

Consider showcasing a minimal ProxyConfig in the example to demonstrate HTTP(S) proxy usage.


668-685: Mount CA bundle via subPath to avoid shadowing system certs
Use subPath to mount only ca-bundle.crt instead of the entire certs directory. The ConfigMap external-secrets-operator-trusted-ca-bundle (with config.openshift.io/inject-trusted-cabundle: "true") is present.

-                volumeMounts:
-                - mountPath: /etc/pki/tls/certs
-                  name: trusted-ca-bundle
-                  readOnly: true
+                volumeMounts:
+                - mountPath: /etc/pki/tls/certs/ca-bundle.crt
+                  name: trusted-ca-bundle
+                  subPath: ca-bundle.crt
+                  readOnly: true
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between c0c39bb and 10effe4.

📒 Files selected for processing (4)
  • bundle/manifests/external-secrets-operator-trusted-ca-bundle_v1_configmap.yaml (1 hunks)
  • bundle/manifests/external-secrets-operator.clusterserviceversion.yaml (7 hunks)
  • config/manager/kustomization.yaml (1 hunks)
  • config/manager/trusted-ca-patch.yaml (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • config/manager/kustomization.yaml
  • bundle/manifests/external-secrets-operator-trusted-ca-bundle_v1_configmap.yaml
🧰 Additional context used
🪛 Checkov (3.2.334)
config/manager/trusted-ca-patch.yaml

[medium] 1-21: Containers should not run with allowPrivilegeEscalation

(CKV_K8S_20)


[medium] 1-21: Minimize the admission of root containers

(CKV_K8S_23)

🔇 Additional comments (2)
config/manager/trusted-ca-patch.yaml (1)

16-21: LGTM on volume wiring and key mapping.

Mounting ca-bundle.crt read-only at /etc/pki/tls/certs is correct for OpenShift’s injected bundle.

If a kustomize ConfigMapGenerator is also used for this bundle, ensure there is no duplication and either:

  • use the static, annotated CM (no nameSuffixHash), or
  • rely solely on the generator and verify kustomize rewrites the name reference in volumes.configMap.name.
bundle/manifests/external-secrets-operator.clusterserviceversion.yaml (1)

267-276: CRD ownership/description rename to ExternalSecretsConfig is consistent

Kind, name, and singleton wording align with the API rebrand.

volumes:
- name: trusted-ca-bundle
configMap:
name: trusted-ca-bundle
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

ConfigMap name likely mismatched with the injected bundle manifest.

This references ConfigMap "trusted-ca-bundle", while the PR context mentions creating "external-secrets-operator-trusted-ca-bundle". If the names differ, the volume will mount an empty/non‑existent CM at runtime.

Apply this diff to align with the injected bundle name:

           configMap:
-            name: trusted-ca-bundle
+            name: external-secrets-operator-trusted-ca-bundle

Run this script to verify CM names across the repo and ensure consistency:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== kustomization in config/manager =="
fd -t f kustomization.yaml | rg -n '^config/manager/.*' -n
rg -n -C2 'configMapGenerator|name:\s*trusted-ca-bundle|external-secrets-operator-trusted-ca-bundle' config/manager/kustomization.yaml || true

echo "== CM manifests mentioning trusted CA bundle =="
fd -H -t f '.*(trusted|ca-bundle).*\.ya?ml' | rg -n -C2 'external-secrets-operator-trusted-ca-bundle|trusted-ca-bundle' || true

Length of output: 375


Align ConfigMap name to the injected bundle
Update both the volume patch and the configMapGenerator in the manager overlay to use the actual ConfigMap name:

  • In config/manager/trusted-ca-patch.yaml, change
                name: trusted-ca-bundle
    to
                name: external-secrets-operator-trusted-ca-bundle
  • In config/manager/kustomization.yaml’s configMapGenerator, change
    - name: trusted-ca-bundle
    to
    - name: external-secrets-operator-trusted-ca-bundle
🤖 Prompt for AI Agents
In config/manager/trusted-ca-patch.yaml around line 18 change the volume's name
from "trusted-ca-bundle" to "external-secrets-operator-trusted-ca-bundle"; also
update config/manager/kustomization.yaml (configMapGenerator) to rename the
generated ConfigMap from "trusted-ca-bundle" to
"external-secrets-operator-trusted-ca-bundle" so both the volume patch and the
generated ConfigMap names match the injected bundle.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
pkg/controller/external_secrets/deployments.go (5)

113-120: Bitwarden image required unconditionally and version label bug

  • getDeploymentObject fails for non-bitwarden assets when BITWARDEN image env is unset.
  • The version label is set to the env var name constant instead of its value.

Scope the bitwarden image requirement to the bitwarden asset and set the label to the actual version value.

Apply this diff:

   image := os.Getenv(externalsecretsImageEnvVarName)
   if image == "" {
     return nil, common.NewIrrecoverableError(fmt.Errorf("%s environment variable with externalsecrets image not set", externalsecretsImageEnvVarName), "failed to update image in %s deployment object", deployment.GetName())
   }
-  bitwardenImage := os.Getenv(bitwardenImageEnvVarName)
-  if bitwardenImage == "" {
-    return nil, common.NewIrrecoverableError(fmt.Errorf("%s environment variable with bitwarden-sdk-server image not set", bitwardenImageEnvVarName), "failed to update image in %s deployment object", deployment.GetName())
-  }
   logLevel := getLogLevel(esc.Spec)

   switch assetName {
   case controllerDeploymentAssetName:
     updateContainerSpec(deployment, esc, image, logLevel)
   case webhookDeploymentAssetName:
     updateWebhookContainerSpec(deployment, image, logLevel)
   case certControllerDeploymentAssetName:
     updateCertControllerContainerSpec(deployment, image, logLevel)
   case bitwardenDeploymentAssetName:
-    deployment.Labels["app.kubernetes.io/version"] = bitwardenImageVersionEnvVarName
-    updateBitwardenServerContainerSpec(deployment, bitwardenImage)
+    // Bitwarden image/env are only required for this asset.
+    bitwardenImage := os.Getenv(bitwardenImageEnvVarName)
+    if bitwardenImage == "" {
+      return nil, common.NewIrrecoverableError(fmt.Errorf("%s environment variable with bitwarden-sdk-server image not set", bitwardenImageEnvVarName), "failed to update image in %s deployment object", deployment.GetName())
+    }
+    if deployment.Labels == nil {
+      deployment.Labels = map[string]string{}
+    }
+    if v := os.Getenv(bitwardenImageVersionEnvVarName); v != "" {
+      deployment.Labels["app.kubernetes.io/version"] = v
+    }
+    updateBitwardenServerContainerSpec(deployment, bitwardenImage)
   }

Also applies to: 124-133


180-202: Nil deref risk: r.esm checks missing

Accesses r.esm.Spec.GlobalConfig without guarding r.esm != nil; can panic.

Apply this diff:

 func (r *Reconciler) updateResourceRequirement(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig) error {
   rscReqs := corev1.ResourceRequirements{}
   if esc.Spec.ApplicationConfig.Resources != nil {
     esc.Spec.ApplicationConfig.Resources.DeepCopyInto(&rscReqs)
-  } else if r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Resources != nil {
+  } else if r.esm != nil && r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Resources != nil {
     r.esm.Spec.GlobalConfig.Resources.DeepCopyInto(&rscReqs)
   } else {
     return nil
   }

211-231: Nil deref risk: r.esm checks missing (node selector)

Same issue here.

Apply this diff:

-  } else if r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.NodeSelector != nil {
+  } else if r.esm != nil && r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.NodeSelector != nil {
     nodeSelector = r.esm.Spec.GlobalConfig.NodeSelector
   }

233-253: Nil deref risk: r.esm checks missing (affinity)

Same issue here.

Apply this diff:

-  } else if r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Affinity != nil {
+  } else if r.esm != nil && r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Affinity != nil {
     affinity = r.esm.Spec.GlobalConfig.Affinity
   }

255-275: Nil deref risk: r.esm checks missing (tolerations)

Same issue here.

Apply this diff:

-  } else if r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Tolerations != nil {
+  } else if r.esm != nil && r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Tolerations != nil {
     tolerations = r.esm.Spec.GlobalConfig.Tolerations
   }
🧹 Nitpick comments (5)
bundle/manifests/external-secrets-operator.clusterserviceversion.yaml (1)

331-336: Minor wording nit in description

“an uniformed interface” → “a unified interface”.

Apply this diff:

-    an uniformed interface to fetch secrets stored in external providers like  AWS
+    a unified interface to fetch secrets stored in external providers like AWS
pkg/controller/external_secrets/deployments_test.go (3)

755-768: Tests compare env vars by order; consider order-insensitive comparison

DeepEqual requires exact ordering. Consider comparing by map keyed by Name to reduce brittleness.


770-787: Volumes check is strict; consider contains-based assertion

DeepEqual on full slice fails if unrelated volumes are present. Prefer asserting the presence and equality of expected volumes.


789-817: VolumeMounts check is strict; consider contains-based assertion per container

Filter is good; still DeepEqual enforces order. Consider sort-by-name or contains check.

pkg/controller/external_secrets/deployments.go (1)

461-522: Trusted CA bundle: make optional and mount for initContainers too

Avoid startup failures if the ConfigMap is not yet present; ensure initContainers also trust the CA.

Apply this diff:

   trustedCAVolume := corev1.Volume{
     Name: trustedCABundleVolumeName,
     VolumeSource: corev1.VolumeSource{
       ConfigMap: &corev1.ConfigMapVolumeSource{
         LocalObjectReference: corev1.LocalObjectReference{
           Name: trustedCABundleConfigMapName,
         },
+        Optional: ptr.To(true),
       },
     },
   }
@@
   for i := range deployment.Spec.Template.Spec.Containers {
     container := &deployment.Spec.Template.Spec.Containers[i]
@@
     if !volumeMountExists {
       container.VolumeMounts = append(container.VolumeMounts, trustedCAVolumeMount)
     }
   }
+
+  // Also mount in initContainers
+  for i := range deployment.Spec.Template.Spec.InitContainers {
+    c := &deployment.Spec.Template.Spec.InitContainers[i]
+    volumeMountExists := false
+    for j, vm := range c.VolumeMounts {
+      if vm.Name == trustedCABundleVolumeName {
+        c.VolumeMounts[j] = trustedCAVolumeMount
+        volumeMountExists = true
+        break
+      }
+    }
+    if !volumeMountExists {
+      c.VolumeMounts = append(c.VolumeMounts, trustedCAVolumeMount)
+    }
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 10effe4 and 8598570.

📒 Files selected for processing (4)
  • bundle/manifests/external-secrets-operator.clusterserviceversion.yaml (7 hunks)
  • config/manager/trusted-ca-patch.yaml (1 hunks)
  • pkg/controller/external_secrets/deployments.go (15 hunks)
  • pkg/controller/external_secrets/deployments_test.go (15 hunks)
🧰 Additional context used
🪛 Checkov (3.2.334)
config/manager/trusted-ca-patch.yaml

[medium] 1-18: Containers should not run with allowPrivilegeEscalation

(CKV_K8S_20)


[medium] 1-18: Minimize the admission of root containers

(CKV_K8S_23)

🔇 Additional comments (7)
config/manager/trusted-ca-patch.yaml (3)

5-5: Remove hardcoded namespace from the patch.

Delete Line 5 to keep the patch namespace-agnostic for overlays.

Apply this diff:

   metadata:
     name: controller-manager
-  namespace: system

16-18: Align ConfigMap name with injected bundle and make it optional.

Use the actual injected CM name and avoid startup failures if it’s not present yet.

Apply this diff:

       volumes:
         - name: trusted-ca-bundle
           configMap:
-            name: trusted-ca-bundle
+            name: external-secrets-operator-trusted-ca-bundle
+            optional: true
+            items:
+              - key: ca-bundle.crt
+                path: ca-bundle.crt

1-18: Verify base image CA path assumptions. No occurrences of SSL_CERT_FILE, REQUESTS_CA_BUNDLE, or alternate CA bundle filenames were found; confirm the controller image reads CAs from /etc/pki/tls/certs/ca-bundle.crt or adjust the mountPath or set SSL_CERT_FILE/REQUESTS_CA_BUNDLE to match its CA path.

bundle/manifests/external-secrets-operator.clusterserviceversion.yaml (3)

213-213: Proxy-aware annotation: good

Enables OLM to inject HTTP(S)_PROXY/NO_PROXY; aligns with the PR goal.


637-637: Pin operator image (avoid :latest) in Deployment

The install spec still uses :latest; pin to a digest to match supply‑chain best practices.

Apply this diff (replace with the real digest/tag):

-                image: openshift.io/external-secrets-operator:latest
+                image: openshift.io/external-secrets-operator@sha256:<digest>

206-206: Pin operator image (avoid :latest) in CSV metadata

Using a mutable tag harms reproducibility and compliance. Pin to an immutable digest.

Apply this diff (replace with the real digest/tag):

-    containerImage: openshift.io/external-secrets-operator:latest
+    containerImage: openshift.io/external-secrets-operator@sha256:<digest>
pkg/controller/external_secrets/deployments.go (1)

410-424: Proxy envs: add lowercase variants and cover initContainers

Many images honor lowercase envs; also initContainers need the same proxy envs. Keep “no change” behavior when no proxy is resolved to avoid breaking existing tests.

Apply this diff:

 func (r *Reconciler) updateProxyEnvironmentVariables(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig) error {
   proxyConfig := r.getProxyConfiguration(esc)
   if proxyConfig == nil {
     return nil
   }

-  // Apply proxy environment variables to all containers
-  for i := range deployment.Spec.Template.Spec.Containers {
-    container := &deployment.Spec.Template.Spec.Containers[i]
-    r.setProxyEnvVars(container, proxyConfig)
-  }
+  // Apply proxy environment variables to all containers and initContainers
+  for i := range deployment.Spec.Template.Spec.Containers {
+    r.setProxyEnvVars(&deployment.Spec.Template.Spec.Containers[i], proxyConfig)
+  }
+  for i := range deployment.Spec.Template.Spec.InitContainers {
+    r.setProxyEnvVars(&deployment.Spec.Template.Spec.InitContainers[i], proxyConfig)
+  }

   return nil
 }
@@
   // Set proxy environment variables
   setEnvVar("HTTP_PROXY", proxyConfig.HTTPProxy)
   setEnvVar("HTTPS_PROXY", proxyConfig.HTTPSProxy)
   setEnvVar("NO_PROXY", proxyConfig.NoProxy)
+  // Lowercase variants for broader compatibility
+  setEnvVar("http_proxy", proxyConfig.HTTPProxy)
+  setEnvVar("https_proxy", proxyConfig.HTTPSProxy)
+  setEnvVar("no_proxy", proxyConfig.NoProxy)
 }

Also applies to: 426-459

Comment on lines +12 to +15
- name: trusted-ca-bundle
mountPath: /etc/pki/tls/certs
readOnly: true
volumes:
Copy link

@coderabbitai coderabbitai bot Sep 25, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Do not mount over the entire /etc/pki/tls/certs (masks system certs). Mount only ca-bundle.crt via subPath.

Mounting the whole directory can break TLS trust by hiding system CA certs.

Apply this diff:

         - name: manager
           volumeMounts:
-            - name: trusted-ca-bundle
-              mountPath: /etc/pki/tls/certs
-              readOnly: true
+            - name: trusted-ca-bundle
+              mountPath: /etc/pki/tls/certs/ca-bundle.crt
+              subPath: ca-bundle.crt
+              readOnly: true
🤖 Prompt for AI Agents
In config/manager/trusted-ca-patch.yaml around lines 12-15, the pod currently
mounts the entire /etc/pki/tls/certs directory which masks system CA certs;
change the volumeMount to only mount the ca-bundle.crt file using subPath (e.g.
set mountPath to /etc/pki/tls/certs/ca-bundle.crt and add subPath:
ca-bundle.crt) and keep readOnly: true so the system certs remain available
while supplying the custom bundle.

Copy link
Member Author

Choose a reason for hiding this comment

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

Mounting as subPath will not automatically update the mounted value during configmap modification.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

OpenShift trusted CA bundle injection mounting best practices subPath vs directory mount automatic updates

💡 Result:

Short answer

  • Don’t use subPath when mounting an OpenShift “trusted CA” ConfigMap if you want automatic in‑place updates; subPath-mounted files are not updated in running pods. Mount the ConfigMap as a directory (or project the file into the directory) so kubelet’s ConfigMap/Secret update mechanism can refresh the bundle. [1][2][3]

Why

  • Kubernetes mounts files from ConfigMaps/Secrets using symlink/atomic‑replace techniques; subPath creates a separate filesystem mount that breaks that update mechanism so the file in the container won’t be replaced while the pod is running. This is a long‑standing limitation/behavior documented in issues and answers. [1][3]

OpenShift specifics / recommended approaches

  • Use OpenShift’s trusted CA injection pattern: create the empty ConfigMap with label config.openshift.io/inject-trusted-cabundle="true" and mount the resulting ConfigMap (with key ca-bundle.crt) into the container trust directory (for example /etc/pki/ca-trust/extracted/pem). Mount the ConfigMap as a directory (or with items that place ca-bundle.crt into the directory), not via subPath. That allows the cluster operators and kubelet to merge/inject and update the bundle correctly. [2][5]

If you must use a single-file mount or need immediate in‑pod refresh

  • Workarounds:
    • Use a directory mount and name the file inside the volume (items: key: ca-bundle.crt path: tls-ca-bundle.pem) instead of subPath. [2][5]
    • Have an initContainer or sidecar that copies the file from the mounted directory to the exact path your process expects, and implement a file-watcher/reload in the container to respond to changes (or restart the process when the file changes).
    • If you need an atomic reload across many pods, consider updating the Deployment/StatefulSet (rolling restart) after changing the ConfigMap, or use an operator that supports certificate/CA updates. [2][4]

Practical checklist

  • Put your CA in a ConfigMap with key ca-bundle.crt. [5]
  • Mount the ConfigMap as a directory under the process trust dir (avoid subPath). [2][5]
  • Confirm the app can pick up changed bundle (either via automatic file watching, restart, or sidecar copy+reload).
  • For cluster‑wide injection, use the OpenShift inject-trusted-cabundle label so cluster operators handle merging/injection. [2][5]

Sources

  1. Kubernetes issue and behavior discussion (subPath prevents ConfigMap/Secret updates). [GitHub kubernetes/kubernetes issue #50345 and related threads]. [1]
  2. OpenShift docs: Configuring a custom PKI / trusted CA bundle injection and mount examples. [2][5]
  3. Community Q&A noting subPath update behavior (StackOverflow). [3]

@chiragkyal You're absolutely correct! Using subPath would indeed prevent automatic ConfigMap updates—that's a known Kubernetes limitation where subPath creates a static filesystem mount that doesn't get refreshed when the ConfigMap changes.

Given this trade-off, here are better alternatives that preserve both automatic updates and system certificates:

Option 1: Use items to project only the CA bundle file (Recommended)

containers:
  - name: manager
    volumeMounts:
      - name: trusted-ca-bundle
        mountPath: /etc/pki/ca-trust/extracted/pem
        readOnly: true
volumes:
  - name: trusted-ca-bundle
    configMap:
      name: trusted-ca-bundle
      items:
        - key: ca-bundle.crt
          path: tls-ca-bundle.pem

This mounts the ConfigMap as a directory but projects only the specific key, allowing updates while not masking the entire system cert directory.

Option 2: Mount to a custom CA trust path

containers:
  - name: manager
    volumeMounts:
      - name: trusted-ca-bundle
        mountPath: /etc/ssl/certs/openshift-injected
        readOnly: true

This avoids masking system certs by using a separate directory, though you'd need to ensure the application can read CAs from this path.

Which approach works best depends on how the external-secrets-operator components consume CA certificates. Do they use the system CA bundle automatically, or would they need explicit configuration to pick up certificates from a custom path?

Copy link
Member Author

@chiragkyal chiragkyal Oct 6, 2025

Choose a reason for hiding this comment

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

Option 1: Use items to project only the CA bundle file (Recommended)

We are creating a blank configmap through OLM which won't have any items configured with it. Later CNO will inject the data field in the configmap. Now, without having the target items in the ConfigMap, the operator pods will never come up.

Copy link

Choose a reason for hiding this comment

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

@chiragkyal Excellent point! You're right that using items would cause pod startup failures when the ConfigMap key doesn't exist yet.

Here's the best solution for your scenario where the ConfigMap starts empty and gets populated later by CNO:

Recommended approach: Use optional: true and mount to the proper CA trust path

containers:
  - name: manager
    volumeMounts:
      - name: trusted-ca-bundle
        mountPath: /etc/pki/ca-trust/extracted/pem
        readOnly: true
volumes:
  - name: trusted-ca-bundle
    configMap:
      name: trusted-ca-bundle
      optional: true  # Allows pod to start even if ConfigMap is empty

This approach:

  • Allows pod startup even with an empty ConfigMap (due to optional: true)
  • Enables automatic updates when CNO injects the CA bundle (no subPath)
  • Doesn't mask system certs (uses /etc/pki/ca-trust/extracted/pem instead of /etc/pki/tls/certs)
  • Follows OpenShift patterns for custom CA injection

The /etc/pki/ca-trust/extracted/pem directory is the standard OpenShift location for PEM-formatted CA bundles, and most applications that use system CA trust will automatically pick up certificates from this location.

Alternative: If the application specifically needs certs under /etc/pki/tls/certs, you could:

  1. Mount to a custom path like /etc/pki/ca-trust/source/anchors or /custom-ca
  2. Configure the application to read from both system certs and the custom path
  3. Use the SSL_CERT_DIR or SSL_CERT_FILE environment variables to point to the merged bundle

Does /etc/pki/ca-trust/extracted/pem work for the external-secrets-operator components, or do they require a specific cert path?

Comment on lines 59 to 75
// ConfigMap exists, ensure it has the correct labels
// Do not update the data of the ConfigMap since it is managed by CNO
if existingConfigMap.Labels == nil {
existingConfigMap.Labels = make(map[string]string)
}

expectedLabels = getTrustedCABundleLabels(resourceLabels)
needsUpdate := false
for k, expectedValue := range expectedLabels {
if existingConfigMap.Labels[k] != expectedValue {
existingConfigMap.Labels[k] = expectedValue
needsUpdate = true
}
}

// Update the ConfigMap if any labels changed
if needsUpdate {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// ConfigMap exists, ensure it has the correct labels
// Do not update the data of the ConfigMap since it is managed by CNO
if existingConfigMap.Labels == nil {
existingConfigMap.Labels = make(map[string]string)
}
expectedLabels = getTrustedCABundleLabels(resourceLabels)
needsUpdate := false
for k, expectedValue := range expectedLabels {
if existingConfigMap.Labels[k] != expectedValue {
existingConfigMap.Labels[k] = expectedValue
needsUpdate = true
}
}
// Update the ConfigMap if any labels changed
if needsUpdate {
// ConfigMap exists, ensure it has the correct labels
// Do not update the data of the ConfigMap since it is managed by CNO
if exist && common.ObjectMetadataModified(configMap, existingConfigMap) {

Copy link
Member Author

Choose a reason for hiding this comment

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

func ObjectMetadataModified(desired, fetched client.Object) bool {
	return !reflect.DeepEqual(desired.GetLabels(), fetched.GetLabels())
}

I think ObjectMetadataModified function name is a bit misleading. Metadata holds a lot of other fields as well: annotations, resourceVersion, uid etc. which are prone to change.

And CNO is also adding it's own annotation openshift.io/owning-component: Networking / cluster-network-operator

If we can rename this function to something like ObjecLabelsModified, then this can be used here.


// setProxyEnvVars sets proxy environment variables on a container.
func (r *Reconciler) setProxyEnvVars(container *corev1.Container, proxyConfig *operatorv1alpha1.ProxyConfig) {
if proxyConfig == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should for proxy ENV vars in operator pod set by OLM, when cluster-wide egress proxy is configured.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, I could not understand it correctly.

We are doing the necessary checks in getProxyConfiguration function and feeding the value in proxyConfig before callingsetProxyEnvVars.

If proxyConfig is non-nil value here, then we will set the env vars in the operand pods.

In case we need any modification to this logic, let me know.

@openshift-merge-robot openshift-merge-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Oct 6, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (5)
config/manager/trusted-ca-patch.yaml (3)

5-5: Remove hardcoded namespace to support kustomize overlays.

The hardcoded namespace: system will cause the patch to miss the target Deployment when overlays change the namespace. Remove this line to make the patch namespace-agnostic.

Apply this diff:

   metadata:
     name: controller-manager
-    namespace: system

12-14: Mounting entire directory masks system CA certificates.

Mounting the entire /etc/pki/tls/certs directory will hide system CA certificates, breaking TLS trust for connections that rely on system CAs. Use subPath to mount only the ca-bundle.crt file.

Apply this diff:

           volumeMounts:
             - name: trusted-ca-bundle
-              mountPath: /etc/pki/tls/certs
+              mountPath: /etc/pki/tls/certs/ca-bundle.crt
+              subPath: ca-bundle.crt
               readOnly: true

18-18: ConfigMap name mismatch.

The volume references ConfigMap "trusted-ca-bundle", but the actual ConfigMap created is named "external-secrets-operator-trusted-ca-bundle" (as seen in the manifest and constants). This will cause the volume to mount an empty/non-existent ConfigMap at runtime.

Apply this diff:

           configMap:
-            name: trusted-ca-bundle
+            name: external-secrets-operator-trusted-ca-bundle
pkg/controller/external_secrets/utils.go (1)

127-155: Guard against nil ApplicationConfig to prevent panic.

Line 133 dereferences esc.Spec.ApplicationConfig without checking for nil, which will panic for CRs that omit ApplicationConfig and rely solely on ESM or OLM defaults.

Apply this diff:

 func (r *Reconciler) getProxyConfiguration(esc *operatorv1alpha1.ExternalSecretsConfig) *operatorv1alpha1.ProxyConfig {
 	var proxyConfig *operatorv1alpha1.ProxyConfig
 
 	// Check ExternalSecretsConfig first
-	if esc.Spec.ApplicationConfig.Proxy != nil { // TODO: check if esc.Spec.ApplicationConfig != nil is required
+	if esc.Spec.ApplicationConfig != nil && esc.Spec.ApplicationConfig.Proxy != nil {
 		proxyConfig = esc.Spec.ApplicationConfig.Proxy
 	} else if r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Proxy != nil {
pkg/controller/external_secrets/deployments.go (1)

413-460: Proxy env vars still linger and miss init containers

We still can’t turn the proxy off once it was set: when proxyConfig becomes nil (or a field goes empty), the old environment variables stay on the pod. We also never touch initContainers and only export upper-case keys, so several common images keep using stale settings. Please fold in cleanup + init container coverage and set both casings.

 func (r *Reconciler) updateProxyEnvironmentVariables(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig) error {
-	proxyConfig := r.getProxyConfiguration(esc)
-	if proxyConfig == nil {
-		return nil
-	}
-
-	// Apply proxy environment variables to all containers
-	for i := range deployment.Spec.Template.Spec.Containers {
-		container := &deployment.Spec.Template.Spec.Containers[i]
-		r.setProxyEnvVars(container, proxyConfig)
-	}
+	proxyConfig := r.getProxyConfiguration(esc)
+
+	process := func(envSetter func(*corev1.Container)) {
+		for i := range deployment.Spec.Template.Spec.Containers {
+			envSetter(&deployment.Spec.Template.Spec.Containers[i])
+		}
+		for i := range deployment.Spec.Template.Spec.InitContainers {
+			envSetter(&deployment.Spec.Template.Spec.InitContainers[i])
+		}
+	}
+	if proxyConfig == nil {
+		process(r.removeProxyEnvVars)
+		return nil
+	}
+
+	process(func(c *corev1.Container) { r.setProxyEnvVars(c, proxyConfig) })
 
 	return nil
 }
 
 func (r *Reconciler) setProxyEnvVars(container *corev1.Container, proxyConfig *operatorv1alpha1.ProxyConfig) {
@@
 	if container.Env == nil {
 		container.Env = []corev1.EnvVar{}
 	}
 
+	removeEnvVar := func(name string) {
+		for i := 0; i < len(container.Env); i++ {
+			if container.Env[i].Name == name {
+				container.Env = append(container.Env[:i], container.Env[i+1:]...)
+				i--
+			}
+		}
+	}
 	setEnvVar := func(name, value string) {
 		if value == "" {
-			return
+			removeEnvVar(name)
+			return
 		}
@@
 	// Set proxy environment variables
 	setEnvVar("HTTP_PROXY", proxyConfig.HTTPProxy)
 	setEnvVar("HTTPS_PROXY", proxyConfig.HTTPSProxy)
 	setEnvVar("NO_PROXY", proxyConfig.NoProxy)
+	setEnvVar("http_proxy", proxyConfig.HTTPProxy)
+	setEnvVar("https_proxy", proxyConfig.HTTPSProxy)
+	setEnvVar("no_proxy", proxyConfig.NoProxy)
 }
+
+// removeProxyEnvVars deletes all proxy-related env vars from a container.
+func (r *Reconciler) removeProxyEnvVars(container *corev1.Container) {
+	if len(container.Env) == 0 {
+		return
+	}
+	toDrop := map[string]struct{}{
+		"HTTP_PROXY": {}, "HTTPS_PROXY": {}, "NO_PROXY": {},
+		"http_proxy": {}, "https_proxy": {}, "no_proxy": {},
+	}
+	dst := container.Env[:0]
+	for _, env := range container.Env {
+		if _, drop := toDrop[env.Name]; drop {
+			continue
+		}
+		dst = append(dst, env)
+	}
+	container.Env = dst
+}

Please refresh the accompanying tests as well.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8598570 and 8008b91.

📒 Files selected for processing (13)
  • bundle/manifests/external-secrets-operator-trusted-ca-bundle_v1_configmap.yaml (1 hunks)
  • bundle/manifests/external-secrets-operator.clusterserviceversion.yaml (3 hunks)
  • config/default/manager_metrics_patch.yaml (0 hunks)
  • config/manager/kustomization.yaml (1 hunks)
  • config/manager/trusted-ca-patch.yaml (1 hunks)
  • config/manifests/bases/external-secrets-operator.clusterserviceversion.yaml (1 hunks)
  • pkg/controller/external_secrets/configmap.go (1 hunks)
  • pkg/controller/external_secrets/constants.go (1 hunks)
  • pkg/controller/external_secrets/controller.go (4 hunks)
  • pkg/controller/external_secrets/deployments.go (2 hunks)
  • pkg/controller/external_secrets/deployments_test.go (2 hunks)
  • pkg/controller/external_secrets/install_external_secrets.go (1 hunks)
  • pkg/controller/external_secrets/utils.go (2 hunks)
💤 Files with no reviewable changes (1)
  • config/default/manager_metrics_patch.yaml
🚧 Files skipped from review as they are similar to previous changes (3)
  • pkg/controller/external_secrets/configmap.go
  • config/manager/kustomization.yaml
  • bundle/manifests/external-secrets-operator.clusterserviceversion.yaml
🧰 Additional context used
🪛 Checkov (3.2.334)
config/manager/trusted-ca-patch.yaml

[medium] 1-18: Containers should not run with allowPrivilegeEscalation

(CKV_K8S_20)


[medium] 1-18: Minimize the admission of root containers

(CKV_K8S_23)

🔇 Additional comments (11)
config/manifests/bases/external-secrets-operator.clusterserviceversion.yaml (1)

16-16: LGTM! Proxy-aware mode enabled.

The annotation change correctly enables proxy-aware behavior for the operator, aligning with the PR objectives.

pkg/controller/external_secrets/constants.go (1)

52-65: LGTM! Constants are well-defined.

The new trusted CA bundle constants follow naming conventions and use appropriate values for OpenShift CA injection.

bundle/manifests/external-secrets-operator-trusted-ca-bundle_v1_configmap.yaml (1)

1-9: LGTM! ConfigMap correctly configured for CA injection.

The ConfigMap manifest is properly structured with the config.openshift.io/inject-trusted-cabundle: "true" label that triggers OpenShift's CNO to inject cluster-wide CA certificates.

pkg/controller/external_secrets/install_external_secrets.go (1)

85-88: LGTM! CA bundle ConfigMap ensured before deployments.

The call to ensureTrustedCABundleConfigMap is correctly placed before createOrApplyDeployments, ensuring the CA bundle ConfigMap exists when deployments need to mount it. Error handling is consistent with other reconciliation steps.

pkg/controller/external_secrets/deployments_test.go (2)

424-752: Test coverage looks comprehensive.

The test function covers important scenarios including proxy precedence, partial configurations, updates to existing environment variables, and no-change cases. The test structure is clear and maintainable.


754-837: Helper functions are well-designed.

The validation helper functions (validateEnvironmentVariables, validateVolumes, validateVolumeMounts, findContainer, filterTrustedCAMounts) provide clear separation of concerns and make the tests more readable.

pkg/controller/external_secrets/controller.go (5)

77-77: LGTM! ConfigMap added to managed resources.

Adding &corev1.ConfigMap{} to controllerManagedResources enables the controller to watch ConfigMap resources, which is necessary for trusted CA bundle management.


255-262: LGTM! Simplified mapFunc aligns with predicate-based filtering.

The simplified mapFunc that always enqueues reconciliation is correct because the predicate now handles all filtering logic. Objects that reach mapFunc have already been filtered by the appropriate predicates.


270-280: LGTM! ConfigMap predicate correctly handles both managed and CA bundle ConfigMaps.

The configMapPredicate properly filters ConfigMaps to include:

  1. Managed resources (with the standard label)
  2. The trusted CA bundle ConfigMap (by name)

This allows the controller to reconcile when the trusted CA bundle ConfigMap is created or updated, even if it doesn't have the managed resource label.


296-297: LGTM! ConfigMap watch uses the correct predicate.

The ConfigMap watch correctly uses configMapResourcePredicate instead of managedResourcePredicate, enabling reconciliation for both managed ConfigMaps and the trusted CA bundle ConfigMap.


467-467: TODO: Consider implementing CA bundle ConfigMap cleanup.

The TODO comment correctly identifies that cleanup logic for the trusted CA bundle ConfigMap should be added to the cleanUp function. When the ExternalSecretsConfig is deleted, the associated CA bundle ConfigMap should also be removed if no longer needed.

Consider implementing the cleanup logic to remove the trusted CA bundle ConfigMap when appropriate, or open a follow-up issue to track this work.

Comment on lines 465 to 519
proxyConfig := r.getProxyConfiguration(esc)

// Only add trusted CA bundle if proxy is configured
if proxyConfig == nil {
return nil
}

// Add the trusted CA bundle volume to the pod spec
trustedCAVolume := corev1.Volume{
Name: trustedCABundleVolumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: trustedCABundleConfigMapName,
},
},
},
}

// Check if the volume already exists, if not add it
volumeExists := false
for i, volume := range deployment.Spec.Template.Spec.Volumes {
if volume.Name == trustedCABundleVolumeName {
deployment.Spec.Template.Spec.Volumes[i] = trustedCAVolume
volumeExists = true
break
}
}
if !volumeExists {
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, trustedCAVolume)
}

// Add volume mounts to all containers
trustedCAVolumeMount := corev1.VolumeMount{
Name: trustedCABundleVolumeName,
MountPath: trustedCABundleMountPath,
ReadOnly: true,
}

for i := range deployment.Spec.Template.Spec.Containers {
container := &deployment.Spec.Template.Spec.Containers[i]

// Check if the volume mount already exists, if not add it
volumeMountExists := false
for j, volumeMount := range container.VolumeMounts {
if volumeMount.Name == trustedCABundleVolumeName {
container.VolumeMounts[j] = trustedCAVolumeMount
volumeMountExists = true
break
}
}
if !volumeMountExists {
container.VolumeMounts = append(container.VolumeMounts, trustedCAVolumeMount)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Trusted CA volume needs symmetry and init-container coverage

Same problem on the CA bundle path: once added, the volume + mount never come off, and initContainers never see the mount. That keeps pods tied to a ConfigMap even after disabling the proxy, and starves any init container of the required CA. Let’s cleanly add/remove for every container type.

 func (r *Reconciler) updateTrustedCABundleVolumes(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig) error {
 	proxyConfig := r.getProxyConfiguration(esc)
 
-	// Only add trusted CA bundle if proxy is configured
-	if proxyConfig == nil {
-		return nil
-	}
+	removeVolumeMount := func(container *corev1.Container) {
+		for i := 0; i < len(container.VolumeMounts); i++ {
+			if container.VolumeMounts[i].Name == trustedCABundleVolumeName {
+				container.VolumeMounts = append(container.VolumeMounts[:i], container.VolumeMounts[i+1:]...)
+				i--
+			}
+		}
+	}
+	if proxyConfig == nil {
+		for i := range deployment.Spec.Template.Spec.Containers {
+			removeVolumeMount(&deployment.Spec.Template.Spec.Containers[i])
+		}
+		for i := range deployment.Spec.Template.Spec.InitContainers {
+			removeVolumeMount(&deployment.Spec.Template.Spec.InitContainers[i])
+		}
+		for i := 0; i < len(deployment.Spec.Template.Spec.Volumes); i++ {
+			if deployment.Spec.Template.Spec.Volumes[i].Name == trustedCABundleVolumeName {
+				deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes[:i], deployment.Spec.Template.Spec.Volumes[i+1:]...)
+				i--
+			}
+		}
+		return nil
+	}
@@
-	// Add volume mounts to all containers
-	trustedCAVolumeMount := corev1.VolumeMount{
+	trustedCAVolumeMount := corev1.VolumeMount{
 		Name:      trustedCABundleVolumeName,
 		MountPath: trustedCABundleMountPath,
 		ReadOnly:  true,
 	}
 
-	for i := range deployment.Spec.Template.Spec.Containers {
-		container := &deployment.Spec.Template.Spec.Containers[i]
-
-		// Check if the volume mount already exists, if not add it
-		volumeMountExists := false
-		for j, volumeMount := range container.VolumeMounts {
-			if volumeMount.Name == trustedCABundleVolumeName {
-				container.VolumeMounts[j] = trustedCAVolumeMount
-				volumeMountExists = true
-				break
-			}
-		}
-		if !volumeMountExists {
-			container.VolumeMounts = append(container.VolumeMounts, trustedCAVolumeMount)
-		}
-	}
+	addMount := func(container *corev1.Container) {
+		for j, volumeMount := range container.VolumeMounts {
+			if volumeMount.Name == trustedCABundleVolumeName {
+				container.VolumeMounts[j] = trustedCAVolumeMount
+				return
+			}
+		}
+		container.VolumeMounts = append(container.VolumeMounts, trustedCAVolumeMount)
+	}
+	for i := range deployment.Spec.Template.Spec.Containers {
+		addMount(&deployment.Spec.Template.Spec.Containers[i])
+	}
+	for i := range deployment.Spec.Template.Spec.InitContainers {
+		addMount(&deployment.Spec.Template.Spec.InitContainers[i])
+	}
 
 	return nil
 }

Remember to mirror the updated behaviour in tests.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
proxyConfig := r.getProxyConfiguration(esc)
// Only add trusted CA bundle if proxy is configured
if proxyConfig == nil {
return nil
}
// Add the trusted CA bundle volume to the pod spec
trustedCAVolume := corev1.Volume{
Name: trustedCABundleVolumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: trustedCABundleConfigMapName,
},
},
},
}
// Check if the volume already exists, if not add it
volumeExists := false
for i, volume := range deployment.Spec.Template.Spec.Volumes {
if volume.Name == trustedCABundleVolumeName {
deployment.Spec.Template.Spec.Volumes[i] = trustedCAVolume
volumeExists = true
break
}
}
if !volumeExists {
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, trustedCAVolume)
}
// Add volume mounts to all containers
trustedCAVolumeMount := corev1.VolumeMount{
Name: trustedCABundleVolumeName,
MountPath: trustedCABundleMountPath,
ReadOnly: true,
}
for i := range deployment.Spec.Template.Spec.Containers {
container := &deployment.Spec.Template.Spec.Containers[i]
// Check if the volume mount already exists, if not add it
volumeMountExists := false
for j, volumeMount := range container.VolumeMounts {
if volumeMount.Name == trustedCABundleVolumeName {
container.VolumeMounts[j] = trustedCAVolumeMount
volumeMountExists = true
break
}
}
if !volumeMountExists {
container.VolumeMounts = append(container.VolumeMounts, trustedCAVolumeMount)
}
}
proxyConfig := r.getProxyConfiguration(esc)
removeVolumeMount := func(container *corev1.Container) {
for i := 0; i < len(container.VolumeMounts); i++ {
if container.VolumeMounts[i].Name == trustedCABundleVolumeName {
container.VolumeMounts = append(container.VolumeMounts[:i], container.VolumeMounts[i+1:]...)
i--
}
}
}
if proxyConfig == nil {
for i := range deployment.Spec.Template.Spec.Containers {
removeVolumeMount(&deployment.Spec.Template.Spec.Containers[i])
}
for i := range deployment.Spec.Template.Spec.InitContainers {
removeVolumeMount(&deployment.Spec.Template.Spec.InitContainers[i])
}
for i := 0; i < len(deployment.Spec.Template.Spec.Volumes); i++ {
if deployment.Spec.Template.Spec.Volumes[i].Name == trustedCABundleVolumeName {
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes[:i], deployment.Spec.Template.Spec.Volumes[i+1:]...)
i--
}
}
return nil
}
// Add the trusted CA bundle volume to the pod spec
trustedCAVolume := corev1.Volume{
Name: trustedCABundleVolumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: trustedCABundleConfigMapName,
},
},
},
}
// Check if the volume already exists, if not add it
volumeExists := false
for i, volume := range deployment.Spec.Template.Spec.Volumes {
if volume.Name == trustedCABundleVolumeName {
deployment.Spec.Template.Spec.Volumes[i] = trustedCAVolume
volumeExists = true
break
}
}
if !volumeExists {
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, trustedCAVolume)
}
trustedCAVolumeMount := corev1.VolumeMount{
Name: trustedCABundleVolumeName,
MountPath: trustedCABundleMountPath,
ReadOnly: true,
}
addMount := func(container *corev1.Container) {
for j, volumeMount := range container.VolumeMounts {
if volumeMount.Name == trustedCABundleVolumeName {
container.VolumeMounts[j] = trustedCAVolumeMount
return
}
}
container.VolumeMounts = append(container.VolumeMounts, trustedCAVolumeMount)
}
for i := range deployment.Spec.Template.Spec.Containers {
addMount(&deployment.Spec.Template.Spec.Containers[i])
}
for i := range deployment.Spec.Template.Spec.InitContainers {
addMount(&deployment.Spec.Template.Spec.InitContainers[i])
}
return nil

Signed-off-by: chiragkyal <[email protected]>
@openshift-merge-robot openshift-merge-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Oct 6, 2025
@openshift-merge-robot
Copy link

PR needs rebase.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (3)
pkg/controller/external_secrets/deployments_test.go (1)

431-431: Verify ConfigMap name matches the manifest.

Based on a previous review comment, the ConfigMap name should be "external-secrets-operator-trusted-ca-bundle" to match the OLM manifest at bundle/manifests/external-secrets-operator-trusted-ca-bundle_v1_configmap.yaml. The current value "external-secrets-trusted-ca-bundle" may cause runtime failures.

Run the following script to verify the ConfigMap name across the codebase:

#!/bin/bash
# Search for all references to the trusted CA bundle ConfigMap name
rg -n 'external-secrets.*trusted-ca-bundle' --type yaml --type go
pkg/controller/external_secrets/deployments.go (2)

412-432: Missing cleanup when proxy configuration is disabled.

When proxyConfig is nil, the function returns early without removing previously set proxy environment variables. This means once proxy is configured, it cannot be disabled by removing the configuration. The same issue exists in updateTrustedCABundleVolumes for CA bundle volumes/mounts.

To support proxy removal, the function should clean up when proxyConfig is nil:

 func (r *Reconciler) updateProxyEnvironmentVariables(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig) error {
 	proxyConfig := r.getProxyConfiguration(esc)
-	if proxyConfig == nil {
-		return nil
-	}
 
-	// Apply proxy environment variables to all containers
-	for i := range deployment.Spec.Template.Spec.Containers {
-		container := &deployment.Spec.Template.Spec.Containers[i]
-		r.setProxyEnvVars(container, proxyConfig)
-	}
-
-	// Apply proxy environment variables to all init containers
-	for i := range deployment.Spec.Template.Spec.InitContainers {
-		initContainer := &deployment.Spec.Template.Spec.InitContainers[i]
-		r.setProxyEnvVars(initContainer, proxyConfig)
-	}
+	process := func(c *corev1.Container) {
+		if proxyConfig == nil {
+			r.removeProxyEnvVars(c)
+		} else {
+			r.setProxyEnvVars(c, proxyConfig)
+		}
+	}
+	
+	for i := range deployment.Spec.Template.Spec.Containers {
+		process(&deployment.Spec.Template.Spec.Containers[i])
+	}
+	for i := range deployment.Spec.Template.Spec.InitContainers {
+		process(&deployment.Spec.Template.Spec.InitContainers[i])
+	}
 
 	return nil
 }

Add the removal helper:

// removeProxyEnvVars removes all proxy-related environment variables from a container.
func (r *Reconciler) removeProxyEnvVars(container *corev1.Container) {
	if len(container.Env) == 0 {
		return
	}
	proxyEnvVars := map[string]struct{}{
		"HTTP_PROXY": {}, "HTTPS_PROXY": {}, "NO_PROXY": {},
		"http_proxy": {}, "https_proxy": {}, "no_proxy": {},
	}
	filtered := container.Env[:0]
	for _, e := range container.Env {
		if _, isProxy := proxyEnvVars[e.Name]; !isProxy {
			filtered = append(filtered, e)
		}
	}
	container.Env = filtered
}

473-528: Missing cleanup of CA bundle volumes when proxy is disabled.

Similar to the environment variables issue, when proxyConfig is nil, the function returns early without removing the trusted CA bundle volume and its mounts. This leaves stale volumes mounted even after proxy configuration is removed.

Apply symmetric cleanup logic:

 func (r *Reconciler) updateTrustedCABundleVolumes(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig) error {
 	proxyConfig := r.getProxyConfiguration(esc)
 
-	// Only add trusted CA bundle if proxy is configured
-	if proxyConfig == nil {
-		return nil
-	}
+	removeVolumeMount := func(container *corev1.Container) {
+		filtered := container.VolumeMounts[:0]
+		for _, vm := range container.VolumeMounts {
+			if vm.Name != trustedCABundleVolumeName {
+				filtered = append(filtered, vm)
+			}
+		}
+		container.VolumeMounts = filtered
+	}
+	
+	if proxyConfig == nil {
+		// Remove volume mounts from all containers and init containers
+		for i := range deployment.Spec.Template.Spec.Containers {
+			removeVolumeMount(&deployment.Spec.Template.Spec.Containers[i])
+		}
+		for i := range deployment.Spec.Template.Spec.InitContainers {
+			removeVolumeMount(&deployment.Spec.Template.Spec.InitContainers[i])
+		}
+		// Remove the volume itself
+		filtered := deployment.Spec.Template.Spec.Volumes[:0]
+		for _, v := range deployment.Spec.Template.Spec.Volumes {
+			if v.Name != trustedCABundleVolumeName {
+				filtered = append(filtered, v)
+			}
+		}
+		deployment.Spec.Template.Spec.Volumes = filtered
+		return nil
+	}
 
 	// Add the trusted CA bundle volume to the pod spec
 	trustedCAVolume := corev1.Volume{
🧹 Nitpick comments (1)
pkg/controller/external_secrets/deployments_test.go (1)

424-830: Add test case for proxy removal scenario.

The test suite comprehensively covers proxy configuration being added and updated, but lacks a test case for when proxy configuration is removed (i.e., when proxyConfig transitions from being set to nil). This scenario should verify that proxy environment variables and CA bundle volumes/mounts are cleaned up.

Add a test case similar to:

{
    name: "Proxy removal cleans up environment variables and volumes",
    deployment: &appsv1.Deployment{
        Spec: appsv1.DeploymentSpec{
            Template: corev1.PodTemplateSpec{
                Spec: corev1.PodSpec{
                    Containers: []corev1.Container{
                        {
                            Name: "external-secrets",
                            Env: []corev1.EnvVar{
                                {Name: "HTTP_PROXY", Value: "http://old-proxy:8080"},
                                {Name: "EXISTING_VAR", Value: "existing-value"},
                            },
                        },
                    },
                    Volumes: []corev1.Volume{
                        {
                            Name: "trusted-ca-bundle",
                            VolumeSource: corev1.VolumeSource{
                                ConfigMap: &corev1.ConfigMapVolumeSource{
                                    LocalObjectReference: corev1.LocalObjectReference{
                                        Name: "external-secrets-trusted-ca-bundle",
                                    },
                                },
                            },
                        },
                    },
                },
            },
        },
    },
    externalSecretsConfig:  &v1alpha1.ExternalSecretsConfig{},
    externalSecretsManager: &v1alpha1.ExternalSecretsManager{},
    olmEnvVars:             map[string]string{},
    expectedContainerEnvVars: map[string][]corev1.EnvVar{
        "external-secrets": {
            {Name: "EXISTING_VAR", Value: "existing-value"},
        },
    },
    expectedVolumes:      []corev1.Volume{},
    expectedVolumeMounts: map[string][]corev1.VolumeMount{},
},
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8008b91 and 898e3c9.

📒 Files selected for processing (5)
  • pkg/controller/external_secrets/configmap.go (1 hunks)
  • pkg/controller/external_secrets/constants.go (1 hunks)
  • pkg/controller/external_secrets/deployments.go (3 hunks)
  • pkg/controller/external_secrets/deployments_test.go (2 hunks)
  • pkg/controller/external_secrets/utils.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/controller/external_secrets/constants.go
  • pkg/controller/external_secrets/configmap.go
🔇 Additional comments (5)
pkg/controller/external_secrets/utils.go (1)

127-155: LGTM! Proxy configuration precedence is correctly implemented.

The three-tier precedence (ExternalSecretsConfig → ExternalSecretsManager → OLM environment variables) is clearly implemented and aligns with the design. The nil checks for nested fields are appropriate given that ApplicationConfig is a struct and r.esm is always initialized.

pkg/controller/external_secrets/deployments_test.go (1)

832-922: LGTM! Well-structured test helpers.

The validation helper functions are well-organized and provide clear, detailed error messages for test failures. The separation of concerns (environment variables, volumes, volume mounts) makes the tests easy to understand and maintain.

pkg/controller/external_secrets/deployments.go (3)

400-410: LGTM! Clean orchestration of proxy configuration updates.

The function correctly delegates to specialized handlers for environment variables and CA bundle volumes, with proper error handling and context.


434-471: LGTM! Comprehensive proxy environment variable handling.

The function correctly handles both uppercase and lowercase variants of proxy environment variables, which improves compatibility across different tools and libraries. The update-or-append logic avoids duplicates.


530-544: LGTM! Clean helper for adding volume mounts.

The function correctly checks for existing mounts and either updates or appends, avoiding duplicates.

Copy link

openshift-ci bot commented Oct 7, 2025

@chiragkyal: The following tests failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/verify 898e3c9 link true /test verify
ci/prow/e2e-operator 898e3c9 link true /test e2e-operator
ci/prow/images 898e3c9 link true /test images
ci/prow/fips-image-scan-external-secrets 898e3c9 link true /test fips-image-scan-external-secrets
ci/prow/ci-bundle-external-secrets-operator-bundle 898e3c9 link true /test ci-bundle-external-secrets-operator-bundle
ci/prow/ci-index-external-secrets-operator-bundle 898e3c9 link true /test ci-index-external-secrets-operator-bundle
ci/prow/unit 898e3c9 link true /test unit
ci/prow/fips-image-scan-operator 898e3c9 link true /test fips-image-scan-operator

Full PR test history. Your PR dashboard.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants