Skip to content

Commit 9034bd5

Browse files
swghoshclaude
andcommitted
feat: implement controller logic for component configuration overrides
Add controller reconciliation logic for: - annotations: applies user-defined annotations to all operand Deployments and Pod templates, with reserved prefix filtering - componentConfigs: applies per-component deployment overrides including revisionHistoryLimit and overrideEnv - overrideEnv: merges custom environment variables into component containers, with reserved prefix filtering (HOSTNAME, KUBERNETES_, EXTERNAL_SECRETS_) - UpdateComponentConfig condition: reports success/failure of component configuration application in ExternalSecretsConfig status New file: component_config.go — contains annotation application logic, component config mapping, and environment variable override handling. Modified files: - deployments.go — integrates annotation and componentConfig application into the deployment object builder pipeline - install_external_secrets.go — adds UpdateComponentConfig condition reporting after deployment reconciliation Enhancement Proposal: openshift/enhancements#1898 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f4d1360 commit 9034bd5

3 files changed

Lines changed: 236 additions & 0 deletions

File tree

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package external_secrets
18+
19+
import (
20+
"strings"
21+
22+
appsv1 "k8s.io/api/apps/v1"
23+
corev1 "k8s.io/api/core/v1"
24+
apimeta "k8s.io/apimachinery/pkg/api/meta"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
27+
operatorv1alpha1 "github.com/openshift/external-secrets-operator/api/v1alpha1"
28+
)
29+
30+
// reservedAnnotationPrefixes is the list of annotation key prefixes that are reserved
31+
// and cannot be overridden by user-specified annotations.
32+
var reservedAnnotationPrefixes = []string{
33+
"kubernetes.io/",
34+
"app.kubernetes.io/",
35+
"openshift.io/",
36+
"k8s.io/",
37+
}
38+
39+
// reservedEnvVarPrefixes is the list of environment variable name prefixes that are reserved
40+
// and cannot be overridden by user-specified environment variables.
41+
var reservedEnvVarPrefixes = []string{
42+
"HOSTNAME",
43+
"KUBERNETES_",
44+
"EXTERNAL_SECRETS_",
45+
}
46+
47+
// componentNameToDeploymentAssetName maps ComponentName values to their corresponding
48+
// deployment asset names used in the operator.
49+
var componentNameToDeploymentAssetName = map[operatorv1alpha1.ComponentName]string{
50+
operatorv1alpha1.CoreController: controllerDeploymentAssetName,
51+
operatorv1alpha1.Webhook: webhookDeploymentAssetName,
52+
operatorv1alpha1.CertController: certControllerDeploymentAssetName,
53+
operatorv1alpha1.BitwardenSDKServer: bitwardenDeploymentAssetName,
54+
}
55+
56+
// componentNameToContainerName maps ComponentName values to their primary container names
57+
// within the respective deployment pods.
58+
var componentNameToContainerName = map[operatorv1alpha1.ComponentName]string{
59+
operatorv1alpha1.CoreController: "external-secrets",
60+
operatorv1alpha1.Webhook: "webhook",
61+
operatorv1alpha1.CertController: "cert-controller",
62+
operatorv1alpha1.BitwardenSDKServer: "bitwarden-sdk-server",
63+
}
64+
65+
// applyAnnotationsToDeployment applies user-specified annotations to the Deployment and its
66+
// Pod template. Reserved annotation prefixes are filtered out and logged.
67+
func (r *Reconciler) applyAnnotationsToDeployment(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig) {
68+
if len(esc.Spec.ControllerConfig.Annotations) == 0 {
69+
return
70+
}
71+
72+
for _, annotation := range esc.Spec.ControllerConfig.Annotations {
73+
if isReservedAnnotation(annotation.Key) {
74+
r.log.V(1).Info("skip adding reserved annotation", "annotation", annotation.Key, "value", annotation.Value)
75+
continue
76+
}
77+
78+
// Apply annotation to the Deployment metadata
79+
deploymentAnnotations := deployment.GetAnnotations()
80+
if deploymentAnnotations == nil {
81+
deploymentAnnotations = make(map[string]string)
82+
}
83+
deploymentAnnotations[annotation.Key] = annotation.Value
84+
deployment.SetAnnotations(deploymentAnnotations)
85+
86+
// Apply annotation to the Pod template
87+
podAnnotations := deployment.Spec.Template.GetAnnotations()
88+
if podAnnotations == nil {
89+
podAnnotations = make(map[string]string)
90+
}
91+
podAnnotations[annotation.Key] = annotation.Value
92+
deployment.Spec.Template.SetAnnotations(podAnnotations)
93+
}
94+
}
95+
96+
// isReservedAnnotation checks if an annotation key starts with any reserved prefix.
97+
func isReservedAnnotation(key string) bool {
98+
for _, prefix := range reservedAnnotationPrefixes {
99+
if strings.HasPrefix(key, prefix) {
100+
return true
101+
}
102+
}
103+
return false
104+
}
105+
106+
// applyComponentConfig applies component-specific configuration overrides to the deployment.
107+
// This includes revisionHistoryLimit and overrideEnv settings.
108+
func (r *Reconciler) applyComponentConfig(deployment *appsv1.Deployment, esc *operatorv1alpha1.ExternalSecretsConfig, assetName string) {
109+
if len(esc.Spec.ControllerConfig.ComponentConfigs) == 0 {
110+
return
111+
}
112+
113+
// Find the component name for this deployment asset
114+
var componentName operatorv1alpha1.ComponentName
115+
found := false
116+
for name, asset := range componentNameToDeploymentAssetName {
117+
if asset == assetName {
118+
componentName = name
119+
found = true
120+
break
121+
}
122+
}
123+
if !found {
124+
return
125+
}
126+
127+
// Find the matching ComponentConfig entry
128+
for _, cc := range esc.Spec.ControllerConfig.ComponentConfigs {
129+
if cc.ComponentName != componentName {
130+
continue
131+
}
132+
133+
// Apply revisionHistoryLimit
134+
if cc.DeploymentConfigs.RevisionHistoryLimit != nil {
135+
deployment.Spec.RevisionHistoryLimit = cc.DeploymentConfigs.RevisionHistoryLimit
136+
r.log.V(1).Info("applied revisionHistoryLimit override",
137+
"component", componentName,
138+
"revisionHistoryLimit", *cc.DeploymentConfigs.RevisionHistoryLimit)
139+
}
140+
141+
// Apply overrideEnv
142+
if len(cc.OverrideEnv) > 0 {
143+
r.applyOverrideEnv(deployment, cc.OverrideEnv, componentName)
144+
}
145+
146+
break
147+
}
148+
}
149+
150+
// applyOverrideEnv applies custom environment variables to the primary container
151+
// of the deployment. Reserved environment variable prefixes are filtered out.
152+
func (r *Reconciler) applyOverrideEnv(deployment *appsv1.Deployment, overrideEnv []corev1.EnvVar, componentName operatorv1alpha1.ComponentName) {
153+
containerName, ok := componentNameToContainerName[componentName]
154+
if !ok {
155+
r.log.V(1).Info("no container name mapping found for component", "component", componentName)
156+
return
157+
}
158+
159+
for i := range deployment.Spec.Template.Spec.Containers {
160+
container := &deployment.Spec.Template.Spec.Containers[i]
161+
if container.Name != containerName {
162+
continue
163+
}
164+
165+
for _, envVar := range overrideEnv {
166+
if isReservedEnvVar(envVar.Name) {
167+
r.log.V(1).Info("skip adding reserved environment variable", "envVar", envVar.Name, "component", componentName)
168+
continue
169+
}
170+
171+
// Check if the env var already exists in the container spec
172+
envUpdated := false
173+
for j := range container.Env {
174+
if container.Env[j].Name == envVar.Name {
175+
container.Env[j] = envVar
176+
envUpdated = true
177+
r.log.V(2).Info("updated existing environment variable",
178+
"envVar", envVar.Name, "component", componentName)
179+
break
180+
}
181+
}
182+
183+
// If not found, append the new env var
184+
if !envUpdated {
185+
container.Env = append(container.Env, envVar)
186+
r.log.V(2).Info("added new environment variable",
187+
"envVar", envVar.Name, "component", componentName)
188+
}
189+
}
190+
191+
break
192+
}
193+
}
194+
195+
// isReservedEnvVar checks if an environment variable name starts with any reserved prefix.
196+
func isReservedEnvVar(name string) bool {
197+
for _, prefix := range reservedEnvVarPrefixes {
198+
if strings.HasPrefix(name, prefix) {
199+
return true
200+
}
201+
}
202+
return false
203+
}
204+
205+
// updateComponentConfigCondition updates the UpdateComponentConfig condition on the
206+
// ExternalSecretsConfig status based on the reconciliation result.
207+
func (r *Reconciler) updateComponentConfigCondition(esc *operatorv1alpha1.ExternalSecretsConfig, err error) {
208+
condition := metav1.Condition{
209+
Type: operatorv1alpha1.UpdateComponentConfig,
210+
ObservedGeneration: esc.GetGeneration(),
211+
}
212+
213+
if err != nil {
214+
condition.Status = metav1.ConditionFalse
215+
condition.Reason = operatorv1alpha1.ReasonFailed
216+
condition.Message = err.Error()
217+
} else {
218+
condition.Status = metav1.ConditionTrue
219+
condition.Reason = operatorv1alpha1.ReasonCompleted
220+
condition.Message = "component configuration overrides applied successfully"
221+
}
222+
223+
apimeta.SetStatusCondition(&esc.Status.Conditions, condition)
224+
}

