From ed1b333ae7b9734ce490c669a328f4ccf183b462 Mon Sep 17 00:00:00 2001 From: siddhi bhor Date: Tue, 3 Feb 2026 19:43:38 +0530 Subject: [PATCH 1/4] Adds the implementation logic for env var --- pkg/controller/external_secrets/constants.go | 6 + .../external_secrets/deployments.go | 47 ++- .../external_secrets/deployments_test.go | 328 +++++++++++++++++- 3 files changed, 371 insertions(+), 10 deletions(-) diff --git a/pkg/controller/external_secrets/constants.go b/pkg/controller/external_secrets/constants.go index 9a81d20e..0c8c5f17 100644 --- a/pkg/controller/external_secrets/constants.go +++ b/pkg/controller/external_secrets/constants.go @@ -110,6 +110,12 @@ const ( controllerDeploymentAssetName = "external-secrets/resources/deployment_external-secrets.yml" certControllerDeploymentAssetName = "external-secrets/resources/deployment_external-secrets-cert-controller.yml" webhookDeploymentAssetName = "external-secrets/resources/deployment_external-secrets-webhook.yml" + + // Container names for each component deployment + controllerContainerName = "external-secrets" + webhookContainerName = "webhook" + certControllerContainerName = "cert-controller" + bitwardenContainerName = "bitwarden-sdk-server" controllerRoleLeaderElectionAssetName = "external-secrets/resources/role_external-secrets-leaderelection.yml" controllerRoleBindingLeaderElectionAssetName = "external-secrets/resources/rolebinding_external-secrets-leaderelection.yml" webhookTLSSecretAssetName = "external-secrets/resources/secret_external-secrets-webhook.yml" diff --git a/pkg/controller/external_secrets/deployments.go b/pkg/controller/external_secrets/deployments.go index 5f628cda..b9f9f294 100644 --- a/pkg/controller/external_secrets/deployments.go +++ b/pkg/controller/external_secrets/deployments.go @@ -661,7 +661,7 @@ func (r *Reconciler) removeTrustedCAVolumeMount(container *corev1.Container) { // applyUserDeploymentConfigs updates the deployment resource spec with user specified configurations. func (r *Reconciler) applyUserDeploymentConfigs(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig, assetName string) error { - componentName, err := getComponentNameFromAsset(assetName) + componentName, containerName, err := getComponentNameFromAsset(assetName) if err != nil { return err } @@ -672,6 +672,16 @@ func (r *Reconciler) applyUserDeploymentConfigs(deployment *appsv1.Deployment, e if i.DeploymentConfigs != nil && i.DeploymentConfigs.RevisionHistoryLimit != nil { deployment.Spec.RevisionHistoryLimit = i.DeploymentConfigs.RevisionHistoryLimit } + + // Apply OverrideEnv if set + if len(i.OverrideEnv) > 0 { + for j := range deployment.Spec.Template.Spec.Containers { + if deployment.Spec.Template.Spec.Containers[j].Name == containerName { + mergeEnvVars(&deployment.Spec.Template.Spec.Containers[j], i.OverrideEnv) + break + } + } + } break } } @@ -679,18 +689,39 @@ func (r *Reconciler) applyUserDeploymentConfigs(deployment *appsv1.Deployment, e return nil } -// getComponentNameFromAsset maps asset file names to ComponentName enum values. -func getComponentNameFromAsset(assetName string) (operatorv1alpha1.ComponentName, error) { +// mergeEnvVars merges user-defined environment variables into a container, User-defined values take precedence over existing values. +func mergeEnvVars(container *corev1.Container, overrideEnv []corev1.EnvVar) { + if container.Env == nil { + container.Env = []corev1.EnvVar{} + } + + for _, override := range overrideEnv { + found := false + for i, existing := range container.Env { + if existing.Name == override.Name { + container.Env[i] = override // User-defined value takes precedence + found = true + break + } + } + if !found { + container.Env = append(container.Env, override) + } + } +} + +// getComponentNameFromAsset maps asset file names to ComponentName enum values and container names. +func getComponentNameFromAsset(assetName string) (operatorv1alpha1.ComponentName, string, error) { switch assetName { case controllerDeploymentAssetName: - return operatorv1alpha1.CoreController, nil + return operatorv1alpha1.CoreController, controllerContainerName, nil case webhookDeploymentAssetName: - return operatorv1alpha1.Webhook, nil + return operatorv1alpha1.Webhook, webhookContainerName, nil case certControllerDeploymentAssetName: - return operatorv1alpha1.CertController, nil + return operatorv1alpha1.CertController, certControllerContainerName, nil case bitwardenDeploymentAssetName: - return operatorv1alpha1.BitwardenSDKServer, nil + return operatorv1alpha1.BitwardenSDKServer, bitwardenContainerName, nil default: - return "", fmt.Errorf("unknown deployment asset name: %s", assetName) + return "", "", fmt.Errorf("unknown deployment asset name: %s", assetName) } } diff --git a/pkg/controller/external_secrets/deployments_test.go b/pkg/controller/external_secrets/deployments_test.go index 9861b451..09876f3d 100644 --- a/pkg/controller/external_secrets/deployments_test.go +++ b/pkg/controller/external_secrets/deployments_test.go @@ -1325,6 +1325,7 @@ func TestGetComponentNameFromAsset(t *testing.T) { name string assetName string wantComponent v1alpha1.ComponentName + wantContainer string wantErr bool errContains string }{ @@ -1332,30 +1333,35 @@ func TestGetComponentNameFromAsset(t *testing.T) { name: "valid controller deployment asset", assetName: controllerDeploymentAssetName, wantComponent: v1alpha1.CoreController, + wantContainer: "external-secrets", wantErr: false, }, { name: "valid webhook deployment asset", assetName: webhookDeploymentAssetName, wantComponent: v1alpha1.Webhook, + wantContainer: "webhook", wantErr: false, }, { name: "valid cert controller deployment asset", assetName: certControllerDeploymentAssetName, wantComponent: v1alpha1.CertController, + wantContainer: "cert-controller", wantErr: false, }, { name: "valid bitwarden deployment asset", assetName: bitwardenDeploymentAssetName, wantComponent: v1alpha1.BitwardenSDKServer, + wantContainer: "bitwarden-sdk-server", wantErr: false, }, { name: "invalid asset name returns error", assetName: "invalid-asset-name.yml", wantComponent: "", + wantContainer: "", wantErr: true, errContains: "unknown deployment asset name", }, @@ -1363,6 +1369,7 @@ func TestGetComponentNameFromAsset(t *testing.T) { name: "empty asset name returns error", assetName: "", wantComponent: "", + wantContainer: "", wantErr: true, errContains: "unknown deployment asset name", }, @@ -1370,6 +1377,7 @@ func TestGetComponentNameFromAsset(t *testing.T) { name: "random string returns error", assetName: "some-random-deployment.yml", wantComponent: "", + wantContainer: "", wantErr: true, errContains: "unknown deployment asset name", }, @@ -1377,7 +1385,7 @@ func TestGetComponentNameFromAsset(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotComponent, err := getComponentNameFromAsset(tt.assetName) + gotComponent, gotContainer, err := getComponentNameFromAsset(tt.assetName) if tt.wantErr { if err == nil { @@ -1390,13 +1398,329 @@ func TestGetComponentNameFromAsset(t *testing.T) { if gotComponent != "" { t.Errorf("getComponentNameFromAsset() on error should return empty component, got %v", gotComponent) } + if gotContainer != "" { + t.Errorf("getComponentNameFromAsset() on error should return empty container, got %v", gotContainer) + } } else { if err != nil { t.Errorf("getComponentNameFromAsset() unexpected error = %v", err) return } if gotComponent != tt.wantComponent { - t.Errorf("getComponentNameFromAsset() = %v, want %v", gotComponent, tt.wantComponent) + t.Errorf("getComponentNameFromAsset() component = %v, want %v", gotComponent, tt.wantComponent) + } + if gotContainer != tt.wantContainer { + t.Errorf("getComponentNameFromAsset() container = %v, want %v", gotContainer, tt.wantContainer) + } + } + }) + } +} + +func TestMergeEnvVars(t *testing.T) { + tests := []struct { + name string + existingEnv []corev1.EnvVar + overrideEnv []corev1.EnvVar + expectedEnv []corev1.EnvVar + }{ + { + name: "empty container env, add new env vars", + existingEnv: nil, + overrideEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + {Name: "TIMEOUT", Value: "30s"}, + }, + expectedEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + {Name: "TIMEOUT", Value: "30s"}, + }, + }, + { + name: "override existing env var", + existingEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "info"}, + {Name: "OTHER_VAR", Value: "value"}, + }, + overrideEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + }, + expectedEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + {Name: "OTHER_VAR", Value: "value"}, + }, + }, + { + name: "add new env var to existing ones", + existingEnv: []corev1.EnvVar{ + {Name: "EXISTING_VAR", Value: "existing"}, + }, + overrideEnv: []corev1.EnvVar{ + {Name: "NEW_VAR", Value: "new"}, + }, + expectedEnv: []corev1.EnvVar{ + {Name: "EXISTING_VAR", Value: "existing"}, + {Name: "NEW_VAR", Value: "new"}, + }, + }, + { + name: "mix of override and new env vars", + existingEnv: []corev1.EnvVar{ + {Name: "VAR_A", Value: "old_a"}, + {Name: "VAR_B", Value: "old_b"}, + }, + overrideEnv: []corev1.EnvVar{ + {Name: "VAR_A", Value: "new_a"}, + {Name: "VAR_C", Value: "new_c"}, + }, + expectedEnv: []corev1.EnvVar{ + {Name: "VAR_A", Value: "new_a"}, + {Name: "VAR_B", Value: "old_b"}, + {Name: "VAR_C", Value: "new_c"}, + }, + }, + { + name: "empty override env vars does nothing", + existingEnv: []corev1.EnvVar{ + {Name: "EXISTING", Value: "value"}, + }, + overrideEnv: []corev1.EnvVar{}, + expectedEnv: []corev1.EnvVar{ + {Name: "EXISTING", Value: "value"}, + }, + }, + { + name: "override env var with ValueFrom", + existingEnv: []corev1.EnvVar{ + {Name: "SECRET_VAR", Value: "plaintext"}, + }, + overrideEnv: []corev1.EnvVar{ + { + Name: "SECRET_VAR", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"}, + Key: "password", + }, + }, + }, + }, + expectedEnv: []corev1.EnvVar{ + { + Name: "SECRET_VAR", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "my-secret"}, + Key: "password", + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + container := &corev1.Container{ + Name: "test-container", + Env: tt.existingEnv, + } + + mergeEnvVars(container, tt.overrideEnv) + + if len(container.Env) != len(tt.expectedEnv) { + t.Errorf("mergeEnvVars() got %d env vars, want %d", len(container.Env), len(tt.expectedEnv)) + return + } + + for i, expected := range tt.expectedEnv { + found := false + for _, actual := range container.Env { + if actual.Name == expected.Name { + found = true + if expected.ValueFrom != nil { + if actual.ValueFrom == nil { + t.Errorf("mergeEnvVars() env var %s expected ValueFrom but got Value", expected.Name) + } + } else if actual.Value != expected.Value { + t.Errorf("mergeEnvVars() env var %s = %v, want %v", expected.Name, actual.Value, expected.Value) + } + break + } + } + if !found { + t.Errorf("mergeEnvVars() missing env var %s at index %d", expected.Name, i) + } + } + }) + } +} + +func TestApplyUserDeploymentConfigsWithOverrideEnv(t *testing.T) { + tests := []struct { + name string + assetName string + containerName string + componentConfig v1alpha1.ComponentConfig + existingEnv []corev1.EnvVar + expectedEnv []corev1.EnvVar + }{ + { + name: "apply override env to core controller", + assetName: controllerDeploymentAssetName, + containerName: "external-secrets", + componentConfig: v1alpha1.ComponentConfig{ + ComponentName: v1alpha1.CoreController, + OverrideEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + }, + }, + existingEnv: []corev1.EnvVar{}, + expectedEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + }, + }, + { + name: "apply override env to webhook", + assetName: webhookDeploymentAssetName, + containerName: "webhook", + componentConfig: v1alpha1.ComponentConfig{ + ComponentName: v1alpha1.Webhook, + OverrideEnv: []corev1.EnvVar{ + {Name: "TIMEOUT", Value: "60s"}, + }, + }, + existingEnv: []corev1.EnvVar{ + {Name: "EXISTING", Value: "value"}, + }, + expectedEnv: []corev1.EnvVar{ + {Name: "EXISTING", Value: "value"}, + {Name: "TIMEOUT", Value: "60s"}, + }, + }, + { + name: "apply override env to cert controller", + assetName: certControllerDeploymentAssetName, + containerName: "cert-controller", + componentConfig: v1alpha1.ComponentConfig{ + ComponentName: v1alpha1.CertController, + OverrideEnv: []corev1.EnvVar{ + {Name: "CERT_DURATION", Value: "8760h"}, + }, + }, + existingEnv: []corev1.EnvVar{}, + expectedEnv: []corev1.EnvVar{ + {Name: "CERT_DURATION", Value: "8760h"}, + }, + }, + { + name: "apply override env to bitwarden", + assetName: bitwardenDeploymentAssetName, + containerName: "bitwarden-sdk-server", + componentConfig: v1alpha1.ComponentConfig{ + ComponentName: v1alpha1.BitwardenSDKServer, + OverrideEnv: []corev1.EnvVar{ + {Name: "API_URL", Value: "https://api.bitwarden.com"}, + }, + }, + existingEnv: []corev1.EnvVar{}, + expectedEnv: []corev1.EnvVar{ + {Name: "API_URL", Value: "https://api.bitwarden.com"}, + }, + }, + { + name: "no override env does not modify container", + assetName: controllerDeploymentAssetName, + containerName: "external-secrets", + componentConfig: v1alpha1.ComponentConfig{ + ComponentName: v1alpha1.CoreController, + OverrideEnv: nil, + }, + existingEnv: []corev1.EnvVar{ + {Name: "EXISTING", Value: "value"}, + }, + expectedEnv: []corev1.EnvVar{ + {Name: "EXISTING", Value: "value"}, + }, + }, + { + name: "both revision history and override env", + assetName: controllerDeploymentAssetName, + containerName: "external-secrets", + componentConfig: v1alpha1.ComponentConfig{ + ComponentName: v1alpha1.CoreController, + DeploymentConfigs: &v1alpha1.DeploymentConfig{ + RevisionHistoryLimit: ptr.To(int32(5)), + }, + OverrideEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + }, + }, + existingEnv: []corev1.EnvVar{}, + expectedEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &Reconciler{} + deployment := &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: tt.containerName, + Env: tt.existingEnv, + }, + }, + }, + }, + }, + } + + esc := &v1alpha1.ExternalSecretsConfig{ + Spec: v1alpha1.ExternalSecretsConfigSpec{ + ControllerConfig: v1alpha1.ControllerConfig{ + ComponentConfigs: []v1alpha1.ComponentConfig{tt.componentConfig}, + }, + }, + } + + err := r.applyUserDeploymentConfigs(deployment, esc, tt.assetName) + if err != nil { + t.Errorf("applyUserDeploymentConfigs() unexpected error: %v", err) + return + } + + container := &deployment.Spec.Template.Spec.Containers[0] + if len(container.Env) != len(tt.expectedEnv) { + t.Errorf("applyUserDeploymentConfigs() got %d env vars, want %d. Got: %v", len(container.Env), len(tt.expectedEnv), container.Env) + return + } + + for _, expected := range tt.expectedEnv { + found := false + for _, actual := range container.Env { + if actual.Name == expected.Name && actual.Value == expected.Value { + found = true + break + } + } + if !found { + t.Errorf("applyUserDeploymentConfigs() missing env var %s=%s", expected.Name, expected.Value) + } + } + + // Verify revision history limit if set + if tt.componentConfig.DeploymentConfigs != nil && tt.componentConfig.DeploymentConfigs.RevisionHistoryLimit != nil { + if deployment.Spec.RevisionHistoryLimit == nil { + t.Error("applyUserDeploymentConfigs() RevisionHistoryLimit should be set") + } else if *deployment.Spec.RevisionHistoryLimit != *tt.componentConfig.DeploymentConfigs.RevisionHistoryLimit { + t.Errorf("applyUserDeploymentConfigs() RevisionHistoryLimit = %d, want %d", + *deployment.Spec.RevisionHistoryLimit, *tt.componentConfig.DeploymentConfigs.RevisionHistoryLimit) } } }) From 484a1d52bed6c47ef6b186c32605ce0eae042b82 Mon Sep 17 00:00:00 2001 From: siddhi bhor Date: Wed, 11 Feb 2026 00:25:11 +0530 Subject: [PATCH 2/4] adds the env-var logic for init-containers --- .../external_secrets/deployments.go | 4 + .../external_secrets/deployments_test.go | 151 +++++++++++++++++- 2 files changed, 149 insertions(+), 6 deletions(-) diff --git a/pkg/controller/external_secrets/deployments.go b/pkg/controller/external_secrets/deployments.go index b9f9f294..ac43edb4 100644 --- a/pkg/controller/external_secrets/deployments.go +++ b/pkg/controller/external_secrets/deployments.go @@ -681,6 +681,10 @@ func (r *Reconciler) applyUserDeploymentConfigs(deployment *appsv1.Deployment, e break } } + // Apply to all init containers in the deployment without name filtering, + for j := range deployment.Spec.Template.Spec.InitContainers { + mergeEnvVars(&deployment.Spec.Template.Spec.InitContainers[j], i.OverrideEnv) + } } break } diff --git a/pkg/controller/external_secrets/deployments_test.go b/pkg/controller/external_secrets/deployments_test.go index 09876f3d..c6cedba2 100644 --- a/pkg/controller/external_secrets/deployments_test.go +++ b/pkg/controller/external_secrets/deployments_test.go @@ -1558,12 +1558,14 @@ func TestMergeEnvVars(t *testing.T) { func TestApplyUserDeploymentConfigsWithOverrideEnv(t *testing.T) { tests := []struct { - name string - assetName string - containerName string - componentConfig v1alpha1.ComponentConfig - existingEnv []corev1.EnvVar - expectedEnv []corev1.EnvVar + name string + assetName string + containerName string + componentConfig v1alpha1.ComponentConfig + existingEnv []corev1.EnvVar + expectedEnv []corev1.EnvVar + initContainers []corev1.Container + expectedInitContainerEnv map[string][]corev1.EnvVar // init container name -> expected env vars }{ { name: "apply override env to core controller", @@ -1661,6 +1663,129 @@ func TestApplyUserDeploymentConfigsWithOverrideEnv(t *testing.T) { {Name: "LOG_LEVEL", Value: "debug"}, }, }, + { + name: "override env applied to init containers", + assetName: controllerDeploymentAssetName, + containerName: "external-secrets", + componentConfig: v1alpha1.ComponentConfig{ + ComponentName: v1alpha1.CoreController, + OverrideEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + {Name: "TIMEOUT", Value: "30s"}, + }, + }, + existingEnv: []corev1.EnvVar{}, + expectedEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + {Name: "TIMEOUT", Value: "30s"}, + }, + initContainers: []corev1.Container{ + {Name: "init-setup"}, + }, + expectedInitContainerEnv: map[string][]corev1.EnvVar{ + "init-setup": { + {Name: "LOG_LEVEL", Value: "debug"}, + {Name: "TIMEOUT", Value: "30s"}, + }, + }, + }, + { + name: "override env merges with existing init container env vars", + assetName: webhookDeploymentAssetName, + containerName: "webhook", + componentConfig: v1alpha1.ComponentConfig{ + ComponentName: v1alpha1.Webhook, + OverrideEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + {Name: "EXISTING_VAR", Value: "overridden"}, + }, + }, + existingEnv: []corev1.EnvVar{ + {Name: "EXISTING", Value: "value"}, + }, + expectedEnv: []corev1.EnvVar{ + {Name: "EXISTING", Value: "value"}, + {Name: "LOG_LEVEL", Value: "debug"}, + {Name: "EXISTING_VAR", Value: "overridden"}, + }, + initContainers: []corev1.Container{ + { + Name: "init-migration", + Env: []corev1.EnvVar{ + {Name: "EXISTING_VAR", Value: "original"}, + {Name: "KEEP_VAR", Value: "keep"}, + }, + }, + }, + expectedInitContainerEnv: map[string][]corev1.EnvVar{ + "init-migration": { + {Name: "EXISTING_VAR", Value: "overridden"}, + {Name: "KEEP_VAR", Value: "keep"}, + {Name: "LOG_LEVEL", Value: "debug"}, + }, + }, + }, + { + name: "override env applied to multiple init containers", + assetName: controllerDeploymentAssetName, + containerName: "external-secrets", + componentConfig: v1alpha1.ComponentConfig{ + ComponentName: v1alpha1.CoreController, + OverrideEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + }, + }, + existingEnv: []corev1.EnvVar{}, + expectedEnv: []corev1.EnvVar{ + {Name: "LOG_LEVEL", Value: "debug"}, + }, + initContainers: []corev1.Container{ + {Name: "init-setup"}, + { + Name: "init-migration", + Env: []corev1.EnvVar{ + {Name: "EXISTING", Value: "value"}, + }, + }, + }, + expectedInitContainerEnv: map[string][]corev1.EnvVar{ + "init-setup": { + {Name: "LOG_LEVEL", Value: "debug"}, + }, + "init-migration": { + {Name: "EXISTING", Value: "value"}, + {Name: "LOG_LEVEL", Value: "debug"}, + }, + }, + }, + { + name: "no override env does not modify init containers", + assetName: controllerDeploymentAssetName, + containerName: "external-secrets", + componentConfig: v1alpha1.ComponentConfig{ + ComponentName: v1alpha1.CoreController, + OverrideEnv: nil, + }, + existingEnv: []corev1.EnvVar{ + {Name: "EXISTING", Value: "value"}, + }, + expectedEnv: []corev1.EnvVar{ + {Name: "EXISTING", Value: "value"}, + }, + initContainers: []corev1.Container{ + { + Name: "init-setup", + Env: []corev1.EnvVar{ + {Name: "INIT_VAR", Value: "init-value"}, + }, + }, + }, + expectedInitContainerEnv: map[string][]corev1.EnvVar{ + "init-setup": { + {Name: "INIT_VAR", Value: "init-value"}, + }, + }, + }, } for _, tt := range tests { @@ -1670,6 +1795,7 @@ func TestApplyUserDeploymentConfigsWithOverrideEnv(t *testing.T) { Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ + InitContainers: tt.initContainers, Containers: []corev1.Container{ { Name: tt.containerName, @@ -1714,6 +1840,19 @@ func TestApplyUserDeploymentConfigsWithOverrideEnv(t *testing.T) { } } + // Verify init container env vars + for initContainerName, expectedEnvVars := range tt.expectedInitContainerEnv { + initContainer := findContainer(deployment, initContainerName) + if initContainer == nil { + t.Errorf("applyUserDeploymentConfigs() init container %s not found", initContainerName) + continue + } + if !reflect.DeepEqual(initContainer.Env, expectedEnvVars) { + t.Errorf("applyUserDeploymentConfigs() init container %s env vars mismatch.\nExpected: %+v\nActual: %+v", + initContainerName, expectedEnvVars, initContainer.Env) + } + } + // Verify revision history limit if set if tt.componentConfig.DeploymentConfigs != nil && tt.componentConfig.DeploymentConfigs.RevisionHistoryLimit != nil { if deployment.Spec.RevisionHistoryLimit == nil { From 0e22b885bfdb63d69ce3927446ac61e8d267a622 Mon Sep 17 00:00:00 2001 From: siddhi bhor Date: Wed, 11 Feb 2026 11:37:41 +0530 Subject: [PATCH 3/4] adds the test cases for env-var --- pkg/controller/external_secrets/constants.go | 46 ++--- .../external_secrets/deployments_test.go | 2 +- test/e2e/e2e_test.go | 161 +++++++++++++++++- 3 files changed, 183 insertions(+), 26 deletions(-) diff --git a/pkg/controller/external_secrets/constants.go b/pkg/controller/external_secrets/constants.go index 0c8c5f17..0a3fb6f6 100644 --- a/pkg/controller/external_secrets/constants.go +++ b/pkg/controller/external_secrets/constants.go @@ -112,29 +112,29 @@ const ( webhookDeploymentAssetName = "external-secrets/resources/deployment_external-secrets-webhook.yml" // Container names for each component deployment - controllerContainerName = "external-secrets" - webhookContainerName = "webhook" - certControllerContainerName = "cert-controller" - bitwardenContainerName = "bitwarden-sdk-server" - controllerRoleLeaderElectionAssetName = "external-secrets/resources/role_external-secrets-leaderelection.yml" - controllerRoleBindingLeaderElectionAssetName = "external-secrets/resources/rolebinding_external-secrets-leaderelection.yml" - webhookTLSSecretAssetName = "external-secrets/resources/secret_external-secrets-webhook.yml" - bitwardenServiceAssetName = "external-secrets/resources/service_bitwarden-sdk-server.yml" - webhookServiceAssetName = "external-secrets/resources/service_external-secrets-webhook.yml" - metricsServiceAssetName = "external-secrets/resources/service_external-secrets-metrics.yml" - certControllerMetricsServiceAssetName = "external-secrets/resources/service_external-secrets-cert-controller-metrics.yml" - controllerServiceAccountAssetName = "external-secrets/resources/serviceaccount_external-secrets.yml" - bitwardenServiceAccountAssetName = "external-secrets/resources/serviceaccount_bitwarden-sdk-server.yml" - certControllerServiceAccountAssetName = "external-secrets/resources/serviceaccount_external-secrets-cert-controller.yml" - webhookServiceAccountAssetName = "external-secrets/resources/serviceaccount_external-secrets-webhook.yml" - validatingWebhookExternalSecretCRDAssetName = "external-secrets/resources/validatingwebhookconfiguration_externalsecret-validate.yml" - validatingWebhookSecretStoreCRDAssetName = "external-secrets/resources/validatingwebhookconfiguration_secretstore-validate.yml" - denyAllNetworkPolicyAssetName = "external-secrets/networkpolicy_deny-all.yaml" - allowMainControllerTrafficAssetName = "external-secrets/networkpolicy_allow-api-server-egress-for-main-controller-traffic.yaml" - allowWebhookTrafficAssetName = "external-secrets/networkpolicy_allow-api-server-and-webhook-traffic.yaml" - allowCertControllerTrafficAssetName = "external-secrets/networkpolicy_allow-api-server-egress-for-cert-controller-traffic.yaml" - allowBitwardenServerTrafficAssetName = "external-secrets/networkpolicy_allow-api-server-egress-for-bitwarden-sever.yaml" - allowDnsTrafficAsserName = "external-secrets/networkpolicy_allow-dns.yaml" + controllerContainerName = "external-secrets" + webhookContainerName = "webhook" + certControllerContainerName = "cert-controller" + bitwardenContainerName = "bitwarden-sdk-server" + controllerRoleLeaderElectionAssetName = "external-secrets/resources/role_external-secrets-leaderelection.yml" + controllerRoleBindingLeaderElectionAssetName = "external-secrets/resources/rolebinding_external-secrets-leaderelection.yml" + webhookTLSSecretAssetName = "external-secrets/resources/secret_external-secrets-webhook.yml" + bitwardenServiceAssetName = "external-secrets/resources/service_bitwarden-sdk-server.yml" + webhookServiceAssetName = "external-secrets/resources/service_external-secrets-webhook.yml" + metricsServiceAssetName = "external-secrets/resources/service_external-secrets-metrics.yml" + certControllerMetricsServiceAssetName = "external-secrets/resources/service_external-secrets-cert-controller-metrics.yml" + controllerServiceAccountAssetName = "external-secrets/resources/serviceaccount_external-secrets.yml" + bitwardenServiceAccountAssetName = "external-secrets/resources/serviceaccount_bitwarden-sdk-server.yml" + certControllerServiceAccountAssetName = "external-secrets/resources/serviceaccount_external-secrets-cert-controller.yml" + webhookServiceAccountAssetName = "external-secrets/resources/serviceaccount_external-secrets-webhook.yml" + validatingWebhookExternalSecretCRDAssetName = "external-secrets/resources/validatingwebhookconfiguration_externalsecret-validate.yml" + validatingWebhookSecretStoreCRDAssetName = "external-secrets/resources/validatingwebhookconfiguration_secretstore-validate.yml" + denyAllNetworkPolicyAssetName = "external-secrets/networkpolicy_deny-all.yaml" + allowMainControllerTrafficAssetName = "external-secrets/networkpolicy_allow-api-server-egress-for-main-controller-traffic.yaml" + allowWebhookTrafficAssetName = "external-secrets/networkpolicy_allow-api-server-and-webhook-traffic.yaml" + allowCertControllerTrafficAssetName = "external-secrets/networkpolicy_allow-api-server-egress-for-cert-controller-traffic.yaml" + allowBitwardenServerTrafficAssetName = "external-secrets/networkpolicy_allow-api-server-egress-for-bitwarden-sever.yaml" + allowDnsTrafficAsserName = "external-secrets/networkpolicy_allow-dns.yaml" ) var ( diff --git a/pkg/controller/external_secrets/deployments_test.go b/pkg/controller/external_secrets/deployments_test.go index c6cedba2..b51cfe06 100644 --- a/pkg/controller/external_secrets/deployments_test.go +++ b/pkg/controller/external_secrets/deployments_test.go @@ -1480,7 +1480,7 @@ func TestMergeEnvVars(t *testing.T) { }, }, { - name: "empty override env vars does nothing", + name: "empty override env vars does nothing", existingEnv: []corev1.EnvVar{ {Name: "EXISTING", Value: "value"}, }, diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 406d1a74..9b2ac99d 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -28,13 +28,19 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + operatorv1alpha1 "github.com/openshift/external-secrets-operator/api/v1alpha1" "github.com/openshift/external-secrets-operator/test/utils" ) @@ -43,9 +49,9 @@ var testassets embed.FS const ( // test bindata - externalSecretsFile = "testdata/external_secret.yaml" + externalSecretsFile = "testdata/external_secret.yaml" externalSecretsFileWithRevisionLimit = "testdata/external_secret_with_revision_limits.yaml" - expectedSecretValueFile = "testdata/expected_value.yaml" + expectedSecretValueFile = "testdata/expected_value.yaml" ) const ( @@ -73,6 +79,7 @@ var _ = Describe("External Secrets Operator End-to-End test scenarios", Ordered, var ( clientset *kubernetes.Clientset dynamicClient *dynamic.DynamicClient + runtimeClient client.Client loader utils.DynamicResourceLoader awsSecretName string testNamespace string @@ -88,6 +95,14 @@ var _ = Describe("External Secrets Operator End-to-End test scenarios", Ordered, dynamicClient, err = dynamic.NewForConfig(cfg) Expect(err).Should(BeNil()) + // Create scheme and register types + scheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(operatorv1alpha1.AddToScheme(scheme)) + + runtimeClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).Should(BeNil()) + awsSecretName = fmt.Sprintf("eso-e2e-secret-%s", utils.GetRandomString(5)) namespace := &corev1.Namespace{ @@ -233,6 +248,148 @@ var _ = Describe("External Secrets Operator End-to-End test scenarios", Ordered, }) }) + Context("Environment Variables", func() { + // Map component names to deployment names + componentToDeployment := map[string]string{ + "ExternalSecretsCoreController": "external-secrets", + "Webhook": "external-secrets-webhook", + "CertController": "external-secrets-cert-controller", + } + + // Define test env vars + envConfigs := []operatorv1alpha1.ComponentConfig{ + { + ComponentName: "ExternalSecretsCoreController", + OverrideEnv: []corev1.EnvVar{ + {Name: "GOMAXPROCS", Value: "4"}, + {Name: "TEST_CONTROLLER_VAR", Value: "controller-value"}, + }, + }, + { + ComponentName: "Webhook", + OverrideEnv: []corev1.EnvVar{ + {Name: "TLS_MIN_VERSION", Value: "1.2"}, + {Name: "TEST_WEBHOOK_VAR", Value: "webhook-value"}, + }, + }, + { + ComponentName: "CertController", + OverrideEnv: []corev1.EnvVar{ + {Name: "TEST_CERT_VAR", Value: "cert-value"}, + {Name: "FOO", Value: "bar"}, + }, + }, + } + + It("should set custom environment variables for all component deployments", func() { + By("Updating ExternalSecretsConfig with custom env vars") + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + existingCR := &operatorv1alpha1.ExternalSecretsConfig{} + if err := runtimeClient.Get(ctx, client.ObjectKey{Name: "cluster"}, existingCR); err != nil { + return err + } + + updatedCR := existingCR.DeepCopy() + updatedCR.Spec.ControllerConfig = operatorv1alpha1.ControllerConfig{ + ComponentConfigs: envConfigs, + } + + return runtimeClient.Update(ctx, updatedCR) + }) + Expect(err).NotTo(HaveOccurred(), "should update ExternalSecretsConfig with custom env vars") + + By("Waiting for pods to be ready after config update") + Expect(utils.VerifyPodsReadyByPrefix(ctx, clientset, operandNamespace, []string{ + operandCoreControllerPodPrefix, + operandCertControllerPodPrefix, + operandWebhookPodPrefix, + })).To(Succeed()) + + for _, config := range envConfigs { + By(fmt.Sprintf("Verifying custom environment variables in %s deployment", config.ComponentName)) + + deploymentName := componentToDeployment[string(config.ComponentName)] + Eventually(func(g Gomega) { + deployment, err := clientset.AppsV1().Deployments(operandNamespace).Get(ctx, deploymentName, metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred(), "should get %s deployment", deploymentName) + + // Collect env vars from all containers and initContainers + envMap := make(map[string]string) + for _, container := range deployment.Spec.Template.Spec.Containers { + for _, env := range container.Env { + envMap[env.Name] = env.Value + } + } + for _, initContainer := range deployment.Spec.Template.Spec.InitContainers { + for _, env := range initContainer.Env { + envMap[env.Name] = env.Value + } + } + + // Verify all expected env vars are present + for _, expectedEnv := range config.OverrideEnv { + g.Expect(envMap).To(HaveKeyWithValue(expectedEnv.Name, expectedEnv.Value), + "%s should have env var %s=%s", deploymentName, expectedEnv.Name, expectedEnv.Value) + } + }, time.Minute, 5*time.Second).Should(Succeed(), "env vars should be set for %s", config.ComponentName) + } + }) + + It("should remove custom environment variables when config is cleared", func() { + By("Removing custom env vars from ExternalSecretsConfig") + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + existingCR := &operatorv1alpha1.ExternalSecretsConfig{} + if err := runtimeClient.Get(ctx, client.ObjectKey{Name: "cluster"}, existingCR); err != nil { + return err + } + + updatedCR := existingCR.DeepCopy() + updatedCR.Spec.ControllerConfig = operatorv1alpha1.ControllerConfig{ + ComponentConfigs: nil, + } + + return runtimeClient.Update(ctx, updatedCR) + }) + Expect(err).NotTo(HaveOccurred(), "should update ExternalSecretsConfig to remove custom env vars") + + By("Waiting for pods to be ready after config update") + Expect(utils.VerifyPodsReadyByPrefix(ctx, clientset, operandNamespace, []string{ + operandCoreControllerPodPrefix, + operandCertControllerPodPrefix, + operandWebhookPodPrefix, + })).To(Succeed()) + + for _, config := range envConfigs { + By(fmt.Sprintf("Verifying custom environment variables in %s deployment", config.ComponentName)) + + deploymentName := componentToDeployment[string(config.ComponentName)] + Eventually(func(g Gomega) { + deployment, err := clientset.AppsV1().Deployments(operandNamespace).Get(ctx, deploymentName, metav1.GetOptions{}) + g.Expect(err).NotTo(HaveOccurred(), "should get %s deployment", deploymentName) + + // Collect env var names from all containers and initContainers + envNames := make(map[string]bool) + for _, container := range deployment.Spec.Template.Spec.Containers { + for _, env := range container.Env { + envNames[env.Name] = true + } + } + for _, initContainer := range deployment.Spec.Template.Spec.InitContainers { + for _, env := range initContainer.Env { + envNames[env.Name] = true + } + } + + // Verify custom env vars are removed + for _, expectedEnv := range config.OverrideEnv { + g.Expect(envNames).NotTo(HaveKey(expectedEnv.Name), + "%s should not have env var %s after removal", deploymentName, expectedEnv.Name) + } + }, time.Minute, 5*time.Second).Should(Succeed(), "env vars should be removed from %s", config.ComponentName) + } + }) + }) + Context("Deployment Revision History Limit", func() { It("should use default revisionHistoryLimit when not configured", func() { By("Verifying default revisionHistoryLimit (10) for ExternalSecretsCoreController deployment") From 53d4ccd6853fcfc08c7d978e5172021bfbe66514 Mon Sep 17 00:00:00 2001 From: siddhi bhor Date: Wed, 11 Feb 2026 12:31:27 +0530 Subject: [PATCH 4/4] adds the vulnerability for GO-2026-4337 --- hack/govulncheck.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hack/govulncheck.sh b/hack/govulncheck.sh index 31af2ec4..fd568dde 100755 --- a/hack/govulncheck.sh +++ b/hack/govulncheck.sh @@ -27,7 +27,8 @@ set -o errexit # - https://pkg.go.dev/vuln/GO-2026-4340 - Handshake messages may be processed at the incorrect encryption level in crypto/tls # - https://pkg.go.dev/vuln/GO-2025-4175 - Improper application of excluded DNS name constraints when verifying wildcard names in crypto/x509 # - https://pkg.go.dev/vuln/GO-2025-4155 - Excessive resource consumption when printing error string for host certificate validation in crypto/x509 -KNOWN_VULNS_PATTERN="GO-2025-3547|GO-2025-3521|GO-2025-4240|GO-2026-4341|GO-2026-4340|GO-2025-4175|GO-2025-4155" +# - https://pkg.go.dev/vuln/GO-2026-4337 - During session resumption in crypto/tls, if the underlying Config has its ClientCAs or RootCAs fields mutated between the initial handshake and the resumed handshake, the resumed handshake may succeed when it should have failed. +KNOWN_VULNS_PATTERN="GO-2025-3547|GO-2025-3521|GO-2025-4240|GO-2026-4341|GO-2026-4340|GO-2025-4175|GO-2025-4155|GO-2026-4337" GOVULNCHECK_BIN="${1:-}" OUTPUT_DIR="${2:-}"