Skip to content

Commit ecff41e

Browse files
authored
Merge pull request #678 from lmiccini/drop_clusterop
Add helpers for StatefulSet container merging, raw ConfigMaps, and preserved Secrets
2 parents 0e00588 + 11ca8d5 commit ecff41e

7 files changed

Lines changed: 803 additions & 24 deletions

File tree

modules/common/configmap/configmap.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,50 @@ func EnsureConfigMaps(
199199
return nil
200200
}
201201

202+
// CreateOrPatchRawConfigMap creates or patches a ConfigMap from raw data
203+
// (map[string]string) without template rendering. Returns the config hash and
204+
// operation result. This is a simpler alternative to EnsureConfigMaps when the
205+
// caller already has the data and doesn't need template machinery.
206+
// When skipSetOwner is true, no controller reference is set on the ConfigMap.
207+
func CreateOrPatchRawConfigMap(
208+
ctx context.Context,
209+
h *helper.Helper,
210+
obj client.Object,
211+
cm *corev1.ConfigMap,
212+
skipSetOwner bool,
213+
) (string, controllerutil.OperationResult, error) {
214+
configMap := &corev1.ConfigMap{
215+
ObjectMeta: metav1.ObjectMeta{
216+
Name: cm.Name,
217+
Namespace: cm.Namespace,
218+
},
219+
}
220+
221+
op, err := controllerutil.CreateOrPatch(ctx, h.GetClient(), configMap, func() error {
222+
configMap.Annotations = util.MergeStringMaps(configMap.Annotations, cm.Annotations)
223+
configMap.Labels = util.MergeStringMaps(configMap.Labels, cm.Labels)
224+
configMap.Data = cm.Data
225+
226+
if !skipSetOwner {
227+
err := controllerutil.SetControllerReference(obj, configMap, h.GetScheme())
228+
if err != nil {
229+
return err
230+
}
231+
}
232+
return nil
233+
})
234+
if err != nil {
235+
return "", op, fmt.Errorf("error create/updating configmap: %w", err)
236+
}
237+
238+
configMapHash, err := Hash(configMap)
239+
if err != nil {
240+
return "", op, fmt.Errorf("error calculating configuration hash: %w", err)
241+
}
242+
243+
return configMapHash, op, nil
244+
}
245+
202246
// GetConfigMaps - get all configmaps required, verify they exist and add the hash to env and status
203247
func GetConfigMaps(
204248
ctx context.Context,

modules/common/secret/secret.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,57 @@ func CreateOrPatchSecret(
156156
return secretHash, op, err
157157
}
158158

159+
// CreateOrPatchSecretPreserve creates a secret on first creation and preserves
160+
// existing Data keys on subsequent reconciles. This is useful for generated
161+
// credentials (passwords, cookies) that should only be set once and not
162+
// rotated on every reconcile. Labels and annotations are always updated.
163+
// When skipSetOwner is true, no controller reference is set on the Secret.
164+
func CreateOrPatchSecretPreserve(
165+
ctx context.Context,
166+
h *helper.Helper,
167+
obj client.Object,
168+
secret *corev1.Secret,
169+
skipSetOwner bool,
170+
) (string, controllerutil.OperationResult, error) {
171+
s := &corev1.Secret{
172+
ObjectMeta: metav1.ObjectMeta{
173+
Name: secret.Name,
174+
Namespace: secret.Namespace,
175+
},
176+
}
177+
178+
op, err := controllerutil.CreateOrPatch(ctx, h.GetClient(), s, func() error {
179+
s.Annotations = util.MergeStringMaps(s.Annotations, secret.Annotations)
180+
s.Labels = util.MergeStringMaps(s.Labels, secret.Labels)
181+
182+
// Only set data on initial creation (when the object has no data yet)
183+
if len(s.Data) == 0 {
184+
s.Immutable = secret.Immutable
185+
s.Type = secret.Type
186+
s.Data = secret.Data
187+
s.StringData = secret.StringData
188+
}
189+
190+
if !skipSetOwner {
191+
err := controllerutil.SetControllerReference(obj, s, h.GetScheme())
192+
if err != nil {
193+
return err
194+
}
195+
}
196+
return nil
197+
})
198+
if err != nil {
199+
return "", op, fmt.Errorf("error create/updating secret: %w", err)
200+
}
201+
202+
secretHash, err := Hash(s)
203+
if err != nil {
204+
return "", "", fmt.Errorf("error calculating configuration hash: %w", err)
205+
}
206+
207+
return secretHash, op, err
208+
}
209+
159210
// createOrUpdateSecret - create or update existing secrte if it already exists
160211
// finally return configuration hash
161212
func createOrUpdateSecret(
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
Copyright 2026 Red Hat
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 statefulset
18+
19+
import (
20+
corev1 "k8s.io/api/core/v1"
21+
)
22+
23+
// MergeContainersByName merges desired container specs into existing containers
24+
// matched by name. It starts from the desired container and preserves only the
25+
// server-defaulted fields (TerminationMessagePath, TerminationMessagePolicy,
26+
// ImagePullPolicy) from the existing container. All other fields come from the
27+
// desired spec, which ensures that new fields added in future Kubernetes
28+
// versions are not silently dropped.
29+
//
30+
// When container counts differ or a desired container name is not found in
31+
// existing, the existing slice is replaced with the desired containers.
32+
func MergeContainersByName(existing *[]corev1.Container, desired []corev1.Container) {
33+
if len(*existing) != len(desired) {
34+
*existing = desired
35+
return
36+
}
37+
38+
existingByName := make(map[string]int, len(*existing))
39+
for i := range *existing {
40+
existingByName[(*existing)[i].Name] = i
41+
}
42+
43+
for _, d := range desired {
44+
idx, ok := existingByName[d.Name]
45+
if !ok {
46+
*existing = desired
47+
return
48+
}
49+
// Preserve server-defaulted fields from the existing container
50+
// only when the desired spec doesn't explicitly set them.
51+
if d.ImagePullPolicy == "" {
52+
d.ImagePullPolicy = (*existing)[idx].ImagePullPolicy
53+
}
54+
if d.TerminationMessagePath == "" {
55+
d.TerminationMessagePath = (*existing)[idx].TerminationMessagePath
56+
}
57+
if d.TerminationMessagePolicy == "" {
58+
d.TerminationMessagePolicy = (*existing)[idx].TerminationMessagePolicy
59+
}
60+
(*existing)[idx] = d
61+
}
62+
}

0 commit comments

Comments
 (0)