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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: v1
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/managed-by: kustomize
app.kubernetes.io/name: external-secrets-operator
config.openshift.io/inject-trusted-cabundle: "true"
control-plane: controller-manager
name: external-secrets-operator-trusted-ca-bundle
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,13 @@ metadata:
categories: Security
console.openshift.io/disable-operand-delete: "true"
containerImage: openshift.io/external-secrets-operator:latest
createdAt: "2025-10-07T03:20:14Z"
createdAt: "2025-10-07T09:42:32Z"
features.operators.openshift.io/cnf: "false"
features.operators.openshift.io/cni: "false"
features.operators.openshift.io/csi: "false"
features.operators.openshift.io/disconnected: "false"
features.operators.openshift.io/fips-compliant: "true"
features.operators.openshift.io/proxy-aware: "false"
features.operators.openshift.io/proxy-aware: "true"
features.operators.openshift.io/tls-profiles: "false"
features.operators.openshift.io/token-auth-aws: "false"
features.operators.openshift.io/token-auth-azure: "false"
Expand Down Expand Up @@ -763,6 +763,9 @@ spec:
seccompProfile:
type: RuntimeDefault
volumeMounts:
- mountPath: /etc/pki/tls/certs
name: trusted-ca-bundle
readOnly: true
- mountPath: /etc/metrics-certs
name: metrics-serving-cert
readOnly: true
Expand All @@ -773,6 +776,9 @@ spec:
serviceAccountName: external-secrets-operator-controller-manager
terminationGracePeriodSeconds: 10
volumes:
- configMap:
name: external-secrets-operator-trusted-ca-bundle
name: trusted-ca-bundle
- name: metrics-serving-cert
secret:
secretName: metrics-serving-cert
Expand Down
6 changes: 0 additions & 6 deletions config/default/manager_metrics_patch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,12 @@
- op: add
path: /spec/template/spec/containers/0/args/-
value: --metrics-cert-dir=/etc/metrics-certs
- op: add
path: /spec/template/spec/containers/0/volumeMounts
value: []
- op: add
path: /spec/template/spec/containers/0/volumeMounts/-
value:
name: metrics-serving-cert
mountPath: /etc/metrics-certs
readOnly: true
- op: add
path: /spec/template/spec/volumes
value: []
- op: add
path: /spec/template/spec/volumes/-
value:
Expand Down
15 changes: 15 additions & 0 deletions config/manager/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,18 @@ images:
- name: controller
newName: openshift.io/external-secrets-operator
newTag: latest
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: trusted-ca-bundle
options:
labels:
app.kubernetes.io/managed-by: kustomize
app.kubernetes.io/name: external-secrets-operator
config.openshift.io/inject-trusted-cabundle: "true"
control-plane: controller-manager
patches:
- path: trusted-ca-patch.yaml
target:
kind: Deployment
name: controller-manager
18 changes: 18 additions & 0 deletions config/manager/trusted-ca-patch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
spec:
template:
spec:
containers:
- name: manager
volumeMounts:
- name: trusted-ca-bundle
mountPath: /etc/pki/tls/certs
readOnly: true
volumes:
- name: trusted-ca-bundle
configMap:
name: trusted-ca-bundle
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ metadata:
features.operators.openshift.io/csi: "false"
features.operators.openshift.io/disconnected: "false"
features.operators.openshift.io/fips-compliant: "true"
features.operators.openshift.io/proxy-aware: "false"
features.operators.openshift.io/proxy-aware: "true"
features.operators.openshift.io/tls-profiles: "false"
features.operators.openshift.io/token-auth-aws: "false"
features.operators.openshift.io/token-auth-azure: "false"
Expand Down
127 changes: 108 additions & 19 deletions pkg/controller/common/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,22 +206,10 @@ func deploymentSpecModified(desired, fetched *appsv1.Deployment) bool {
return true
}