pkg/controller/external_secrets/deployments.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,12 @@ func (r *Reconciler) getDeploymentObject(assetName string, esc *operatorv1alpha1
148148
return nil, fmt.Errorf("failed to update proxy configuration: %w", err)
149149
}
150150

151+
// Apply user-specified annotations to Deployment and Pod template
152+
r.applyAnnotationsToDeployment(deployment, esc)
153+
154+
// Apply component-specific configuration overrides (revisionHistoryLimit, overrideEnv)
155+
r.applyComponentConfig(deployment, esc, assetName)
156+
151157
return deployment, nil
152158
}
153159

pkg/controller/external_secrets/install_external_secrets.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,15 @@ func (r *Reconciler) reconcileExternalSecretsDeployment(esc *operatorv1alpha1.Ex
9494

9595
if err := r.createOrApplyDeployments(esc, resourceLabels, recon); err != nil {
9696
r.log.Error(err, "failed to reconcile deployment resource")
97+
r.updateComponentConfigCondition(esc, err)
9798
return err
9899
}
99100

101+
// Update condition to reflect successful component config application
102+
if len(esc.Spec.ControllerConfig.ComponentConfigs) > 0 || len(esc.Spec.ControllerConfig.Annotations) > 0 {
103+
r.updateComponentConfigCondition(esc, nil)
104+
}
105+
100106
if err := r.createOrApplyValidatingWebhookConfiguration(esc, resourceLabels, recon); err != nil {
101107
r.log.Error(err, "failed to reconcile validating webhook resource")
102108
return err

0 commit comments

Comments
 (0)