Skip to content

Commit 4e36464

Browse files
authored
adds MergePodTemplateSpecs that merges podTemplateSpecs (#2770)
merge podtemplatespecs Signed-off-by: Chris Burns <[email protected]>
1 parent e724609 commit 4e36464

File tree

2 files changed

+593
-0
lines changed

2 files changed

+593
-0
lines changed

cmd/thv-operator/pkg/registryapi/podtemplatespec.go

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,290 @@ func DefaultRegistryAPIPodTemplateSpec(labels map[string]string, configHash stri
270270
).Build()
271271
}
272272

273+
// MergePodTemplateSpecs merges a default PodTemplateSpec with a user-provided one.
274+
// User-provided values take precedence over defaults. This allows users to customize
275+
// infrastructure concerns while ensuring sensible defaults are applied where values
276+
// are not specified.
277+
//
278+
// The merge strategy starts with the user's PodTemplateSpec and fills in defaults
279+
// only where the user hasn't specified values. This means any field the user sets
280+
// (affinity, tolerations, nodeSelector, etc.) is automatically preserved.
281+
//
282+
// Merge behavior:
283+
// - Labels/Annotations: Merged, with defaults added for missing keys
284+
// - ServiceAccountName: Default only if user hasn't specified
285+
// - Containers: Merged by name - defaults fill in missing container fields
286+
// - Volumes: Merged by name - defaults added only if not present
287+
// - All other PodSpec fields: User values preserved as-is
288+
func MergePodTemplateSpecs(defaultPTS, userPTS *corev1.PodTemplateSpec) corev1.PodTemplateSpec {
289+
if userPTS == nil {
290+
if defaultPTS == nil {
291+
return corev1.PodTemplateSpec{}
292+
}
293+
return *defaultPTS.DeepCopy()
294+
}
295+
296+
if defaultPTS == nil {
297+
return *userPTS.DeepCopy()
298+
}
299+
300+
// Start with a deep copy of the user's spec - this preserves all user fields automatically
301+
result := userPTS.DeepCopy()
302+
303+
// Merge labels: add default labels that user hasn't specified
304+
result.Labels = mergeStringMapsDefaultsFirst(defaultPTS.Labels, result.Labels)
305+
306+
// Merge annotations: add default annotations that user hasn't specified
307+
result.Annotations = mergeStringMapsDefaultsFirst(defaultPTS.Annotations, result.Annotations)
308+
309+
// Set service account only if user hasn't specified one
310+
if result.Spec.ServiceAccountName == "" {
311+
result.Spec.ServiceAccountName = defaultPTS.Spec.ServiceAccountName
312+
}
313+
314+
// Merge containers: user containers take precedence, defaults fill gaps
315+
result.Spec.Containers = mergeContainersUserFirst(defaultPTS.Spec.Containers, result.Spec.Containers)
316+
317+
// Merge init containers
318+
result.Spec.InitContainers = mergeContainersUserFirst(defaultPTS.Spec.InitContainers, result.Spec.InitContainers)
319+
320+
// Merge volumes: add default volumes that user hasn't specified
321+
result.Spec.Volumes = mergeVolumesUserFirst(defaultPTS.Spec.Volumes, result.Spec.Volumes)
322+
323+
return *result
324+
}
325+
326+
// mergeContainersUserFirst merges containers where user containers take precedence.
327+
// User containers are preserved, and default container fields fill in gaps.
328+
func mergeContainersUserFirst(defaults, user []corev1.Container) []corev1.Container {
329+
if len(user) == 0 {
330+
return defaults
331+
}
332+
if len(defaults) == 0 {
333+
return user
334+
}
335+
336+
// Create a map of default containers by name
337+
defaultMap := make(map[string]corev1.Container)
338+
for _, c := range defaults {
339+
defaultMap[c.Name] = c
340+
}
341+
342+
// Start with user containers, filling in defaults where needed
343+
result := make([]corev1.Container, 0, len(user)+len(defaults))
344+
merged := make(map[string]bool)
345+
346+
for _, userContainer := range user {
347+
if defaultContainer, exists := defaultMap[userContainer.Name]; exists {
348+
// Merge: user values take precedence, defaults fill gaps
349+
result = append(result, mergeContainer(defaultContainer, userContainer))
350+
merged[userContainer.Name] = true
351+
} else {
352+
// User container with no default - keep as-is
353+
result = append(result, userContainer)
354+
}
355+
}
356+
357+
// Add default containers that user didn't specify
358+
for _, defaultContainer := range defaults {
359+
if !merged[defaultContainer.Name] {
360+
result = append(result, defaultContainer)
361+
}
362+
}
363+
364+
return result
365+
}
366+
367+
// mergeContainer merges a default container with a user container.
368+
// User values take precedence; defaults fill in where user hasn't specified.
369+
func mergeContainer(defaultContainer, userContainer corev1.Container) corev1.Container {
370+
// Start with user container - preserves all user-specified fields
371+
result := userContainer
372+
373+
// Fill in defaults only where user hasn't specified
374+
if result.Image == "" {
375+
result.Image = defaultContainer.Image
376+
}
377+
if len(result.Command) == 0 {
378+
result.Command = defaultContainer.Command
379+
}
380+
if len(result.Args) == 0 {
381+
result.Args = defaultContainer.Args
382+
}
383+
if result.WorkingDir == "" {
384+
result.WorkingDir = defaultContainer.WorkingDir
385+
}
386+
if isResourcesEmpty(result.Resources) {
387+
result.Resources = defaultContainer.Resources
388+
}
389+
if result.LivenessProbe == nil {
390+
result.LivenessProbe = defaultContainer.LivenessProbe
391+
}
392+
if result.ReadinessProbe == nil {
393+
result.ReadinessProbe = defaultContainer.ReadinessProbe
394+
}
395+
if result.StartupProbe == nil {
396+
result.StartupProbe = defaultContainer.StartupProbe
397+
}
398+
if result.SecurityContext == nil {
399+
result.SecurityContext = defaultContainer.SecurityContext
400+
}
401+
if result.ImagePullPolicy == "" {
402+
result.ImagePullPolicy = defaultContainer.ImagePullPolicy
403+
}
404+
405+
// Merge slices: add defaults that user hasn't specified
406+
result.Ports = mergePortsUserFirst(defaultContainer.Ports, result.Ports)
407+
result.Env = mergeEnvVarsUserFirst(defaultContainer.Env, result.Env)
408+
result.VolumeMounts = mergeVolumeMountsUserFirst(defaultContainer.VolumeMounts, result.VolumeMounts)
409+
410+
return result
411+
}
412+
413+
// mergeVolumesUserFirst merges volumes where user volumes take precedence.
414+
func mergeVolumesUserFirst(defaults, user []corev1.Volume) []corev1.Volume {
415+
if len(user) == 0 {
416+
return defaults
417+
}
418+
if len(defaults) == 0 {
419+
return user
420+
}
421+
422+
// Create a map of user volumes by name
423+
userMap := make(map[string]bool)
424+
for _, v := range user {
425+
userMap[v.Name] = true
426+
}
427+
428+
// Start with user volumes
429+
result := make([]corev1.Volume, 0, len(user)+len(defaults))
430+
result = append(result, user...)
431+
432+
// Add default volumes that user hasn't specified
433+
for _, defaultVolume := range defaults {
434+
if !userMap[defaultVolume.Name] {
435+
result = append(result, defaultVolume)
436+
}
437+
}
438+
439+
return result
440+
}
441+
442+
// mergePortsUserFirst merges ports where user ports take precedence.
443+
func mergePortsUserFirst(defaults, user []corev1.ContainerPort) []corev1.ContainerPort {
444+
if len(user) == 0 {
445+
return defaults
446+
}
447+
if len(defaults) == 0 {
448+
return user
449+
}
450+
451+
// Track user ports by name and port number
452+
userByName := make(map[string]bool)
453+
userByPort := make(map[int32]bool)
454+
for _, p := range user {
455+
if p.Name != "" {
456+
userByName[p.Name] = true
457+
}
458+
userByPort[p.ContainerPort] = true
459+
}
460+
461+
// Start with user ports
462+
result := make([]corev1.ContainerPort, 0, len(user)+len(defaults))
463+
result = append(result, user...)
464+
465+
// Add default ports that user hasn't specified
466+
for _, defaultPort := range defaults {
467+
nameConflict := defaultPort.Name != "" && userByName[defaultPort.Name]
468+
portConflict := userByPort[defaultPort.ContainerPort]
469+
if !nameConflict && !portConflict {
470+
result = append(result, defaultPort)
471+
}
472+
}
473+
474+
return result
475+
}
476+
477+
// mergeEnvVarsUserFirst merges env vars where user env vars take precedence.
478+
func mergeEnvVarsUserFirst(defaults, user []corev1.EnvVar) []corev1.EnvVar {
479+
if len(user) == 0 {
480+
return defaults
481+
}
482+
if len(defaults) == 0 {
483+
return user
484+
}
485+
486+
// Create a map of user env vars by name
487+
userMap := make(map[string]bool)
488+
for _, e := range user {
489+
userMap[e.Name] = true
490+
}
491+
492+
// Start with user env vars
493+
result := make([]corev1.EnvVar, 0, len(user)+len(defaults))
494+
result = append(result, user...)
495+
496+
// Add default env vars that user hasn't specified
497+
for _, defaultEnv := range defaults {
498+
if !userMap[defaultEnv.Name] {
499+
result = append(result, defaultEnv)
500+
}
501+
}
502+
503+
return result
504+
}
505+
506+
// mergeVolumeMountsUserFirst merges volume mounts where user mounts take precedence.
507+
func mergeVolumeMountsUserFirst(defaults, user []corev1.VolumeMount) []corev1.VolumeMount {
508+
if len(user) == 0 {
509+
return defaults
510+
}
511+
if len(defaults) == 0 {
512+
return user
513+
}
514+
515+
// Create a map of user volume mounts by name
516+
userMap := make(map[string]bool)
517+
for _, m := range user {
518+
userMap[m.Name] = true
519+
}
520+
521+
// Start with user mounts
522+
result := make([]corev1.VolumeMount, 0, len(user)+len(defaults))
523+
result = append(result, user...)
524+
525+
// Add default mounts that user hasn't specified
526+
for _, defaultMount := range defaults {
527+
if !userMap[defaultMount.Name] {
528+
result = append(result, defaultMount)
529+
}
530+
}
531+
532+
return result
533+
}
534+
535+
// mergeStringMapsDefaultsFirst merges string maps where user values override defaults.
536+
// Returns a map with all default keys, plus any additional user keys, with user values taking precedence.
537+
func mergeStringMapsDefaultsFirst(defaults, user map[string]string) map[string]string {
538+
if len(defaults) == 0 && len(user) == 0 {
539+
return nil
540+
}
541+
542+
result := make(map[string]string)
543+
for k, v := range defaults {
544+
result[k] = v
545+
}
546+
for k, v := range user {
547+
result[k] = v // User values override defaults
548+
}
549+
return result
550+
}
551+
552+
// isResourcesEmpty checks if ResourceRequirements are empty.
553+
func isResourcesEmpty(resources corev1.ResourceRequirements) bool {
554+
return len(resources.Requests) == 0 && len(resources.Limits) == 0
555+
}
556+
273557
// findContainerByName finds a container by name in a slice of containers.
274558
// Returns a pointer to the container if found, nil otherwise.
275559
func findContainerByName(containers []corev1.Container, name string) *corev1.Container {

0 commit comments

Comments
 (0)