if desired.Spec.Template.Spec.Volumes != nil && len(desired.Spec.Template.Spec.Volumes) != len(fetched.Spec.Template.Spec.Volumes) {
// Check volumes
if !volumesEqual(desired.Spec.Template.Spec.Volumes, fetched.Spec.Template.Spec.Volumes) {
return true
}
for _, desiredVolume := range desired.Spec.Template.Spec.Volumes {
if desiredVolume.Secret != nil && desiredVolume.Secret.Items != nil {
for _, fetchedVolume := range fetched.Spec.Template.Spec.Volumes {
if !reflect.DeepEqual(desiredVolume.Secret.Items, fetchedVolume.Secret.Items) {
return true
}
if desiredVolume.Secret.SecretName != fetchedVolume.Secret.SecretName {
return true
}
}

}
}

if desired.Spec.Template.Spec.NodeSelector != nil && !reflect.DeepEqual(desired.Spec.Template.Spec.NodeSelector, fetched.Spec.Template.Spec.NodeSelector) {
return true
Expand All @@ -235,20 +223,62 @@ func deploymentSpecModified(desired, fetched *appsv1.Deployment) bool {
return true
}

// Check regular containers
if len(desired.Spec.Template.Spec.Containers) != len(fetched.Spec.Template.Spec.Containers) {
return true
}
fetchedContainers := make(map[string]*corev1.Container)
for i := range fetched.Spec.Template.Spec.Containers {
fetchedContainers[fetched.Spec.Template.Spec.Containers[i].Name] = &fetched.Spec.Template.Spec.Containers[i]
}
for i := range desired.Spec.Template.Spec.Containers {
desiredContainer := &desired.Spec.Template.Spec.Containers[i]
fetchedContainer, exists := fetchedContainers[desiredContainer.Name]
if !exists {
return true
}
if containerSpecModified(desiredContainer, fetchedContainer) {
return true
}
}

// Check init containers
if len(desired.Spec.Template.Spec.InitContainers) != len(fetched.Spec.Template.Spec.InitContainers) {
return true
}
fetchedInitContainers := make(map[string]*corev1.Container)
for i := range fetched.Spec.Template.Spec.InitContainers {
fetchedInitContainers[fetched.Spec.Template.Spec.InitContainers[i].Name] = &fetched.Spec.Template.Spec.InitContainers[i]
}
for i := range desired.Spec.Template.Spec.InitContainers {
desiredInitContainer := &desired.Spec.Template.Spec.InitContainers[i]
fetchedInitContainer, exists := fetchedInitContainers[desiredInitContainer.Name]
if !exists {
return true
}
if containerSpecModified(desiredInitContainer, fetchedInitContainer) {
return true
}
}

desiredContainer := desired.Spec.Template.Spec.Containers[0]
fetchedContainer := fetched.Spec.Template.Spec.Containers[0]
return false
}

func containerSpecModified(desiredContainer, fetchedContainer *corev1.Container) bool {
// Check basic container properties
if !reflect.DeepEqual(desiredContainer.Args, fetchedContainer.Args) ||
desiredContainer.Name != fetchedContainer.Name ||
desiredContainer.Image != fetchedContainer.Image ||
desiredContainer.ImagePullPolicy != fetchedContainer.ImagePullPolicy {
return true
}

// Check environment variables
if !reflect.DeepEqual(desiredContainer.Env, fetchedContainer.Env) {
return true
}

// Check ports
if len(desiredContainer.Ports) != len(fetchedContainer.Ports) {
return true
}
Expand Down Expand Up @@ -285,18 +315,77 @@ func deploymentSpecModified(desired, fetched *appsv1.Deployment) bool {
return true
}

if desiredContainer.VolumeMounts != nil && !reflect.DeepEqual(desiredContainer.VolumeMounts, fetchedContainer.VolumeMounts) {
// Check volume mounts
if !reflect.DeepEqual(desiredContainer.VolumeMounts, fetchedContainer.VolumeMounts) {
return true
}

if reflect.DeepEqual(desiredContainer.Resources, corev1.ResourceRequirements{}) &&
!reflect.DeepEqual(desiredContainer.Resources, fetchedContainer.Resources) {
// Check resources
if !reflect.DeepEqual(desiredContainer.Resources, fetchedContainer.Resources) {
return true
}

return false
}

func volumesEqual(desired, fetched []corev1.Volume) bool {
if len(desired) == 0 && len(fetched) == 0 {
return true
}
if len(desired) != len(fetched) {
return false
}

// Create a map of fetched volumes by name for easier lookup
fetchedMap := make(map[string]corev1.Volume)
for _, v := range fetched {
fetchedMap[v.Name] = v
}

// Check each desired volume exists and matches in fetched
for _, desiredVol := range desired {
fetchedVol, exists := fetchedMap[desiredVol.Name]
if !exists {
return false
}

// Compare volume sources
// Check ConfigMap volume
if desiredVol.ConfigMap != nil {
if fetchedVol.ConfigMap == nil {
return false
}
if desiredVol.ConfigMap.Name != fetchedVol.ConfigMap.Name {
return false
}
}

// Check Secret volume
if desiredVol.Secret != nil {
if fetchedVol.Secret == nil {
return false
}
if desiredVol.Secret.SecretName != fetchedVol.Secret.SecretName {
return false
}
if desiredVol.Secret.Items != nil && !reflect.DeepEqual(desiredVol.Secret.Items, fetchedVol.Secret.Items) {
return false
}
}

// Check EmptyDir volume
if desiredVol.EmptyDir != nil {
if fetchedVol.EmptyDir == nil {
return false
}
}

// Add other volume types as needed (PVC, HostPath, etc.)
}

return true
}
Comment on lines +331 to +387
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

volumesEqual misses fields and is asymmetric; may fail to detect differences

ConfigMap: ignores Items/defaultMode/Optional; Secret: Items check only when desired.Items != nil; EmptyDir: ignores Medium/SizeLimit. Type mismatches aren’t handled explicitly.

 func volumesEqual(desired, fetched []corev1.Volume) bool {
@@
-		// Compare volume sources
-		// Check ConfigMap volume
-		if desiredVol.ConfigMap != nil {
-			if fetchedVol.ConfigMap == nil {
-				return false
-			}
-			if desiredVol.ConfigMap.Name != fetchedVol.ConfigMap.Name {
-				return false
-			}
-		}
+		// Compare volume sources (type and key fields)
+		if (desiredVol.ConfigMap == nil) != (fetchedVol.ConfigMap == nil) {
+			return false
+		}
+		if desiredVol.ConfigMap != nil {
+			d, f := desiredVol.ConfigMap, fetchedVol.ConfigMap
+			if d.Name != f.Name || !boolPtrEqual(d.Optional, f.Optional) || !int32PtrEqual(d.DefaultMode, f.DefaultMode) {
+				return false
+			}
+			if !cmItemsEqual(d.Items, f.Items) {
+				return false
+			}
+		}
@@
-		// Check Secret volume
-		if desiredVol.Secret != nil {
-			if fetchedVol.Secret == nil {
-				return false
-			}
-			if desiredVol.Secret.SecretName != fetchedVol.Secret.SecretName {
-				return false
-			}
-			if desiredVol.Secret.Items != nil && !reflect.DeepEqual(desiredVol.Secret.Items, fetchedVol.Secret.Items) {
-				return false
-			}
-		}
+		if (desiredVol.Secret == nil) != (fetchedVol.Secret == nil) {
+			return false
+		}
+		if desiredVol.Secret != nil {
+			d, f := desiredVol.Secret, fetchedVol.Secret
+			if d.SecretName != f.SecretName || !boolPtrEqual(d.Optional, f.Optional) || !int32PtrEqual(d.DefaultMode, f.DefaultMode) {
+				return false
+			}
+			if !cmItemsEqual(d.Items, f.Items) { // same schema as ConfigMapKeySelector items
+				return false
+			}
+		}
@@
-		// Check EmptyDir volume
-		if desiredVol.EmptyDir != nil {
-			if fetchedVol.EmptyDir == nil {
-				return false
-			}
-		}
+		if (desiredVol.EmptyDir == nil) != (fetchedVol.EmptyDir == nil) {
+			return false
+		}
+		if desiredVol.EmptyDir != nil {
+			d, f := desiredVol.EmptyDir, fetchedVol.EmptyDir
+			if d.Medium != f.Medium {
+				return false
+			}
+			if (d.SizeLimit == nil) != (f.SizeLimit == nil) {
+				return false
+			}
+			if d.SizeLimit != nil && f.SizeLimit != nil && d.SizeLimit.Cmp(*f.SizeLimit) != 0 {
+				return false
+			}
+		}

Add helpers outside the function:

func boolPtrEqual(a, b *bool) bool { if a==nil || b==nil { return a==b }; return *a==*b }
func int32PtrEqual(a, b *int32) bool { if a==nil || b==nil { return a==b }; return *a==*b }
func cmItemsEqual(a, b []corev1.KeyToPath) bool {
	if len(a) != len(b) { return false }
	aa := append([]corev1.KeyToPath(nil), a...)
	bb := append([]corev1.KeyToPath(nil), b...)
	sort.Slice(aa, func(i, j int) bool { return aa[i].Key < aa[j].Key || (aa[i].Key==aa[j].Key && aa[i].Path < aa[j].Path) })
	sort.Slice(bb, func(i, j int) bool { return bb[i].Key < bb[j].Key || (bb[i].Key==bb[j].Key && bb[i].Path < bb[j].Path) })
	return equality.Semantic.DeepEqual(aa, bb)
}

Imports:

+	"sort"
+	"k8s.io/apimachinery/pkg/api/equality"
🤖 Prompt for AI Agents
In pkg/controller/common/utils.go around lines 331-387, volumesEqual is
asymmetric and misses key fields (ConfigMap: Items/defaultMode/Optional; Secret:
defaultMode/Optional and Items only checked when desired non-nil; EmptyDir:
Medium/SizeLimit) and doesn’t explicitly detect type mismatches; add helper
functions outside the function to compare pointer values (boolPtrEqual,
int32PtrEqual) and to compare KeyToPath slices in an order-insensitive way
(sort.Slice then equality.Semantic.DeepEqual), import sort and
k8s.io/apimachinery/pkg/api/equality, then update volumesEqual to: (1) treat nil
vs non-nil as unequal for each volume source type, (2) compare all relevant
fields for ConfigMap (Name, Items, DefaultMode, Optional) and Secret
(SecretName, Items, DefaultMode, Optional) using the helpers, (3) compare
EmptyDir Medium and SizeLimit, (4) explicitly handle other volume types and type
mismatches, and (5) use the KeyToPath comparator to ensure Items equality
regardless of order.


func serviceSpecModified(desired, fetched *corev1.Service) bool {
if desired.Spec.Type != fetched.Spec.Type ||
!reflect.DeepEqual(desired.Spec.Ports, fetched.Spec.Ports) ||
Expand Down
91 changes: 91 additions & 0 deletions pkg/controller/external_secrets/configmap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package external_secrets

import (
"fmt"

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

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

// ensureTrustedCABundleConfigMap creates or ensures the trusted CA bundle ConfigMap exists
// in the operand namespace when proxy configuration is present. The ConfigMap is labeled
// with the injection label required by the Cluster Network Operator (CNO), which watches
// for this label and injects the cluster's trusted CA bundle into the ConfigMap's data.
// This function ensures the correct labels are present so that CNO can manage the CA bundle
// content as expected.
func (r *Reconciler) ensureTrustedCABundleConfigMap(esc *operatorv1alpha1.ExternalSecretsConfig, resourceLabels map[string]string) error {
proxyConfig := r.getProxyConfiguration(esc)

// Only create ConfigMap if proxy is configured
if proxyConfig == nil {
// TODO: ConfigMap removal when proxy configuration is removed
// will be revisited in a follow-up implementation.
r.log.V(4).Info("no proxy configuration found, skipping trusted CA bundle ConfigMap creation")
return nil
}

namespace := getNamespace(esc)
expectedLabels := getTrustedCABundleLabels(resourceLabels)

desiredConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: trustedCABundleConfigMapName,
Namespace: namespace,
Labels: expectedLabels,
},
}

configMapName := fmt.Sprintf("%s/%s", desiredConfigMap.GetNamespace(), desiredConfigMap.GetName())
r.log.V(4).Info("reconciling trusted CA bundle ConfigMap resource", "name", configMapName)

// Check if the ConfigMap already exists
existingConfigMap := &corev1.ConfigMap{}
exist, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desiredConfigMap), existingConfigMap)
if err != nil {
return common.FromClientError(err, "failed to check %s trusted CA bundle ConfigMap resource already exists", configMapName)
}

if !exist {
// Create the ConfigMap
if err := r.Create(r.ctx, desiredConfigMap); err != nil {
return common.FromClientError(err, "failed to create %s trusted CA bundle ConfigMap resource", configMapName)
}
r.eventRecorder.Eventf(esc, corev1.EventTypeNormal, "Reconciled", "trusted CA bundle ConfigMap resource %s created", configMapName)
return nil
}

// ConfigMap exists, ensure it has the correct labels
// Do not update the data of the ConfigMap since it is managed by CNO
// Check if metadata (labels) has been modified.
// NOTE: Currently ObjectMetadataModified only checks labels, but if it's extended
// in the future to check annotations as well, CNO may race with this update since
// CNO adds `openshift.io/owning-component: Networking / cluster-network-operator` annotations on this ConfigMap.
if exist && common.ObjectMetadataModified(desiredConfigMap, existingConfigMap) {
r.log.V(1).Info("trusted CA bundle ConfigMap has been modified, updating to desired state", "name", configMapName)
// Update the labels since
existingConfigMap.Labels = desiredConfigMap.Labels

Comment on lines +31 to +71
Copy link

@coderabbitai coderabbitai bot Oct 9, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Use the CA-bundle injection annotation, not a label

CNO injects trust bundles only when the ConfigMap carries the annotation config.openshift.io/inject-trusted-cabundle: "true". Populating it under metadata.labels (as done here via getTrustedCABundleLabels) means the operand ConfigMap will never be patched with the cluster CA, so every proxy/TLS flow that relies on this bundle will fail. Please move the injection marker to metadata.annotations (and ensure the reconcile path preserves any annotations CNO adds) so the bundle actually gets injected.

Apply the following diff:

-	expectedLabels := getTrustedCABundleLabels(resourceLabels)
+	expectedLabels := maps.Clone(resourceLabels)
+	expectedAnnotations := map[string]string{
+		trustedCABundleInjectAnnotation: "true",
+	}
 
 	desiredConfigMap := &corev1.ConfigMap{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      trustedCABundleConfigMapName,
 			Namespace: namespace,
-			Labels:    expectedLabels,
+			Labels:    expectedLabels,
+			Annotations: expectedAnnotations,
 		},
 	}

Then, when the ConfigMap already exists, make sure you merge the annotation without dropping any others before calling UpdateWithRetry:

-	if exist && common.ObjectMetadataModified(desiredConfigMap, existingConfigMap) {
+	needsUpdate := common.ObjectMetadataModified(desiredConfigMap, existingConfigMap)
+
+	if existingConfigMap.Annotations == nil {
+		existingConfigMap.Annotations = map[string]string{}
+	}
+	if existingConfigMap.Annotations[trustedCABundleInjectAnnotation] != "true" {
+		existingConfigMap.Annotations[trustedCABundleInjectAnnotation] = "true"
+		needsUpdate = true
+	}
+
+	if needsUpdate {
 		r.log.V(1).Info("trusted CA bundle ConfigMap has been modified, updating to desired state", "name", configMapName)
-		// Update the labels since
+		// Update the labels
 		existingConfigMap.Labels = desiredConfigMap.Labels

(You'll need to import maps from the standard library and define trustedCABundleInjectAnnotation alongside the existing constants.)

📝 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
namespace := getNamespace(esc)
expectedLabels := getTrustedCABundleLabels(resourceLabels)
desiredConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: trustedCABundleConfigMapName,
Namespace: namespace,
Labels: expectedLabels,
},
}
configMapName := fmt.Sprintf("%s/%s", desiredConfigMap.GetNamespace(), desiredConfigMap.GetName())
r.log.V(4).Info("reconciling trusted CA bundle ConfigMap resource", "name", configMapName)
// Check if the ConfigMap already exists
existingConfigMap := &corev1.ConfigMap{}
exist, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desiredConfigMap), existingConfigMap)
if err != nil {
return common.FromClientError(err, "failed to check %s trusted CA bundle ConfigMap resource already exists", configMapName)
}
if !exist {
// Create the ConfigMap
if err := r.Create(r.ctx, desiredConfigMap); err != nil {
return common.FromClientError(err, "failed to create %s trusted CA bundle ConfigMap resource", configMapName)
}
r.eventRecorder.Eventf(esc, corev1.EventTypeNormal, "Reconciled", "trusted CA bundle ConfigMap resource %s created", configMapName)
return nil
}
// ConfigMap exists, ensure it has the correct labels
// Do not update the data of the ConfigMap since it is managed by CNO
// Check if metadata (labels) has been modified.
// NOTE: Currently ObjectMetadataModified only checks labels, but if it's extended
// in the future to check annotations as well, CNO may race with this update since
// CNO adds `openshift.io/owning-component: Networking / cluster-network-operator` annotations on this ConfigMap.
if exist && common.ObjectMetadataModified(desiredConfigMap, existingConfigMap) {
r.log.V(1).Info("trusted CA bundle ConfigMap has been modified, updating to desired state", "name", configMapName)
// Update the labels since
existingConfigMap.Labels = desiredConfigMap.Labels
namespace := getNamespace(esc)
// Use the base resourceLabels as labels, and set the injection annotation
expectedLabels := maps.Clone(resourceLabels)
expectedAnnotations := map[string]string{
trustedCABundleInjectAnnotation: "true",
}
desiredConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: trustedCABundleConfigMapName,
Namespace: namespace,
Labels: expectedLabels,
Annotations: expectedAnnotations,
},
}
configMapName := fmt.Sprintf("%s/%s", desiredConfigMap.GetNamespace(), desiredConfigMap.GetName())
r.log.V(4).Info("reconciling trusted CA bundle ConfigMap resource", "name", configMapName)
// Check if the ConfigMap already exists
existingConfigMap := &corev1.ConfigMap{}
exist, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desiredConfigMap), existingConfigMap)
if err != nil {
return common.FromClientError(err, "failed to check %s trusted CA bundle ConfigMap resource already exists", configMapName)
}
if !exist {
// Create the ConfigMap
if err := r.Create(r.ctx, desiredConfigMap); err != nil {
return common.FromClientError(err, "failed to create %s trusted CA bundle ConfigMap resource", configMapName)
}
r.eventRecorder.Eventf(esc, corev1.EventTypeNormal, "Reconciled", "trusted CA bundle ConfigMap resource %s created", configMapName)
return nil
}
// Determine if labels drifted, then ensure the inject annotation is present
needsUpdate := common.ObjectMetadataModified(desiredConfigMap, existingConfigMap)
if existingConfigMap.Annotations == nil {
existingConfigMap.Annotations = map[string]string{}
}
if existingConfigMap.Annotations[trustedCABundleInjectAnnotation] != "true" {
existingConfigMap.Annotations[trustedCABundleInjectAnnotation] = "true"
needsUpdate = true
}
if needsUpdate {
r.log.V(1).Info("trusted CA bundle ConfigMap has been modified, updating to desired state", "name", configMapName)
// Update the labels
existingConfigMap.Labels = desiredConfigMap.Labels
// (the update call goes here)
}
🤖 Prompt for AI Agents
In pkg/controller/external_secrets/configmap.go around lines 31 to 71, the code
currently puts the CNO CA-bundle injection marker into metadata.labels which
prevents CNO from injecting the trust bundle; change the implementation to place
the marker key "config.openshift.io/inject-trusted-cabundle" with value "true"
under desiredConfigMap.ObjectMeta.Annotations (define a constant
trustedCABundleInjectAnnotation), not Labels, and ensure you still set the
expected labels on ObjectMeta.Labels; when reconciling an existing ConfigMap
merge annotations rather than overwriting them (preserve any annotations CNO
added) — use the standard library maps helpers to clone/merge annotation maps so
existingConfigMap.Annotations = merged(existingConfigMap.Annotations,
desiredConfigMap.Annotations) before calling UpdateWithRetry, and keep updating
labels as before.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is not true, label is expected and not annotation.

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 Cluster Network Operator CNO CA bundle injection config.openshift.io/inject-trusted-cabundle label or annotation

