From 0dbc4daee9b0df85fc4b73d3db136432e3ef52b3 Mon Sep 17 00:00:00 2001 From: Swarup Ghosh Date: Mon, 6 Apr 2026 13:56:13 +0530 Subject: [PATCH] feat: add API types for ExternalSecretsConfig component overrides (EP-1898) Extend the ExternalSecretsConfig API with: - annotations: global custom annotations for all operand Deployments and Pod templates - componentConfigs: per-component configuration overrides (Controller, Webhook, CertController, BitwardenSDKServer) - deploymentConfig: deployment-level overrides including revisionHistoryLimit - overrideEnv: custom environment variables per component with reserved prefix validation Also extends ComponentName enum with Webhook and CertController values, adds KVPair and Annotation types for structured annotation configuration, and includes comprehensive integration test coverage for all new fields and validation rules. Ref: https://github.com/openshift/enhancements/pull/1898 Co-Authored-By: Claude Opus 4.6 --- api/v1alpha1/external_secrets_config_types.go | 85 ++- api/v1alpha1/meta.go | 23 + .../externalsecretsconfig.testsuite.yaml | 678 +++++++++++++++++- api/v1alpha1/zz_generated.deepcopy.go | 86 +++ ...r.openshift.io_externalsecretsconfigs.yaml | 223 ++++++ 5 files changed, 1090 insertions(+), 5 deletions(-) diff --git a/api/v1alpha1/external_secrets_config_types.go b/api/v1alpha1/external_secrets_config_types.go index f06a7df79..2ffcd7203 100644 --- a/api/v1alpha1/external_secrets_config_types.go +++ b/api/v1alpha1/external_secrets_config_types.go @@ -1,6 +1,7 @@ package v1alpha1 import ( + corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -113,6 +114,35 @@ type ControllerConfig struct { // +kubebuilder:validation:Optional Labels map[string]string `json:"labels,omitempty"` + // annotations allows adding custom annotations to all external-secrets component + // Deployments and Pod templates. These annotations are applied globally to all + // operand components (Controller, Webhook, CertController, BitwardenSDKServer). + // These annotations are merged with any default annotations set by the operator. + // User-specified annotations take precedence over defaults in case of conflicts. + // Annotations with keys starting with kubernetes.io/, app.kubernetes.io/, openshift.io/, or k8s.io/ + // are reserved and cannot be overridden. + // + // +kubebuilder:validation:XValidation:rule="self.all(a, !['kubernetes.io/', 'app.kubernetes.io/', 'openshift.io/', 'k8s.io/'].exists(p, a.key.startsWith(p)))",message="annotations with reserved prefixes 'kubernetes.io/', 'app.kubernetes.io/', 'openshift.io/', 'k8s.io/' are not allowed" + // +kubebuilder:validation:MaxItems:=50 + // +kubebuilder:validation:Optional + // +listType=map + // +listMapKey=key + // +optional + Annotations []Annotation `json:"annotations,omitempty"` + + // componentConfigs allows specifying component-specific configuration overrides for + // individual external-secrets components (Controller, Webhook, CertController, BitwardenSDKServer). + // Each entry configures deployment-level overrides and custom environment variables for a single component. + // The componentName must be unique across all entries. + // + // +kubebuilder:validation:XValidation:rule="self.all(x, self.exists_one(y, x.componentName == y.componentName))",message="componentName must be unique across all componentConfig entries" + // +kubebuilder:validation:MaxItems:=4 + // +kubebuilder:validation:Optional + // +listType=map + // +listMapKey=componentName + // +optional + ComponentConfigs []ComponentConfig `json:"componentConfigs,omitempty"` + // networkPolicies specifies the list of network policy configurations // to be applied to external-secrets pods. // @@ -212,14 +242,61 @@ type CertProvidersConfig struct { CertManager *CertManagerConfig `json:"certManager,omitempty"` } -// ComponentName represents the different external-secrets components that can have network policies applied. +// ComponentConfig allows specifying configuration overrides for a single external-secrets component. +// This includes deployment-level configuration such as revisionHistoryLimit and custom environment variables. +type ComponentConfig struct { + // componentName specifies which deployment component this configuration applies to. + // Each component can only appear once in the componentConfigs list. + // +kubebuilder:validation:Enum:=ExternalSecretsCoreController;Webhook;CertController;BitwardenSDKServer + // +kubebuilder:validation:Required + ComponentName ComponentName `json:"componentName"` + + // deploymentConfig allows specifying deployment-level configuration overrides + // for the specified component. + // +kubebuilder:validation:Optional + // +optional + DeploymentConfig DeploymentConfig `json:"deploymentConfig,omitempty"` + + // overrideEnv allows setting custom environment variables for the component's container. + // These environment variables are merged with the default environment variables set by + // the operator. User-specified variables take precedence in case of conflicts. + // Environment variables with names starting with HOSTNAME, KUBERNETES_, or EXTERNAL_SECRETS_ are reserved + // and cannot be overridden. + // + // +kubebuilder:validation:XValidation:rule="self.all(e, !['HOSTNAME', 'KUBERNETES_', 'EXTERNAL_SECRETS_'].exists(p, e.name.startsWith(p)))",message="environment variable names with reserved prefixes 'HOSTNAME', 'KUBERNETES_', 'EXTERNAL_SECRETS_' are not allowed" + // +kubebuilder:validation:MaxItems:=50 + // +kubebuilder:validation:Optional + // +listType=map + // +listMapKey=name + // +optional + OverrideEnv []corev1.EnvVar `json:"overrideEnv,omitempty"` +} + +// DeploymentConfig allows specifying deployment-level configuration overrides. +type DeploymentConfig struct { + // revisionHistoryLimit specifies the number of old ReplicaSets to retain for rollback. + // Minimum value of 1 is enforced to ensure rollback capability. + // + // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Optional + // +optional + RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty"` +} + +// ComponentName represents the different external-secrets components that can be configured. type ComponentName string const ( - // CoreController represents the external-secrets component + // CoreController represents the external-secrets core controller component. CoreController ComponentName = "ExternalSecretsCoreController" - // BitwardenSDKServer represents the bitwarden-sdk-server component + // Webhook represents the external-secrets webhook component. + Webhook ComponentName = "Webhook" + + // CertController represents the external-secrets cert-controller component. + CertController ComponentName = "CertController" + + // BitwardenSDKServer represents the bitwarden-sdk-server component. BitwardenSDKServer ComponentName = "BitwardenSDKServer" ) @@ -234,7 +311,7 @@ type NetworkPolicy struct { Name string `json:"name"` // componentName specifies which external-secrets component this network policy applies to. - // +kubebuilder:validation:Enum:=ExternalSecretsCoreController;BitwardenSDKServer + // +kubebuilder:validation:Enum:=ExternalSecretsCoreController;Webhook;CertController;BitwardenSDKServer // +kubebuilder:validation:Required ComponentName ComponentName `json:"componentName"` diff --git a/api/v1alpha1/meta.go b/api/v1alpha1/meta.go index 68a273d97..c66f6987e 100644 --- a/api/v1alpha1/meta.go +++ b/api/v1alpha1/meta.go @@ -112,6 +112,29 @@ type ProxyConfig struct { NoProxy string `json:"noProxy,omitempty"` } +// KVPair represents a generic key-value pair for configuration. +type KVPair struct { + // key is the name of the key-value pair. + // This field must be non-empty and can have a maximum of 317 characters. + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=317 + // +kubebuilder:validation:Required + Key string `json:"key"` + + // value is the value of the key-value pair. + // This field can have a maximum of 4096 characters. + // +kubebuilder:validation:MaxLength:=4096 + // +kubebuilder:validation:Optional + Value string `json:"value,omitempty"` +} + +// Annotation represents a custom annotation key-value pair. +// It embeds KVPair inline for reusability. +type Annotation struct { + // Embedded KVPair provides key and value fields. + KVPair `json:",inline"` +} + // Mode indicates the operational state of the optional features. type Mode string diff --git a/api/v1alpha1/tests/externalsecretsconfig.operator.openshift.io/externalsecretsconfig.testsuite.yaml b/api/v1alpha1/tests/externalsecretsconfig.operator.openshift.io/externalsecretsconfig.testsuite.yaml index 720d7fc23..7583f3112 100644 --- a/api/v1alpha1/tests/externalsecretsconfig.operator.openshift.io/externalsecretsconfig.testsuite.yaml +++ b/api/v1alpha1/tests/externalsecretsconfig.operator.openshift.io/externalsecretsconfig.testsuite.yaml @@ -493,6 +493,500 @@ tests: webhookConfig: certificateCheckInterval: "15m" operatingNamespace: "test-ns" + - name: Should be able to create ExternalSecretsConfig with custom annotations + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "example.com/custom-annotation" + value: "my-value" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "example.com/custom-annotation" + value: "my-value" + - name: Should be able to create ExternalSecretsConfig with multiple annotations + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "example.com/annotation-one" + value: "value-one" + - key: "example.com/annotation-two" + value: "value-two" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "example.com/annotation-one" + value: "value-one" + - key: "example.com/annotation-two" + value: "value-two" + - name: Should fail to create with annotation using reserved prefix kubernetes.io/ + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "kubernetes.io/some-annotation" + value: "forbidden" + expectedError: "annotations with reserved prefixes 'kubernetes.io/', 'app.kubernetes.io/', 'openshift.io/', 'k8s.io/' are not allowed" + - name: Should fail to create with annotation using reserved prefix app.kubernetes.io/ + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "app.kubernetes.io/name" + value: "forbidden" + expectedError: "annotations with reserved prefixes 'kubernetes.io/', 'app.kubernetes.io/', 'openshift.io/', 'k8s.io/' are not allowed" + - name: Should fail to create with annotation using reserved prefix openshift.io/ + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "openshift.io/some-annotation" + value: "forbidden" + expectedError: "annotations with reserved prefixes 'kubernetes.io/', 'app.kubernetes.io/', 'openshift.io/', 'k8s.io/' are not allowed" + - name: Should fail to create with annotation using reserved prefix k8s.io/ + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "k8s.io/some-annotation" + value: "forbidden" + expectedError: "annotations with reserved prefixes 'kubernetes.io/', 'app.kubernetes.io/', 'openshift.io/', 'k8s.io/' are not allowed" + - name: Should fail to create with annotation missing required key + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - value: "value-without-key" + expectedError: "spec.controllerConfig.annotations[0].key: Required value" + - name: Should be able to create ExternalSecretsConfig with componentConfigs for Controller + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 5 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 5 + - name: Should be able to create ExternalSecretsConfig with componentConfigs for Webhook + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: Webhook + deploymentConfig: + revisionHistoryLimit: 3 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: Webhook + deploymentConfig: + revisionHistoryLimit: 3 + - name: Should be able to create ExternalSecretsConfig with componentConfigs for CertController + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: CertController + deploymentConfig: + revisionHistoryLimit: 3 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: CertController + deploymentConfig: + revisionHistoryLimit: 3 + - name: Should be able to create ExternalSecretsConfig with componentConfigs for BitwardenSDKServer + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: BitwardenSDKServer + deploymentConfig: + revisionHistoryLimit: 2 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: BitwardenSDKServer + deploymentConfig: + revisionHistoryLimit: 2 + - name: Should be able to create ExternalSecretsConfig with multiple componentConfigs + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 10 + - componentName: Webhook + deploymentConfig: + revisionHistoryLimit: 3 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 10 + - componentName: Webhook + deploymentConfig: + revisionHistoryLimit: 3 + - name: Should be able to create ExternalSecretsConfig with all four componentConfigs + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 10 + - componentName: Webhook + deploymentConfig: + revisionHistoryLimit: 3 + - componentName: CertController + deploymentConfig: + revisionHistoryLimit: 5 + - componentName: BitwardenSDKServer + deploymentConfig: + revisionHistoryLimit: 2 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 10 + - componentName: Webhook + deploymentConfig: + revisionHistoryLimit: 3 + - componentName: CertController + deploymentConfig: + revisionHistoryLimit: 5 + - componentName: BitwardenSDKServer + deploymentConfig: + revisionHistoryLimit: 2 + - name: Should fail to create with invalid componentName + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: InvalidComponent + deploymentConfig: + revisionHistoryLimit: 5 + expectedError: "spec.controllerConfig.componentConfigs[0].componentName: Unsupported value: \"InvalidComponent\"" + - name: Should fail to create with duplicate componentName in componentConfigs + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 5 + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 10 + expectedError: "spec.controllerConfig.componentConfigs[1].componentName: Duplicate value: \"ExternalSecretsCoreController\"" + - name: Should fail to create with revisionHistoryLimit less than 1 + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 0 + expectedError: "spec.controllerConfig.componentConfigs[0].deploymentConfig.revisionHistoryLimit: Invalid value: 0: spec.controllerConfig.componentConfigs[0].deploymentConfig.revisionHistoryLimit in body should be greater than or equal to 1" + - name: Should be able to create ExternalSecretsConfig with overrideEnv + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + overrideEnv: + - name: GOMAXPROCS + value: "4" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + overrideEnv: + - name: GOMAXPROCS + value: "4" + - name: Should be able to create ExternalSecretsConfig with multiple overrideEnv entries + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: Webhook + overrideEnv: + - name: GOMAXPROCS + value: "2" + - name: LOG_FORMAT + value: "json" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: Webhook + overrideEnv: + - name: GOMAXPROCS + value: "2" + - name: LOG_FORMAT + value: "json" + - name: Should fail to create with overrideEnv using reserved prefix HOSTNAME + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + overrideEnv: + - name: HOSTNAME + value: "custom-host" + expectedError: "environment variable names with reserved prefixes 'HOSTNAME', 'KUBERNETES_', 'EXTERNAL_SECRETS_' are not allowed" + - name: Should fail to create with overrideEnv using reserved prefix KUBERNETES_ + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + overrideEnv: + - name: KUBERNETES_SERVICE_HOST + value: "10.0.0.1" + expectedError: "environment variable names with reserved prefixes 'HOSTNAME', 'KUBERNETES_', 'EXTERNAL_SECRETS_' are not allowed" + - name: Should fail to create with overrideEnv using reserved prefix EXTERNAL_SECRETS_ + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + overrideEnv: + - name: EXTERNAL_SECRETS_CUSTOM_VAR + value: "not-allowed" + expectedError: "environment variable names with reserved prefixes 'HOSTNAME', 'KUBERNETES_', 'EXTERNAL_SECRETS_' are not allowed" + - name: Should be able to create ExternalSecretsConfig with componentConfigs and overrideEnv together + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 10 + overrideEnv: + - name: GOMAXPROCS + value: "4" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 10 + overrideEnv: + - name: GOMAXPROCS + value: "4" + - name: Should be able to create ExternalSecretsConfig with annotations and componentConfigs combined + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "example.com/custom-annotation" + value: "my-value" + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 10 + overrideEnv: + - name: GOMAXPROCS + value: "4" + - componentName: Webhook + deploymentConfig: + revisionHistoryLimit: 3 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "example.com/custom-annotation" + value: "my-value" + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 10 + overrideEnv: + - name: GOMAXPROCS + value: "4" + - componentName: Webhook + deploymentConfig: + revisionHistoryLimit: 3 + - name: Should be able to create componentConfig without deploymentConfig (only overrideEnv) + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: CertController + overrideEnv: + - name: CUSTOM_VAR + value: "custom-value" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: CertController + overrideEnv: + - name: CUSTOM_VAR + value: "custom-value" + - name: Should be able to create componentConfig without overrideEnv (only deploymentConfig) + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 7 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 7 + - name: Should fail to create with more than 4 componentConfigs + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 5 + - componentName: Webhook + deploymentConfig: + revisionHistoryLimit: 3 + - componentName: CertController + deploymentConfig: + revisionHistoryLimit: 2 + - componentName: BitwardenSDKServer + deploymentConfig: + revisionHistoryLimit: 1 + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 10 + expectedError: "spec.controllerConfig.componentConfigs: Too many: 5: must have at most 4 items" onUpdate: - name: Should be able to update labels in controller config resourceName: cluster @@ -596,4 +1090,186 @@ tests: bitwardenSecretManagerProvider: mode: Enabled secretRef: - name: "bitwarden-certs" \ No newline at end of file + name: "bitwarden-certs" + - name: Should be able to add annotations after creation + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: {} + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "example.com/new-annotation" + value: "new-value" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "example.com/new-annotation" + value: "new-value" + - name: Should be able to update annotations + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "example.com/annotation" + value: "old-value" + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "example.com/annotation" + value: "new-value" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "example.com/annotation" + value: "new-value" + - name: Should be able to add componentConfigs after creation + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: {} + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 5 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 5 + - name: Should be able to update revisionHistoryLimit + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 5 + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 10 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 10 + - name: Should be able to add overrideEnv after creation + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 5 + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 5 + overrideEnv: + - name: GOMAXPROCS + value: "4" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + deploymentConfig: + revisionHistoryLimit: 5 + overrideEnv: + - name: GOMAXPROCS + value: "4" + - name: Should fail to update with annotation using reserved prefix + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "example.com/annotation" + value: "valid" + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + annotations: + - key: "example.com/annotation" + value: "valid" + - key: "kubernetes.io/forbidden" + value: "not-allowed" + expectedError: "annotations with reserved prefixes 'kubernetes.io/', 'app.kubernetes.io/', 'openshift.io/', 'k8s.io/' are not allowed" + - name: Should fail to update with overrideEnv using reserved prefix + resourceName: cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + overrideEnv: + - name: GOMAXPROCS + value: "4" + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: ExternalSecretsConfig + spec: + controllerConfig: + componentConfigs: + - componentName: ExternalSecretsCoreController + overrideEnv: + - name: GOMAXPROCS + value: "4" + - name: HOSTNAME + value: "custom" + expectedError: "environment variable names with reserved prefixes 'HOSTNAME', 'KUBERNETES_', 'EXTERNAL_SECRETS_' are not allowed" \ No newline at end of file diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 99f6ec14b..c62f1acf7 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -27,6 +27,22 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Annotation) DeepCopyInto(out *Annotation) { + *out = *in + out.KVPair = in.KVPair +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Annotation. +func (in *Annotation) DeepCopy() *Annotation { + if in == nil { + return nil + } + out := new(Annotation) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApplicationConfig) DeepCopyInto(out *ApplicationConfig) { *out = *in @@ -162,6 +178,29 @@ func (in *CommonConfigs) DeepCopy() *CommonConfigs { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComponentConfig) DeepCopyInto(out *ComponentConfig) { + *out = *in + in.DeploymentConfig.DeepCopyInto(&out.DeploymentConfig) + if in.OverrideEnv != nil { + in, out := &in.OverrideEnv, &out.OverrideEnv + *out = make([]corev1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentConfig. +func (in *ComponentConfig) DeepCopy() *ComponentConfig { + if in == nil { + return nil + } + out := new(ComponentConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Condition) DeepCopyInto(out *Condition) { *out = *in @@ -214,6 +253,18 @@ func (in *ControllerConfig) DeepCopyInto(out *ControllerConfig) { (*out)[key] = val } } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make([]Annotation, len(*in)) + copy(*out, *in) + } + if in.ComponentConfigs != nil { + in, out := &in.ComponentConfigs, &out.ComponentConfigs + *out = make([]ComponentConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.NetworkPolicies != nil { in, out := &in.NetworkPolicies, &out.NetworkPolicies *out = make([]NetworkPolicy, len(*in)) @@ -253,6 +304,26 @@ func (in *ControllerStatus) DeepCopy() *ControllerStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentConfig) DeepCopyInto(out *DeploymentConfig) { + *out = *in + if in.RevisionHistoryLimit != nil { + in, out := &in.RevisionHistoryLimit, &out.RevisionHistoryLimit + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentConfig. +func (in *DeploymentConfig) DeepCopy() *DeploymentConfig { + if in == nil { + return nil + } + out := new(DeploymentConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExternalSecretsConfig) DeepCopyInto(out *ExternalSecretsConfig) { *out = *in @@ -471,6 +542,21 @@ func (in *GlobalConfig) DeepCopy() *GlobalConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KVPair) DeepCopyInto(out *KVPair) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KVPair. +func (in *KVPair) DeepCopy() *KVPair { + if in == nil { + return nil + } + out := new(KVPair) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NetworkPolicy) DeepCopyInto(out *NetworkPolicy) { *out = *in diff --git a/config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml b/config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml index ae6890cdc..88581a81e 100644 --- a/config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml +++ b/config/crd/bases/operator.openshift.io_externalsecretsconfigs.yaml @@ -1173,6 +1173,46 @@ spec: for the controller to use while installing the `external-secrets` operand and the plugins. properties: + annotations: + description: |- + annotations allows adding custom annotations to all external-secrets component + Deployments and Pod templates. These annotations are applied globally to all + operand components (Controller, Webhook, CertController, BitwardenSDKServer). + These annotations are merged with any default annotations set by the operator. + User-specified annotations take precedence over defaults in case of conflicts. + Annotations with keys starting with kubernetes.io/, app.kubernetes.io/, openshift.io/, or k8s.io/ + are reserved and cannot be overridden. + items: + description: |- + Annotation represents a custom annotation key-value pair. + It embeds KVPair inline for reusability. + properties: + key: + description: |- + key is the name of the key-value pair. + This field must be non-empty and can have a maximum of 317 characters. + maxLength: 317 + minLength: 1 + type: string + value: + description: |- + value is the value of the key-value pair. + This field can have a maximum of 4096 characters. + maxLength: 4096 + type: string + required: + - key + type: object + maxItems: 50 + type: array + x-kubernetes-list-map-keys: + - key + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: annotations with reserved prefixes 'kubernetes.io/', + 'app.kubernetes.io/', 'openshift.io/', 'k8s.io/' are not allowed + rule: self.all(a, !['kubernetes.io/', 'app.kubernetes.io/', + 'openshift.io/', 'k8s.io/'].exists(p, a.key.startsWith(p))) certProvider: description: certProvider is for defining the configuration for certificate providers used to manage TLS certificates for webhook @@ -1263,6 +1303,187 @@ spec: rule: 'has(self.injectAnnotations) && self.injectAnnotations != ''false'' ? self.mode != ''Disabled'' : true' type: object + componentConfigs: + description: |- + componentConfigs allows specifying component-specific configuration overrides for + individual external-secrets components (Controller, Webhook, CertController, BitwardenSDKServer). + Each entry configures deployment-level overrides and custom environment variables for a single component. + The componentName must be unique across all entries. + items: + description: |- + ComponentConfig allows specifying configuration overrides for a single external-secrets component. + This includes deployment-level configuration such as revisionHistoryLimit and custom environment variables. + properties: + componentName: + description: |- + componentName specifies which deployment component this configuration applies to. + Each component can only appear once in the componentConfigs list. + enum: + - ExternalSecretsCoreController + - Webhook + - CertController + - BitwardenSDKServer + type: string + deploymentConfig: + description: |- + deploymentConfig allows specifying deployment-level configuration overrides + for the specified component. + properties: + revisionHistoryLimit: + description: |- + revisionHistoryLimit specifies the number of old ReplicaSets to retain for rollback. + Minimum value of 1 is enforced to ensure rollback capability. + format: int32 + minimum: 1 + type: integer + type: object + overrideEnv: + description: |- + overrideEnv allows setting custom environment variables for the component's container. + These environment variables are merged with the default environment variables set by + the operator. User-specified variables take precedence in case of conflicts. + Environment variables with names starting with HOSTNAME, KUBERNETES_, or EXTERNAL_SECRETS_ are reserved + and cannot be overridden. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + maxItems: 50 + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: environment variable names with reserved prefixes + 'HOSTNAME', 'KUBERNETES_', 'EXTERNAL_SECRETS_' are not + allowed + rule: self.all(e, !['HOSTNAME', 'KUBERNETES_', 'EXTERNAL_SECRETS_'].exists(p, + e.name.startsWith(p))) + required: + - componentName + type: object + maxItems: 4 + type: array + x-kubernetes-list-map-keys: + - componentName + x-kubernetes-list-type: map + x-kubernetes-validations: + - message: componentName must be unique across all componentConfig + entries + rule: self.all(x, self.exists_one(y, x.componentName == y.componentName)) labels: additionalProperties: type: string @@ -1293,6 +1514,8 @@ spec: component this network policy applies to. enum: - ExternalSecretsCoreController + - Webhook + - CertController - BitwardenSDKServer type: string egress: