diff --git a/bundle/manifests/external-secrets-operator-trusted-ca-bundle_v1_configmap.yaml b/bundle/manifests/external-secrets-operator-trusted-ca-bundle_v1_configmap.yaml new file mode 100644 index 00000000..c7259475 --- /dev/null +++ b/bundle/manifests/external-secrets-operator-trusted-ca-bundle_v1_configmap.yaml @@ -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 diff --git a/bundle/manifests/external-secrets-operator.clusterserviceversion.yaml b/bundle/manifests/external-secrets-operator.clusterserviceversion.yaml index 648188dc..2bd23ed3 100644 --- a/bundle/manifests/external-secrets-operator.clusterserviceversion.yaml +++ b/bundle/manifests/external-secrets-operator.clusterserviceversion.yaml @@ -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" @@ -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 @@ -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 diff --git a/config/default/manager_metrics_patch.yaml b/config/default/manager_metrics_patch.yaml index 98e648b5..ab1a403d 100644 --- a/config/default/manager_metrics_patch.yaml +++ b/config/default/manager_metrics_patch.yaml @@ -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: diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 2d385d2c..2066b0a1 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -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 diff --git a/config/manager/trusted-ca-patch.yaml b/config/manager/trusted-ca-patch.yaml new file mode 100644 index 00000000..40d023b7 --- /dev/null +++ b/config/manager/trusted-ca-patch.yaml @@ -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 diff --git a/config/manifests/bases/external-secrets-operator.clusterserviceversion.yaml b/config/manifests/bases/external-secrets-operator.clusterserviceversion.yaml index 7fbc25ad..5a3f5109 100644 --- a/config/manifests/bases/external-secrets-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/external-secrets-operator.clusterserviceversion.yaml @@ -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" diff --git a/pkg/controller/common/utils.go b/pkg/controller/common/utils.go index b9b29e15..c5c0b08b 100644 --- a/pkg/controller/common/utils.go +++ b/pkg/controller/common/utils.go @@ -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 @@ -235,13 +223,49 @@ 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 || @@ -249,6 +273,12 @@ func deploymentSpecModified(desired, fetched *appsv1.Deployment) bool { 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 } @@ -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 +} + func serviceSpecModified(desired, fetched *corev1.Service) bool { if desired.Spec.Type != fetched.Spec.Type || !reflect.DeepEqual(desired.Spec.Ports, fetched.Spec.Ports) || diff --git a/pkg/controller/external_secrets/configmap.go b/pkg/controller/external_secrets/configmap.go new file mode 100644 index 00000000..e8ec2433 --- /dev/null +++ b/pkg/controller/external_secrets/configmap.go @@ -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 + + 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 +} diff --git a/pkg/controller/external_secrets/constants.go b/pkg/controller/external_secrets/constants.go index 389199fa..caa117c6 100644 --- a/pkg/controller/external_secrets/constants.go +++ b/pkg/controller/external_secrets/constants.go @@ -48,6 +48,30 @@ const ( // externalsecretsDefaultNamespace is the namespace where the `external-secrets` operand required resources // will be created, when ExternalSecretsConfig.Spec.Namespace is not set. externalsecretsDefaultNamespace = "external-secrets" + + // trustedCABundleConfigMapName is the name of the ConfigMap containing the trusted CA bundle + trustedCABundleConfigMapName = externalsecretsCommonName + "-trusted-ca-bundle" + + // trustedCABundleInjectLabel is the label that triggers OpenShift CNO to inject cluster-wide CA certificates + trustedCABundleInjectLabel = "config.openshift.io/inject-trusted-cabundle" + + // trustedCABundleVolumeName is the name of the volume for mounting the CA bundle + trustedCABundleVolumeName = "trusted-ca-bundle" + + // trustedCABundleMountPath is the path where the CA bundle should be mounted in containers + // Default certificate path is taken from the golang source: + // https://cs.opensource.google/go/go/+/refs/tags/go1.24.4:src/crypto/x509/root_linux.go;l=22 + trustedCABundleMountPath = "/etc/pki/tls/certs" + + // Proxy environment variable names (uppercase) + httpProxyEnvVar = "HTTP_PROXY" + httpsProxyEnvVar = "HTTPS_PROXY" + noProxyEnvVar = "NO_PROXY" + + // Proxy environment variable names (lowercase) - required for compatibility with some applications + httpProxyEnvVarLowercase = "http_proxy" + httpsProxyEnvVarLowercase = "https_proxy" + noProxyEnvVarLowercase = "no_proxy" ) var ( diff --git a/pkg/controller/external_secrets/controller.go b/pkg/controller/external_secrets/controller.go index 4a73bc7f..74939cef 100644 --- a/pkg/controller/external_secrets/controller.go +++ b/pkg/controller/external_secrets/controller.go @@ -74,6 +74,7 @@ var ( &corev1.Secret{}, &corev1.Service{}, &corev1.ServiceAccount{}, + &corev1.ConfigMap{}, &webhook.ValidatingWebhookConfiguration{}, } ) @@ -251,26 +252,21 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { mapFunc := func(ctx context.Context, obj client.Object) []reconcile.Request { r.log.V(4).Info("received reconcile event", "object", fmt.Sprintf("%T", obj), "name", obj.GetName(), "namespace", obj.GetNamespace()) - objLabels := obj.GetLabels() - if objLabels != nil { - if objLabels[requestEnqueueLabelKey] == requestEnqueueLabelValue { - return []reconcile.Request{ - { - NamespacedName: types.NamespacedName{ - Name: common.ExternalSecretsConfigObjectName, - }, - }, - } - } + // Since predicate already filtered, all objects that reach here should trigger reconciliation + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: common.ExternalSecretsConfigObjectName, + }, + }, } - r.log.V(4).Info("object not of interest, ignoring reconcile event", "object", fmt.Sprintf("%T", obj), "name", obj.GetName(), "namespace", obj.GetNamespace()) - return []reconcile.Request{} } // predicate function to ignore events for objects not managed by controller. managedResources := predicate.NewPredicateFuncs(func(object client.Object) bool { return object.GetLabels() != nil && object.GetLabels()[requestEnqueueLabelKey] == requestEnqueueLabelValue }) + withIgnoreStatusUpdatePredicates := builder.WithPredicates(predicate.GenerationChangedPredicate{}, managedResources) managedResourcePredicate := builder.WithPredicates(managedResources) @@ -286,8 +282,8 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { if _, ok := r.optionalResourcesList[certificateCRDGKV]; ok { mgrBuilder.Watches(res, handler.EnqueueRequestsFromMapFunc(mapFunc), managedResourcePredicate) } - case &corev1.Secret{}: - mgrBuilder.WatchesMetadata(res, handler.EnqueueRequestsFromMapFunc(mapFunc), builder.WithPredicates(predicate.LabelChangedPredicate{})) + case &corev1.Secret{}, &corev1.ConfigMap{}: + mgrBuilder.WatchesMetadata(res, handler.EnqueueRequestsFromMapFunc(mapFunc), builder.WithPredicates(predicate.LabelChangedPredicate{}, managedResources)) default: mgrBuilder.Watches(res, handler.EnqueueRequestsFromMapFunc(mapFunc), managedResourcePredicate) } diff --git a/pkg/controller/external_secrets/deployments.go b/pkg/controller/external_secrets/deployments.go index 113b1285..ff3e0e96 100644 --- a/pkg/controller/external_secrets/deployments.go +++ b/pkg/controller/external_secrets/deployments.go @@ -143,6 +143,9 @@ func (r *Reconciler) getDeploymentObject(assetName string, esc *operatorv1alpha1 if err := r.updateNodeSelector(deployment, esc); err != nil { return nil, fmt.Errorf("failed to update node selector: %w", err) } + if err := r.updateProxyConfiguration(deployment, esc); err != nil { + return nil, fmt.Errorf("failed to update proxy configuration: %w", err) + } return deployment, nil } @@ -423,3 +426,223 @@ func updateSecretVolumeConfig(deployment *appsv1.Deployment, volumeName, secretN }) } } + +// updateProxyConfiguration applies all proxy-related configuration to the deployment. +func (r *Reconciler) updateProxyConfiguration(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig) error { + proxyConfig := r.getProxyConfiguration(esc) + + if err := r.updateProxyEnvironmentVariables(deployment, proxyConfig); err != nil { + return fmt.Errorf("failed to update proxy environment variables: %w", err) + } + if err := r.updateTrustedCABundleVolumes(deployment, proxyConfig); err != nil { + return fmt.Errorf("failed to update trusted CA bundle volumes: %w", err) + } + + return nil +} + +// updateProxyEnvironmentVariables sets or removes proxy environment variables on all containers and init containers in the deployment. +func (r *Reconciler) updateProxyEnvironmentVariables(deployment *appsv1.Deployment, proxyConfig *operatorv1alpha1.ProxyConfig) error { + // Apply proxy environment variables to all containers + for i := range deployment.Spec.Template.Spec.Containers { + container := &deployment.Spec.Template.Spec.Containers[i] + if proxyConfig != nil { + r.setProxyEnvVars(container, proxyConfig) + } else { + r.removeProxyEnvVars(container) + } + } + + // Apply proxy environment variables to all init containers + for i := range deployment.Spec.Template.Spec.InitContainers { + initContainer := &deployment.Spec.Template.Spec.InitContainers[i] + if proxyConfig != nil { + r.setProxyEnvVars(initContainer, proxyConfig) + } else { + r.removeProxyEnvVars(initContainer) + } + } + + return nil +} + +// setProxyEnvVars sets proxy environment variables on a container. +func (r *Reconciler) setProxyEnvVars(container *corev1.Container, proxyConfig *operatorv1alpha1.ProxyConfig) { + if proxyConfig == nil { + return + } + if container.Env == nil { + container.Env = []corev1.EnvVar{} + } + + setEnvVar := func(name, value string) { + if value == "" { + return + } + + // Check if the environment variable already exists + for i, env := range container.Env { + if env.Name == name { + container.Env[i].Value = value + return + } + } + + // Add new environment variable if it doesn't exist + container.Env = append(container.Env, corev1.EnvVar{ + Name: name, + Value: value, + }) + } + + // Set proxy environment variables + setEnvVar(httpProxyEnvVar, proxyConfig.HTTPProxy) + setEnvVar(httpsProxyEnvVar, proxyConfig.HTTPSProxy) + setEnvVar(noProxyEnvVar, proxyConfig.NoProxy) + + setEnvVar(httpProxyEnvVarLowercase, proxyConfig.HTTPProxy) + setEnvVar(httpsProxyEnvVarLowercase, proxyConfig.HTTPSProxy) + setEnvVar(noProxyEnvVarLowercase, proxyConfig.NoProxy) +} + +// removeProxyEnvVars removes proxy environment variables from a container. +func (r *Reconciler) removeProxyEnvVars(container *corev1.Container) { + if len(container.Env) == 0 { + return + } + + // Helper function to check if an env var name is a proxy variable + isProxyEnvVar := func(name string) bool { + switch name { + case httpProxyEnvVar, httpsProxyEnvVar, noProxyEnvVar, + httpProxyEnvVarLowercase, httpsProxyEnvVarLowercase, noProxyEnvVarLowercase: + return true + default: + return false + } + } + + // Filter out proxy environment variables + filteredEnv := make([]corev1.EnvVar, 0, len(container.Env)) + for _, env := range container.Env { + if !isProxyEnvVar(env.Name) { + filteredEnv = append(filteredEnv, env) + } + } + container.Env = filteredEnv +} + +// updateTrustedCABundleVolumes adds or removes trusted CA bundle volume and volume mounts to/from the deployment +// based on proxy configuration presence. +func (r *Reconciler) updateTrustedCABundleVolumes(deployment *appsv1.Deployment, proxyConfig *operatorv1alpha1.ProxyConfig) error { + + if proxyConfig != nil { + // Add trusted CA bundle volume and volume mounts + return r.addTrustedCABundleVolumes(deployment) + } else { + // Remove trusted CA bundle volume and volume mounts + return r.removeTrustedCABundleVolumes(deployment) + } +} + +// addTrustedCABundleVolumes adds trusted CA bundle volume and volume mounts to the deployment. +func (r *Reconciler) addTrustedCABundleVolumes(deployment *appsv1.Deployment) error { + // 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 and init containers + trustedCAVolumeMount := corev1.VolumeMount{ + Name: trustedCABundleVolumeName, + MountPath: trustedCABundleMountPath, + ReadOnly: true, + } + + // Add volume mount to all containers + for i := range deployment.Spec.Template.Spec.Containers { + container := &deployment.Spec.Template.Spec.Containers[i] + r.addTrustedCAVolumeMount(container, trustedCAVolumeMount) + } + + // Add volume mount to all init containers + for i := range deployment.Spec.Template.Spec.InitContainers { + initContainer := &deployment.Spec.Template.Spec.InitContainers[i] + r.addTrustedCAVolumeMount(initContainer, trustedCAVolumeMount) + } + + return nil +} + +// removeTrustedCABundleVolumes removes trusted CA bundle volume and volume mounts from the deployment. +func (r *Reconciler) removeTrustedCABundleVolumes(deployment *appsv1.Deployment) error { + // Remove the trusted CA bundle volume from the pod spec + var filteredVolumes []corev1.Volume + for _, volume := range deployment.Spec.Template.Spec.Volumes { + if volume.Name != trustedCABundleVolumeName { + filteredVolumes = append(filteredVolumes, volume) + } + } + deployment.Spec.Template.Spec.Volumes = filteredVolumes + + // Remove volume mounts from all containers + for i := range deployment.Spec.Template.Spec.Containers { + container := &deployment.Spec.Template.Spec.Containers[i] + r.removeTrustedCAVolumeMount(container) + } + + // Remove volume mounts from all init containers + for i := range deployment.Spec.Template.Spec.InitContainers { + initContainer := &deployment.Spec.Template.Spec.InitContainers[i] + r.removeTrustedCAVolumeMount(initContainer) + } + + return nil +} + +// addTrustedCAVolumeMount adds the trusted CA bundle volume mount to a container if it doesn't already exist. +func (r *Reconciler) addTrustedCAVolumeMount(container *corev1.Container, trustedCAVolumeMount corev1.VolumeMount) { + // 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) + } +} + +// removeTrustedCAVolumeMount removes the trusted CA bundle volume mount from a container. +func (r *Reconciler) removeTrustedCAVolumeMount(container *corev1.Container) { + var filteredVolumeMounts []corev1.VolumeMount + for _, volumeMount := range container.VolumeMounts { + if volumeMount.Name != trustedCABundleVolumeName { + filteredVolumeMounts = append(filteredVolumeMounts, volumeMount) + } + } + container.VolumeMounts = filteredVolumeMounts +} diff --git a/pkg/controller/external_secrets/deployments_test.go b/pkg/controller/external_secrets/deployments_test.go index b01d8c87..a9f01579 100644 --- a/pkg/controller/external_secrets/deployments_test.go +++ b/pkg/controller/external_secrets/deployments_test.go @@ -2,6 +2,7 @@ package external_secrets import ( "context" + "reflect" "testing" appsv1 "k8s.io/api/apps/v1" @@ -599,3 +600,629 @@ func TestCreateOrApplyDeployments(t *testing.T) { }) } } + +func TestUpdateProxyConfiguration(t *testing.T) { + // Expected trusted CA bundle volume + expectedTrustedCAVolume := corev1.Volume{ + Name: "trusted-ca-bundle", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "external-secrets-trusted-ca-bundle", + }, + }, + }, + } + + tests := []struct { + name string + deployment *appsv1.Deployment + externalSecretsConfig *v1alpha1.ExternalSecretsConfig + externalSecretsManager *v1alpha1.ExternalSecretsManager + olmEnvVars map[string]string + expectedContainerEnvVars map[string][]corev1.EnvVar // container name -> env vars + expectedVolumes []corev1.Volume // expected volumes in the deployment + expectedVolumeMounts map[string][]corev1.VolumeMount // container name -> volume mounts + }{ + { + name: "ExternalSecretsConfig proxy takes precedence", + deployment: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + {Name: "init-migration"}, + }, + Containers: []corev1.Container{ + {Name: "external-secrets"}, + {Name: "webhook"}, + }, + }, + }, + }, + }, + externalSecretsConfig: &v1alpha1.ExternalSecretsConfig{ + Spec: v1alpha1.ExternalSecretsConfigSpec{ + ApplicationConfig: v1alpha1.ApplicationConfig{ + CommonConfigs: v1alpha1.CommonConfigs{ + Proxy: &v1alpha1.ProxyConfig{ + HTTPProxy: "http://esc-proxy:8080", + HTTPSProxy: "https://esc-proxy:8443", + NoProxy: "esc.local", + }, + }, + }, + }, + }, + externalSecretsManager: &v1alpha1.ExternalSecretsManager{ + Spec: v1alpha1.ExternalSecretsManagerSpec{ + GlobalConfig: &v1alpha1.GlobalConfig{ + CommonConfigs: v1alpha1.CommonConfigs{ + Proxy: &v1alpha1.ProxyConfig{ + HTTPProxy: "http://esm-proxy:8080", + HTTPSProxy: "https://esm-proxy:8443", + NoProxy: "esm.local", + }, + }, + }, + }, + }, + olmEnvVars: map[string]string{ + "HTTP_PROXY": "http://olm-proxy:8080", + "HTTPS_PROXY": "https://olm-proxy:8443", + "NO_PROXY": "olm.local", + }, + expectedContainerEnvVars: map[string][]corev1.EnvVar{ + "init-migration": { + {Name: "HTTP_PROXY", Value: "http://esc-proxy:8080"}, + {Name: "HTTPS_PROXY", Value: "https://esc-proxy:8443"}, + {Name: "NO_PROXY", Value: "esc.local"}, + {Name: "http_proxy", Value: "http://esc-proxy:8080"}, + {Name: "https_proxy", Value: "https://esc-proxy:8443"}, + {Name: "no_proxy", Value: "esc.local"}, + }, + "external-secrets": { + {Name: "HTTP_PROXY", Value: "http://esc-proxy:8080"}, + {Name: "HTTPS_PROXY", Value: "https://esc-proxy:8443"}, + {Name: "NO_PROXY", Value: "esc.local"}, + {Name: "http_proxy", Value: "http://esc-proxy:8080"}, + {Name: "https_proxy", Value: "https://esc-proxy:8443"}, + {Name: "no_proxy", Value: "esc.local"}, + }, + "webhook": { + {Name: "HTTP_PROXY", Value: "http://esc-proxy:8080"}, + {Name: "HTTPS_PROXY", Value: "https://esc-proxy:8443"}, + {Name: "NO_PROXY", Value: "esc.local"}, + {Name: "http_proxy", Value: "http://esc-proxy:8080"}, + {Name: "https_proxy", Value: "https://esc-proxy:8443"}, + {Name: "no_proxy", Value: "esc.local"}, + }, + }, + expectedVolumes: []corev1.Volume{expectedTrustedCAVolume}, + expectedVolumeMounts: map[string][]corev1.VolumeMount{ + "init-migration": { + {Name: "trusted-ca-bundle", MountPath: "/etc/pki/tls/certs", ReadOnly: true}, + }, + "external-secrets": { + {Name: "trusted-ca-bundle", MountPath: "/etc/pki/tls/certs", ReadOnly: true}, + }, + "webhook": { + {Name: "trusted-ca-bundle", MountPath: "/etc/pki/tls/certs", ReadOnly: true}, + }, + }, + }, + { + name: "ExternalSecretsManager proxy when ESC has no proxy", + deployment: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "external-secrets"}, + {Name: "webhook"}, + }, + }, + }, + }, + }, + externalSecretsConfig: &v1alpha1.ExternalSecretsConfig{ + Spec: v1alpha1.ExternalSecretsConfigSpec{ + ApplicationConfig: v1alpha1.ApplicationConfig{ + CommonConfigs: v1alpha1.CommonConfigs{ + // No proxy config + }, + }, + }, + }, + externalSecretsManager: &v1alpha1.ExternalSecretsManager{ + Spec: v1alpha1.ExternalSecretsManagerSpec{ + GlobalConfig: &v1alpha1.GlobalConfig{ + CommonConfigs: v1alpha1.CommonConfigs{ + Proxy: &v1alpha1.ProxyConfig{ + HTTPProxy: "http://esm-proxy:8080", + HTTPSProxy: "https://esm-proxy:8443", + NoProxy: "esm.local", + }, + }, + }, + }, + }, + olmEnvVars: map[string]string{ + "HTTP_PROXY": "http://olm-proxy:8080", + "HTTPS_PROXY": "https://olm-proxy:8443", + "NO_PROXY": "olm.local", + }, + expectedContainerEnvVars: map[string][]corev1.EnvVar{ + "external-secrets": { + {Name: "HTTP_PROXY", Value: "http://esm-proxy:8080"}, + {Name: "HTTPS_PROXY", Value: "https://esm-proxy:8443"}, + {Name: "NO_PROXY", Value: "esm.local"}, + {Name: "http_proxy", Value: "http://esm-proxy:8080"}, + {Name: "https_proxy", Value: "https://esm-proxy:8443"}, + {Name: "no_proxy", Value: "esm.local"}, + }, + "webhook": { + {Name: "HTTP_PROXY", Value: "http://esm-proxy:8080"}, + {Name: "HTTPS_PROXY", Value: "https://esm-proxy:8443"}, + {Name: "NO_PROXY", Value: "esm.local"}, + {Name: "http_proxy", Value: "http://esm-proxy:8080"}, + {Name: "https_proxy", Value: "https://esm-proxy:8443"}, + {Name: "no_proxy", Value: "esm.local"}, + }, + }, + expectedVolumes: []corev1.Volume{expectedTrustedCAVolume}, + expectedVolumeMounts: map[string][]corev1.VolumeMount{ + "external-secrets": { + {Name: "trusted-ca-bundle", MountPath: "/etc/pki/tls/certs", ReadOnly: true}, + }, + "webhook": { + {Name: "trusted-ca-bundle", MountPath: "/etc/pki/tls/certs", ReadOnly: true}, + }, + }, + }, + { + name: "OLM environment variables used when no config proxy", + deployment: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "external-secrets"}, + }, + }, + }, + }, + }, + externalSecretsConfig: &v1alpha1.ExternalSecretsConfig{}, + externalSecretsManager: &v1alpha1.ExternalSecretsManager{}, + olmEnvVars: map[string]string{ + "HTTP_PROXY": "http://olm-proxy:8080", + "HTTPS_PROXY": "https://olm-proxy:8443", + "NO_PROXY": "olm.local", + }, + expectedContainerEnvVars: map[string][]corev1.EnvVar{ + "external-secrets": { + {Name: "HTTP_PROXY", Value: "http://olm-proxy:8080"}, + {Name: "HTTPS_PROXY", Value: "https://olm-proxy:8443"}, + {Name: "NO_PROXY", Value: "olm.local"}, + {Name: "http_proxy", Value: "http://olm-proxy:8080"}, + {Name: "https_proxy", Value: "https://olm-proxy:8443"}, + {Name: "no_proxy", Value: "olm.local"}, + }, + }, + expectedVolumes: []corev1.Volume{expectedTrustedCAVolume}, + expectedVolumeMounts: map[string][]corev1.VolumeMount{ + "external-secrets": { + {Name: "trusted-ca-bundle", MountPath: "/etc/pki/tls/certs", ReadOnly: true}, + }, + }, + }, + { + name: "Partial proxy configuration", + deployment: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "external-secrets"}, + }, + }, + }, + }, + }, + externalSecretsConfig: &v1alpha1.ExternalSecretsConfig{ + Spec: v1alpha1.ExternalSecretsConfigSpec{ + ApplicationConfig: v1alpha1.ApplicationConfig{ + CommonConfigs: v1alpha1.CommonConfigs{ + Proxy: &v1alpha1.ProxyConfig{ + HTTPProxy: "http://esc-proxy:8080", + // HTTPSProxy and NoProxy are empty + }, + }, + }, + }, + }, + externalSecretsManager: &v1alpha1.ExternalSecretsManager{}, + olmEnvVars: map[string]string{}, + expectedContainerEnvVars: map[string][]corev1.EnvVar{ + "external-secrets": { + {Name: "HTTP_PROXY", Value: "http://esc-proxy:8080"}, + {Name: "http_proxy", Value: "http://esc-proxy:8080"}, + }, + }, + expectedVolumes: []corev1.Volume{expectedTrustedCAVolume}, + expectedVolumeMounts: map[string][]corev1.VolumeMount{ + "external-secrets": { + {Name: "trusted-ca-bundle", MountPath: "/etc/pki/tls/certs", ReadOnly: true}, + }, + }, + }, + { + name: "Update existing proxy environment variables", + 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"}, + }, + }, + }, + }, + }, + }, + }, + externalSecretsConfig: &v1alpha1.ExternalSecretsConfig{ + Spec: v1alpha1.ExternalSecretsConfigSpec{ + ApplicationConfig: v1alpha1.ApplicationConfig{ + CommonConfigs: v1alpha1.CommonConfigs{ + Proxy: &v1alpha1.ProxyConfig{ + HTTPProxy: "http://new-proxy:8080", + HTTPSProxy: "https://new-proxy:8443", + NoProxy: "localhost", + }, + }, + }, + }, + }, + externalSecretsManager: &v1alpha1.ExternalSecretsManager{}, + olmEnvVars: map[string]string{}, + expectedContainerEnvVars: map[string][]corev1.EnvVar{ + "external-secrets": { + {Name: "HTTP_PROXY", Value: "http://new-proxy:8080"}, + {Name: "EXISTING_VAR", Value: "existing-value"}, + {Name: "HTTPS_PROXY", Value: "https://new-proxy:8443"}, + {Name: "NO_PROXY", Value: "localhost"}, + {Name: "http_proxy", Value: "http://new-proxy:8080"}, + {Name: "https_proxy", Value: "https://new-proxy:8443"}, + {Name: "no_proxy", Value: "localhost"}, + }, + }, + expectedVolumes: []corev1.Volume{expectedTrustedCAVolume}, + expectedVolumeMounts: map[string][]corev1.VolumeMount{ + "external-secrets": { + {Name: "trusted-ca-bundle", MountPath: "/etc/pki/tls/certs", ReadOnly: true}, + }, + }, + }, + { + name: "No proxy configuration results in no changes", + deployment: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "external-secrets", + Env: []corev1.EnvVar{ + {Name: "EXISTING_VAR", Value: "existing-value"}, + }, + }, + }, + }, + }, + }, + }, + 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{}, + }, + { + name: "Proxy configuration applied to init containers", + deployment: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + {Name: "init-setup"}, + }, + Containers: []corev1.Container{ + {Name: "external-secrets"}, + }, + }, + }, + }, + }, + externalSecretsConfig: &v1alpha1.ExternalSecretsConfig{ + Spec: v1alpha1.ExternalSecretsConfigSpec{ + ApplicationConfig: v1alpha1.ApplicationConfig{ + CommonConfigs: v1alpha1.CommonConfigs{ + Proxy: &v1alpha1.ProxyConfig{ + HTTPProxy: "http://esc-proxy:8080", + HTTPSProxy: "https://esc-proxy:8443", + NoProxy: "esc.local", + }, + }, + }, + }, + }, + externalSecretsManager: &v1alpha1.ExternalSecretsManager{}, + olmEnvVars: map[string]string{}, + expectedContainerEnvVars: map[string][]corev1.EnvVar{ + "init-setup": { + {Name: "HTTP_PROXY", Value: "http://esc-proxy:8080"}, + {Name: "HTTPS_PROXY", Value: "https://esc-proxy:8443"}, + {Name: "NO_PROXY", Value: "esc.local"}, + {Name: "http_proxy", Value: "http://esc-proxy:8080"}, + {Name: "https_proxy", Value: "https://esc-proxy:8443"}, + {Name: "no_proxy", Value: "esc.local"}, + }, + "external-secrets": { + {Name: "HTTP_PROXY", Value: "http://esc-proxy:8080"}, + {Name: "HTTPS_PROXY", Value: "https://esc-proxy:8443"}, + {Name: "NO_PROXY", Value: "esc.local"}, + {Name: "http_proxy", Value: "http://esc-proxy:8080"}, + {Name: "https_proxy", Value: "https://esc-proxy:8443"}, + {Name: "no_proxy", Value: "esc.local"}, + }, + }, + expectedVolumes: []corev1.Volume{expectedTrustedCAVolume}, + expectedVolumeMounts: map[string][]corev1.VolumeMount{ + "init-setup": { + {Name: "trusted-ca-bundle", MountPath: "/etc/pki/tls/certs", ReadOnly: true}, + }, + "external-secrets": { + {Name: "trusted-ca-bundle", MountPath: "/etc/pki/tls/certs", ReadOnly: true}, + }, + }, + }, + { + name: "Proxy configuration removal cleans up environment variables and volumes", + deployment: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + InitContainers: []corev1.Container{ + { + Name: "init-setup", + Env: []corev1.EnvVar{ + {Name: "HTTP_PROXY", Value: "http://old-proxy:8080"}, + {Name: "HTTPS_PROXY", Value: "https://old-proxy:8443"}, + {Name: "NO_PROXY", Value: "old.local"}, + {Name: "http_proxy", Value: "http://old-proxy:8080"}, + {Name: "https_proxy", Value: "https://old-proxy:8443"}, + {Name: "no_proxy", Value: "old.local"}, + {Name: "KEEP_THIS_VAR", Value: "keep-value"}, + }, + VolumeMounts: []corev1.VolumeMount{ + {Name: "trusted-ca-bundle", MountPath: "/etc/pki/tls/certs", ReadOnly: true}, + {Name: "other-volume", MountPath: "/other", ReadOnly: true}, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "external-secrets", + Env: []corev1.EnvVar{ + {Name: "HTTP_PROXY", Value: "http://old-proxy:8080"}, + {Name: "HTTPS_PROXY", Value: "https://old-proxy:8443"}, + {Name: "NO_PROXY", Value: "old.local"}, + {Name: "http_proxy", Value: "http://old-proxy:8080"}, + {Name: "https_proxy", Value: "https://old-proxy:8443"}, + {Name: "no_proxy", Value: "old.local"}, + {Name: "KEEP_THIS_VAR", Value: "keep-value"}, + }, + VolumeMounts: []corev1.VolumeMount{ + {Name: "trusted-ca-bundle", MountPath: "/etc/pki/tls/certs", ReadOnly: true}, + {Name: "other-volume", MountPath: "/other", ReadOnly: true}, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "trusted-ca-bundle", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "external-secrets-trusted-ca-bundle", + }, + }, + }, + }, + { + Name: "other-volume", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + }, + }, + }, + }, + externalSecretsConfig: &v1alpha1.ExternalSecretsConfig{}, // No proxy configuration + externalSecretsManager: &v1alpha1.ExternalSecretsManager{}, + olmEnvVars: map[string]string{}, + expectedContainerEnvVars: map[string][]corev1.EnvVar{ + "init-setup": { + {Name: "KEEP_THIS_VAR", Value: "keep-value"}, + }, + "external-secrets": { + {Name: "KEEP_THIS_VAR", Value: "keep-value"}, + }, + }, + expectedVolumes: []corev1.Volume{ + { + Name: "other-volume", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + }, + expectedVolumeMounts: map[string][]corev1.VolumeMount{ + "init-setup": { + {Name: "other-volume", MountPath: "/other", ReadOnly: true}, + }, + "external-secrets": { + {Name: "other-volume", MountPath: "/other", ReadOnly: true}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up environment variables + for key, value := range tt.olmEnvVars { + t.Setenv(key, value) + } + + r := &Reconciler{ + esm: tt.externalSecretsManager, + } + + err := r.updateProxyConfiguration(tt.deployment, tt.externalSecretsConfig) + if err != nil { + t.Errorf("updateProxyConfiguration() error = %v", err) + return + } + + validateEnvironmentVariables(t, tt.deployment, tt.expectedContainerEnvVars) + validateVolumes(t, tt.deployment, tt.expectedVolumes) + validateVolumeMounts(t, tt.deployment, tt.expectedVolumeMounts) + }) + } +} + +// validateEnvironmentVariables validates that containers have expected environment variables +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) { + t.Errorf("Container %s environment variables mismatch.\nExpected: %+v\nActual: %+v", + containerName, expectedEnvVars, container.Env) + } + } +} + +// validateVolumes validates that deployment has expected volumes +func validateVolumes(t *testing.T, deployment *appsv1.Deployment, expectedVolumes []corev1.Volume) { + if len(expectedVolumes) == 0 { + // Verify no trusted CA bundle volume was added + for _, volume := range deployment.Spec.Template.Spec.Volumes { + if volume.Name == trustedCABundleVolumeName { + t.Errorf("Expected no trusted CA bundle volume, but found one: %+v", volume) + } + } + return + } + + // Verify expected volumes exist and match exactly + if !reflect.DeepEqual(deployment.Spec.Template.Spec.Volumes, expectedVolumes) { + t.Errorf("Volumes mismatch.\nExpected: %+v\nActual: %+v", + expectedVolumes, deployment.Spec.Template.Spec.Volumes) + } +} + +// validateVolumeMounts validates that containers have expected volume mounts +func validateVolumeMounts(t *testing.T, deployment *appsv1.Deployment, expectedVolumeMounts map[string][]corev1.VolumeMount) { + if len(expectedVolumeMounts) == 0 { + // Verify no trusted CA bundle volume mounts exist in any container + for _, container := range deployment.Spec.Template.Spec.Containers { + trustedCAMounts := filterTrustedCAMounts(container.VolumeMounts) + if len(trustedCAMounts) > 0 { + t.Errorf("Expected no trusted CA bundle volume mount in container %s, but found: %+v", + container.Name, trustedCAMounts) + } + } + return + } + + // Verify expected volume mounts exist + for containerName, expectedMounts := range expectedVolumeMounts { + container := findContainer(deployment, containerName) + if container == nil { + t.Errorf("Container %s not found for volume mount validation", containerName) + continue + } + + // Determine if we're testing for trusted CA mounts or non-trusted CA mounts + var actualMounts []corev1.VolumeMount + if len(expectedMounts) > 0 && expectedMounts[0].Name == trustedCABundleVolumeName { + // Testing for trusted CA mounts + actualMounts = filterTrustedCAMounts(container.VolumeMounts) + } else { + // Testing for non-trusted CA mounts (e.g., in removal scenarios) + actualMounts = filterNonTrustedCAMounts(container.VolumeMounts) + } + + if !reflect.DeepEqual(actualMounts, expectedMounts) { + t.Errorf("Container %s volume mounts mismatch.\nExpected: %+v\nActual: %+v", + containerName, expectedMounts, actualMounts) + } + } +} + +// findContainer finds a container by name in the deployment +func findContainer(deployment *appsv1.Deployment, containerName string) *corev1.Container { + // Search regular containers first + for i, container := range deployment.Spec.Template.Spec.Containers { + if container.Name == containerName { + return &deployment.Spec.Template.Spec.Containers[i] + } + } + // Search init containers + for i, container := range deployment.Spec.Template.Spec.InitContainers { + if container.Name == containerName { + return &deployment.Spec.Template.Spec.InitContainers[i] + } + } + return nil +} + +// filterTrustedCAMounts filters volume mounts to only include trusted CA bundle mounts +func filterTrustedCAMounts(volumeMounts []corev1.VolumeMount) []corev1.VolumeMount { + var trustedCAMounts []corev1.VolumeMount + for _, mount := range volumeMounts { + if mount.Name == trustedCABundleVolumeName { + trustedCAMounts = append(trustedCAMounts, mount) + } + } + return trustedCAMounts +} + +// filterNonTrustedCAMounts filters volume mounts to exclude trusted CA bundle mounts +func filterNonTrustedCAMounts(volumeMounts []corev1.VolumeMount) []corev1.VolumeMount { + var nonTrustedCAMounts []corev1.VolumeMount + for _, mount := range volumeMounts { + if mount.Name != trustedCABundleVolumeName { + nonTrustedCAMounts = append(nonTrustedCAMounts, mount) + } + } + return nonTrustedCAMounts +} diff --git a/pkg/controller/external_secrets/install_external_secrets.go b/pkg/controller/external_secrets/install_external_secrets.go index 8357569e..4ec79622 100644 --- a/pkg/controller/external_secrets/install_external_secrets.go +++ b/pkg/controller/external_secrets/install_external_secrets.go @@ -72,6 +72,11 @@ func (r *Reconciler) reconcileExternalSecretsDeployment(esc *operatorv1alpha1.Ex return err } + if err := r.ensureTrustedCABundleConfigMap(esc, resourceLabels); err != nil { + r.log.Error(err, "failed to ensure trusted CA bundle ConfigMap") + return err + } + if err := r.createOrApplyRBACResource(esc, resourceLabels, recon); err != nil { r.log.Error(err, "failed to reconcile rbac resources") return err diff --git a/pkg/controller/external_secrets/utils.go b/pkg/controller/external_secrets/utils.go index b68348bf..cb96719f 100644 --- a/pkg/controller/external_secrets/utils.go +++ b/pkg/controller/external_secrets/utils.go @@ -3,6 +3,7 @@ package external_secrets import ( "context" "fmt" + "os" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/client-go/util/retry" @@ -122,3 +123,33 @@ func (r *Reconciler) IsCertManagerInstalled() bool { _, ok := r.optionalResourcesList[certificateCRDGKV] return ok } + +// getProxyConfiguration returns the proxy configuration based on precedence. +// The precedence order is: ExternalSecretsConfig > ExternalSecretsManager > OLM environment variables. +func (r *Reconciler) getProxyConfiguration(esc *operatorv1alpha1.ExternalSecretsConfig) *operatorv1alpha1.ProxyConfig { + var proxyConfig *operatorv1alpha1.ProxyConfig + + // Check ExternalSecretsConfig first + if esc.Spec.ApplicationConfig.Proxy != nil { + proxyConfig = esc.Spec.ApplicationConfig.Proxy + } else if r.esm.Spec.GlobalConfig != nil && r.esm.Spec.GlobalConfig.Proxy != nil { + // Check ExternalSecretsManager second + proxyConfig = r.esm.Spec.GlobalConfig.Proxy + } else { + // Fall back to OLM environment variables + olmHTTPProxy := os.Getenv(httpProxyEnvVar) + olmHTTPSProxy := os.Getenv(httpsProxyEnvVar) + olmNoProxy := os.Getenv(noProxyEnvVar) + + // Only create proxy config if at least one OLM env var is set + if olmHTTPProxy != "" || olmHTTPSProxy != "" || olmNoProxy != "" { + proxyConfig = &operatorv1alpha1.ProxyConfig{ + HTTPProxy: olmHTTPProxy, + HTTPSProxy: olmHTTPSProxy, + NoProxy: olmNoProxy, + } + } + } + + return proxyConfig +}