diff --git a/api/v1alpha1/external_secrets_config_types.go b/api/v1alpha1/external_secrets_config_types.go
index ccac0512..5f45b534 100644
--- a/api/v1alpha1/external_secrets_config_types.go
+++ b/api/v1alpha1/external_secrets_config_types.go
@@ -191,6 +191,13 @@ type ComponentConfig struct {
// +listMapKey=name
// +optional
OverrideEnv []corev1.EnvVar `json:"overrideEnv,omitempty"`
+
+ // extraArgs specifies additional command-line arguments for this component's container.
+ // These are appended (de-duped) to the operator's default args for the component.
+ // +kubebuilder:validation:MaxItems:=50
+ // +listType=atomic
+ // +optional
+ ExtraArgs []string `json:"extraArgs,omitempty"`
}
// DeploymentConfig defines configuration overrides for a Kubernetes Deployment resource.
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 3743fc25..36b71e60 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -177,6 +177,11 @@ func (in *ComponentConfig) DeepCopyInto(out *ComponentConfig) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.ExtraArgs != nil {
+ in, out := &in.ExtraArgs, &out.ExtraArgs
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentConfig.
diff --git a/bundle/manifests/openshift-external-secrets-operator.clusterserviceversion.yaml b/bundle/manifests/openshift-external-secrets-operator.clusterserviceversion.yaml
index fd449d41..0a55e72f 100644
--- a/bundle/manifests/openshift-external-secrets-operator.clusterserviceversion.yaml
+++ b/bundle/manifests/openshift-external-secrets-operator.clusterserviceversion.yaml
@@ -220,7 +220,7 @@ metadata:
categories: Security
console.openshift.io/disable-operand-delete: "true"
containerImage: openshift.io/external-secrets-operator:latest
- createdAt: "2026-06-19T12:17:03Z"
+ createdAt: "2026-06-20T09:38:01Z"
features.operators.openshift.io/cnf: "false"
features.operators.openshift.io/cni: "false"
features.operators.openshift.io/csi: "false"
diff --git a/bundle/manifests/operator.openshift.io_externalsecretsconfigs.yaml b/bundle/manifests/operator.openshift.io_externalsecretsconfigs.yaml
index 699f85bf..ccaca704 100644
--- a/bundle/manifests/operator.openshift.io_externalsecretsconfigs.yaml
+++ b/bundle/manifests/operator.openshift.io_externalsecretsconfigs.yaml
@@ -1330,6 +1330,15 @@ spec:
minimum: 1
type: integer
type: object
+ extraArgs:
+ description: |-
+ extraArgs specifies additional command-line arguments for this component's container.
+ These are appended (de-duped) to the operator's default args for the component.
+ items:
+ type: string
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
overrideEnv:
description: |-
overrideEnv specifies custom environment variables for this component's container. These are merged with operator-managed environment variables, with user-defined values taking precedence.
diff --git a/config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml b/config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml
index 4dcfb213..3d81298e 100644
--- a/config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml
+++ b/config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml
@@ -1330,6 +1330,15 @@ spec:
minimum: 1
type: integer
type: object
+ extraArgs:
+ description: |-
+ extraArgs specifies additional command-line arguments for this component's container.
+ These are appended (de-duped) to the operator's default args for the component.
+ items:
+ type: string
+ maxItems: 50
+ type: array
+ x-kubernetes-list-type: atomic
overrideEnv:
description: |-
overrideEnv specifies custom environment variables for this component's container. These are merged with operator-managed environment variables, with user-defined values taking precedence.
diff --git a/docs/api_reference.md b/docs/api_reference.md
index a9851a1b..cefb553a 100644
--- a/docs/api_reference.md
+++ b/docs/api_reference.md
@@ -130,6 +130,7 @@ _Appears in:_
| `componentName` _[ComponentName](#componentname)_ | componentName identifies which external-secrets component this configuration applies to.
Valid component names: ExternalSecretsCoreController, Webhook, CertController, BitwardenSDKServer. | | Enum: [ExternalSecretsCoreController Webhook CertController BitwardenSDKServer]
|
| `deploymentConfigs` _[DeploymentConfig](#deploymentconfig)_ | deploymentConfigs specifies overrides for the Kubernetes Deployment resource of this component. | | |
| `overrideEnv` _[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#envvar-v1-core) array_ | overrideEnv specifies custom environment variables for this component's container. These are merged with operator-managed environment variables, with user-defined values taking precedence.
Names starting with 'KUBERNETES_' or 'EXTERNAL_SECRETS_' are reserved prefixes and will be rejected.
The exact names 'HOSTNAME', 'SSL_CERT_DIR', and 'SSL_CERT_FILE' are also reserved. | | MaxItems: 50
|
+| `extraArgs` _string array_ | extraArgs specifies additional command-line arguments for this component's container.
These are appended (de-duped) to the operator's default args for the component. | | MaxItems: 50
|
#### ComponentName
diff --git a/pkg/controller/external_secrets/deployments.go b/pkg/controller/external_secrets/deployments.go
index 3f2ed58e..f30751e1 100644
--- a/pkg/controller/external_secrets/deployments.go
+++ b/pkg/controller/external_secrets/deployments.go
@@ -5,6 +5,7 @@ import (
"maps"
"os"
"slices"
+ "strings"
"time"
"unsafe"
@@ -815,6 +816,16 @@ func (r *Reconciler) applyUserDeploymentConfigs(deployment *appsv1.Deployment, e
}
}
}
+
+ // Apply ExtraArgs only to the target component container.
+ if len(i.ExtraArgs) > 0 {
+ for j := range deployment.Spec.Template.Spec.Containers {
+ if deployment.Spec.Template.Spec.Containers[j].Name == containerName {
+ mergeArgs(&deployment.Spec.Template.Spec.Containers[j], i.ExtraArgs)
+ break
+ }
+ }
+ }
break
}
}
@@ -844,6 +855,29 @@ func mergeUserEnvVars(container *corev1.Container, overrideEnv []corev1.EnvVar)
}
}
+// mergeArgs merges user-defined extra arguments into a container, user-defined values take precedence over existing values.
+func mergeArgs(container *corev1.Container, extraArgs []string) {
+ if container.Args == nil {
+ container.Args = []string{}
+ }
+
+ for _, extra := range extraArgs {
+ extraKey, _, _ := strings.Cut(extra, "=")
+ found := false
+ for i, existing := range container.Args {
+ existingKey, _, _ := strings.Cut(existing, "=")
+ if existingKey == extraKey {
+ container.Args[i] = extra // User-defined value takes precedence
+ found = true
+ break
+ }
+ }
+ if !found {
+ container.Args = append(container.Args, extra)
+ }
+ }
+}
+
// getComponentNameFromAsset maps asset file names to ComponentName enum values and container names.
func getComponentNameFromAsset(assetName string) (operatorv1alpha1.ComponentName, string, error) {
switch assetName {
diff --git a/pkg/controller/external_secrets/deployments_test.go b/pkg/controller/external_secrets/deployments_test.go
index 8c21569b..e52511e0 100644
--- a/pkg/controller/external_secrets/deployments_test.go
+++ b/pkg/controller/external_secrets/deployments_test.go
@@ -718,6 +718,66 @@ func TestCreateOrApplyDeployments(t *testing.T) {
},
validateDeployment: validateRevisionHistory(10),
},
+ {
+ name: "deployment with extraArgs appended to controller default args",
+ preReq: func(r *Reconciler, m *fakes.FakeCtrlClient, d **appsv1.Deployment) {
+ setupDeploymentCreate(m, d, "external-secrets")
+ },
+ updateExternalSecretsConfig: escWithComponentConfigs(v1alpha1.ComponentConfig{
+ ComponentName: v1alpha1.CoreController,
+ ExtraArgs: []string{"--client-qps=100"},
+ }),
+ validateDeployment: func(t *testing.T, d *appsv1.Deployment) {
+ if d == nil {
+ t.Error("deployment should not be nil")
+ return
+ }
+ for _, c := range d.Spec.Template.Spec.Containers {
+ if c.Name == "external-secrets" {
+ for _, arg := range c.Args {
+ if arg == "--client-qps=100" {
+ return
+ }
+ }
+ t.Errorf("expected --client-qps=100 in args, got %v", c.Args)
+ return
+ }
+ }
+ t.Error("external-secrets container not found")
+ },
+ },
+ {
+ name: "deployment with extraArgs overriding controller default arg",
+ preReq: func(r *Reconciler, m *fakes.FakeCtrlClient, d **appsv1.Deployment) {
+ setupDeploymentCreate(m, d, "external-secrets")
+ },
+ updateExternalSecretsConfig: escWithComponentConfigs(v1alpha1.ComponentConfig{
+ ComponentName: v1alpha1.CoreController,
+ ExtraArgs: []string{"--concurrent=5"},
+ }),
+ validateDeployment: func(t *testing.T, d *appsv1.Deployment) {
+ if d == nil {
+ t.Error("deployment should not be nil")
+ return
+ }
+ for _, c := range d.Spec.Template.Spec.Containers {
+ if c.Name == "external-secrets" {
+ for _, arg := range c.Args {
+ if arg == "--concurrent=1" {
+ t.Errorf("default --concurrent=1 should have been overridden, got args: %v", c.Args)
+ return
+ }
+ if arg == "--concurrent=5" {
+ return
+ }
+ }
+ t.Errorf("expected --concurrent=5 in args, got %v", c.Args)
+ return
+ }
+ }
+ t.Error("external-secrets container not found")
+ },
+ },
{
name: "multiple components with mixed revisionHistoryLimit configurations",
preReq: func(r *Reconciler, m *fakes.FakeCtrlClient, d **appsv1.Deployment) {
@@ -1873,6 +1933,67 @@ func TestMergeContainerEnvVars(t *testing.T) {
})
}
+func TestMergeArgs(t *testing.T) {
+ tests := []struct {
+ name string
+ existingArgs []string
+ extraArgs []string
+ expectedArgs []string
+ }{
+ {
+ name: "nil container args, add new args",
+ existingArgs: nil,
+ extraArgs: []string{"--loglevel=debug", "--timeout=30s"},
+ expectedArgs: []string{"--loglevel=debug", "--timeout=30s"},
+ },
+ {
+ name: "override existing arg by flag key",
+ existingArgs: []string{"--loglevel=info", "--metrics-addr=:8080"},
+ extraArgs: []string{"--loglevel=debug"},
+ expectedArgs: []string{"--loglevel=debug", "--metrics-addr=:8080"},
+ },
+ {
+ name: "add new arg to existing ones",
+ existingArgs: []string{"--loglevel=info"},
+ extraArgs: []string{"--timeout=30s"},
+ expectedArgs: []string{"--loglevel=info", "--timeout=30s"},
+ },
+ {
+ name: "mix of override and new args",
+ existingArgs: []string{"--loglevel=info", "--metrics-addr=:8080"},
+ extraArgs: []string{"--loglevel=debug", "--timeout=30s"},
+ expectedArgs: []string{"--loglevel=debug", "--metrics-addr=:8080", "--timeout=30s"},
+ },
+ {
+ name: "empty extra args does nothing",
+ existingArgs: []string{"--loglevel=info"},
+ extraArgs: []string{},
+ expectedArgs: []string{"--loglevel=info"},
+ },
+ {
+ name: "positional arg without equals sign is matched exactly",
+ existingArgs: []string{"webhook", "--loglevel=info"},
+ extraArgs: []string{"webhook"},
+ expectedArgs: []string{"webhook", "--loglevel=info"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ container := &corev1.Container{
+ Name: "test-container",
+ Args: tt.existingArgs,
+ }
+
+ mergeArgs(container, tt.extraArgs)
+
+ if !reflect.DeepEqual(container.Args, tt.expectedArgs) {
+ t.Errorf("mergeArgs() got %v, want %v", container.Args, tt.expectedArgs)
+ }
+ })
+ }
+}
+
func TestApplyUserDeploymentConfigsWithOverrideEnv(t *testing.T) {
tests := []struct {
name string
@@ -2396,6 +2517,148 @@ func TestApplyUserCABundleConfig(t *testing.T) {
}
}
+func TestApplyUserDeploymentConfigsWithExtraArgs(t *testing.T) {
+ tests := []struct {
+ name string
+ assetName string
+ containerName string
+ componentConfig v1alpha1.ComponentConfig
+ existingArgs []string
+ expectedArgs []string
+ }{
+ {
+ name: "append extra arg to core controller",
+ assetName: controllerDeploymentAssetName,
+ containerName: "external-secrets",
+ componentConfig: v1alpha1.ComponentConfig{
+ ComponentName: v1alpha1.CoreController,
+ ExtraArgs: []string{"--enable-feature=true"},
+ },
+ existingArgs: []string{"--loglevel=info", "--metrics-addr=:8080"},
+ expectedArgs: []string{"--loglevel=info", "--metrics-addr=:8080", "--enable-feature=true"},
+ },
+ {
+ name: "extra arg overrides existing arg by flag key",
+ assetName: controllerDeploymentAssetName,
+ containerName: "external-secrets",
+ componentConfig: v1alpha1.ComponentConfig{
+ ComponentName: v1alpha1.CoreController,
+ ExtraArgs: []string{"--loglevel=debug"},
+ },
+ existingArgs: []string{"--loglevel=info", "--metrics-addr=:8080"},
+ expectedArgs: []string{"--loglevel=debug", "--metrics-addr=:8080"},
+ },
+ {
+ name: "apply extra args to webhook container",
+ assetName: webhookDeploymentAssetName,
+ containerName: "webhook",
+ componentConfig: v1alpha1.ComponentConfig{
+ ComponentName: v1alpha1.Webhook,
+ ExtraArgs: []string{"--timeout=60s"},
+ },
+ existingArgs: []string{"webhook", "--port=10250"},
+ expectedArgs: []string{"webhook", "--port=10250", "--timeout=60s"},
+ },
+ {
+ name: "apply extra args to cert-controller container",
+ assetName: certControllerDeploymentAssetName,
+ containerName: "cert-controller",
+ componentConfig: v1alpha1.ComponentConfig{
+ ComponentName: v1alpha1.CertController,
+ ExtraArgs: []string{"--requeue-interval=10m"},
+ },
+ existingArgs: []string{"certcontroller", "--metrics-addr=:8080"},
+ expectedArgs: []string{"certcontroller", "--metrics-addr=:8080", "--requeue-interval=10m"},
+ },
+ {
+ name: "apply extra args to bitwarden container",
+ assetName: bitwardenDeploymentAssetName,
+ containerName: "bitwarden-sdk-server",
+ componentConfig: v1alpha1.ComponentConfig{
+ ComponentName: v1alpha1.BitwardenSDKServer,
+ ExtraArgs: []string{"--log-level=debug"},
+ },
+ existingArgs: []string{"--port=9090"},
+ expectedArgs: []string{"--port=9090", "--log-level=debug"},
+ },
+ {
+ name: "empty extra args does not modify container",
+ assetName: controllerDeploymentAssetName,
+ containerName: "external-secrets",
+ componentConfig: v1alpha1.ComponentConfig{
+ ComponentName: v1alpha1.CoreController,
+ ExtraArgs: nil,
+ },
+ existingArgs: []string{"--loglevel=info"},
+ expectedArgs: []string{"--loglevel=info"},
+ },
+ {
+ name: "both extra args and override env applied together",
+ assetName: controllerDeploymentAssetName,
+ containerName: "external-secrets",
+ componentConfig: v1alpha1.ComponentConfig{
+ ComponentName: v1alpha1.CoreController,
+ ExtraArgs: []string{"--enable-feature=true"},
+ OverrideEnv: []corev1.EnvVar{{Name: "LOG_LEVEL", Value: "debug"}},
+ },
+ existingArgs: []string{"--loglevel=info"},
+ expectedArgs: []string{"--loglevel=info", "--enable-feature=true"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ r := &Reconciler{}
+ initArgs := []string{"--init-only-flag"}
+ deployment := &appsv1.Deployment{
+ Spec: appsv1.DeploymentSpec{
+ Template: corev1.PodTemplateSpec{
+ Spec: corev1.PodSpec{
+ InitContainers: []corev1.Container{
+ {Name: "init-setup", Args: initArgs},
+ },
+ Containers: []corev1.Container{
+ {
+ Name: tt.containerName,
+ Args: tt.existingArgs,
+ },
+ },
+ },
+ },
+ },
+ }
+
+ 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 !reflect.DeepEqual(container.Args, tt.expectedArgs) {
+ t.Errorf("applyUserDeploymentConfigs() args = %v, want %v", container.Args, tt.expectedArgs)
+ }
+
+ // Verify init containers are NOT modified by ExtraArgs
+ if len(deployment.Spec.Template.Spec.InitContainers) > 0 {
+ initContainer := &deployment.Spec.Template.Spec.InitContainers[0]
+ if !reflect.DeepEqual(initContainer.Args, initArgs) {
+ t.Errorf("applyUserDeploymentConfigs() should not modify init container args.\nExpected: %+v\nActual: %+v",
+ initArgs, initContainer.Args)
+ }
+ }
+ })
+ }
+}
+
func TestCreateOrApplyDeploymentFromAssetReturnsTrustedCAError(t *testing.T) {
esc := commontest.TestExternalSecretsConfig()
esc.Spec.ControllerConfig.TrustedCABundle = &v1alpha1.ConfigMapKeyReference{