💡 Result:

Use a label. The CNO (and other operators) request trusted-CA injection by creating an empty ConfigMap with the label
config.openshift.io/inject-trusted-cabundle="true". (Do not combine that label with the service.beta.openshift.io/inject-cabundle annotation — the latter is a different mechanism for service CA injection.) [1][2]


@bharath-b-rh You're absolutely right, and I apologize for the incorrect review comment.

The CNO does indeed use a label (config.openshift.io/inject-trusted-cabundle: "true"), not an annotation, to trigger CA bundle injection. The current implementation in the PR is correct. [1][2]

The suggestion to move it to annotations was incorrect and should be disregarded. Thank you for catching that!

if err := r.UpdateWithRetry(r.ctx, existingConfigMap); err != nil {
return common.FromClientError(err, "failed to update %s trusted CA bundle ConfigMap resource", configMapName)
}
r.eventRecorder.Eventf(esc, corev1.EventTypeNormal, "Reconciled", "trusted CA bundle ConfigMap resource %s reconciled back to desired state", configMapName)
} else {
r.log.V(4).Info("trusted CA bundle ConfigMap resource already exists and is in expected state", "name", configMapName)
}

return nil
}

// getTrustedCABundleLabels merges resource labels with the injection label
func getTrustedCABundleLabels(resourceLabels map[string]string) map[string]string {
labels := make(map[string]string)
for k, v := range resourceLabels {
labels[k] = v
}
labels[trustedCABundleInjectLabel] = "true"
return labels
}
Loading