/* * SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package controller import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" nvidiacomv1beta1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1beta1" ) // protectedLabelKeys are controller-managed label keys that user overrides // must not overwrite. The controller relies on these for ownership tracking // and watch predicates. var protectedLabelKeys = map[string]struct{}{ nvidiacomv1beta1.LabelApp: {}, nvidiacomv1beta1.LabelDGDR: {}, nvidiacomv1beta1.LabelDGDRName: {}, nvidiacomv1beta1.LabelDGDRNamespace: {}, nvidiacomv1beta1.LabelManagedBy: {}, } // applyProfilingJobOverrides merges user-provided overrides from // spec.overrides.profilingJob into the controller-generated Job. // Uses a deterministic allowlist: only explicitly handled fields are merged. func applyProfilingJobOverrides(job *batchv1.Job, overrides *batchv1.JobSpec) { if overrides == nil { return } applyJobSpecOverrides(&job.Spec, overrides) applyPodTemplateOverrides(&job.Spec.Template, &overrides.Template) } // applyJobSpecOverrides merges JobSpec-level scalar fields. func applyJobSpecOverrides(spec *batchv1.JobSpec, overrides *batchv1.JobSpec) { if overrides.BackoffLimit != nil { spec.BackoffLimit = overrides.BackoffLimit } if overrides.ActiveDeadlineSeconds != nil { spec.ActiveDeadlineSeconds = overrides.ActiveDeadlineSeconds } if overrides.TTLSecondsAfterFinished != nil { spec.TTLSecondsAfterFinished = overrides.TTLSecondsAfterFinished } if overrides.Completions != nil { spec.Completions = overrides.Completions } if overrides.Parallelism != nil { spec.Parallelism = overrides.Parallelism } if overrides.Suspend != nil { spec.Suspend = overrides.Suspend } } // applyPodTemplateOverrides merges PodTemplateSpec metadata and PodSpec fields. func applyPodTemplateOverrides(tmpl *corev1.PodTemplateSpec, overrides *corev1.PodTemplateSpec) { mergeLabels(tmpl, overrides.Labels) mergeAnnotations(tmpl, overrides.Annotations) applyPodSpecOverrides(&tmpl.Spec, &overrides.Spec) } // mergeLabels adds user labels to the template, skipping protected controller keys. func mergeLabels(tmpl *corev1.PodTemplateSpec, userLabels map[string]string) { if len(userLabels) == 0 { return } if tmpl.Labels == nil { tmpl.Labels = make(map[string]string, len(userLabels)) } for k, v := range userLabels { if _, protected := protectedLabelKeys[k]; protected { continue } tmpl.Labels[k] = v } } // mergeAnnotations adds user annotations to the template. func mergeAnnotations(tmpl *corev1.PodTemplateSpec, userAnnotations map[string]string) { if len(userAnnotations) == 0 { return } if tmpl.Annotations == nil { tmpl.Annotations = make(map[string]string, len(userAnnotations)) } for k, v := range userAnnotations { tmpl.Annotations[k] = v } } // mergeImagePullSecrets combines base and override secrets, deduplicating by name. // Override secrets that already exist in base are skipped (base wins on conflict). func mergeImagePullSecrets(base, overrides []corev1.LocalObjectReference) []corev1.LocalObjectReference { if len(overrides) == 0 { return base } seen := make(map[string]bool, len(base)) result := make([]corev1.LocalObjectReference, len(base)) copy(result, base) for _, s := range base { seen[s.Name] = true } for _, s := range overrides { if !seen[s.Name] { result = append(result, s) seen[s.Name] = true } } return result } // applyPodSpecOverrides merges PodSpec-level fields and the first container. func applyPodSpecOverrides(spec *corev1.PodSpec, overrides *corev1.PodSpec) { if len(overrides.Tolerations) > 0 { spec.Tolerations = overrides.Tolerations } if len(overrides.NodeSelector) > 0 { spec.NodeSelector = overrides.NodeSelector } if overrides.Affinity != nil { spec.Affinity = overrides.Affinity } if overrides.PriorityClassName != "" { spec.PriorityClassName = overrides.PriorityClassName } if len(overrides.ImagePullSecrets) > 0 { spec.ImagePullSecrets = mergeImagePullSecrets(spec.ImagePullSecrets, overrides.ImagePullSecrets) } if overrides.ServiceAccountName != "" { spec.ServiceAccountName = overrides.ServiceAccountName } if overrides.RuntimeClassName != nil { spec.RuntimeClassName = overrides.RuntimeClassName } if overrides.DNSPolicy != "" { spec.DNSPolicy = overrides.DNSPolicy } if overrides.DNSConfig != nil { spec.DNSConfig = overrides.DNSConfig } if overrides.SecurityContext != nil { if spec.SecurityContext == nil { spec.SecurityContext = &corev1.PodSecurityContext{} } mergePodSecurityContext(spec.SecurityContext, overrides.SecurityContext) } if overrides.TerminationGracePeriodSeconds != nil { spec.TerminationGracePeriodSeconds = overrides.TerminationGracePeriodSeconds } if len(overrides.TopologySpreadConstraints) > 0 { spec.TopologySpreadConstraints = overrides.TopologySpreadConstraints } if overrides.AutomountServiceAccountToken != nil { spec.AutomountServiceAccountToken = overrides.AutomountServiceAccountToken } spec.Volumes = mergeNamedSlice(spec.Volumes, overrides.Volumes, func(v corev1.Volume) string { return v.Name }) spec.InitContainers = mergeNamedSlice(spec.InitContainers, overrides.InitContainers, func(c corev1.Container) string { return c.Name }) if len(overrides.Containers) > 0 && len(spec.Containers) > 0 { applyContainerOverrides(&spec.Containers[0], &overrides.Containers[0]) } } // applyContainerOverrides merges fields from the user's first container override // into the controller-generated profiler container. func applyContainerOverrides(container *corev1.Container, overrides *corev1.Container) { if overrides.Image != "" { container.Image = overrides.Image } if len(overrides.Resources.Requests) > 0 || len(overrides.Resources.Limits) > 0 || len(overrides.Resources.Claims) > 0 { container.Resources = overrides.Resources } if overrides.SecurityContext != nil { container.SecurityContext = overrides.SecurityContext } container.Env = mergeNamedSlice(container.Env, overrides.Env, func(e corev1.EnvVar) string { return e.Name }) container.VolumeMounts = mergeNamedSlice(container.VolumeMounts, overrides.VolumeMounts, func(vm corev1.VolumeMount) string { return vm.Name }) if len(overrides.EnvFrom) > 0 { container.EnvFrom = append(container.EnvFrom, overrides.EnvFrom...) } } // mergePodSecurityContext copies non-nil fields from src into dst, preserving // any controller-enforced defaults already present on dst. func mergePodSecurityContext(dst, src *corev1.PodSecurityContext) { if src.RunAsNonRoot != nil { dst.RunAsNonRoot = src.RunAsNonRoot } if src.RunAsUser != nil { dst.RunAsUser = src.RunAsUser } if src.RunAsGroup != nil { dst.RunAsGroup = src.RunAsGroup } if src.FSGroup != nil { dst.FSGroup = src.FSGroup } if src.SupplementalGroups != nil { dst.SupplementalGroups = src.SupplementalGroups } if src.Sysctls != nil { dst.Sysctls = src.Sysctls } if src.FSGroupChangePolicy != nil { dst.FSGroupChangePolicy = src.FSGroupChangePolicy } if src.SeccompProfile != nil { dst.SeccompProfile = src.SeccompProfile } if src.AppArmorProfile != nil { dst.AppArmorProfile = src.AppArmorProfile } if src.SELinuxOptions != nil { dst.SELinuxOptions = src.SELinuxOptions } if src.WindowsOptions != nil { dst.WindowsOptions = src.WindowsOptions } } // mergeNamedSlice merges two slices of named items. Items from overrides with // the same name as a base item replace the base entry; new names are appended. // Preserves ordering of base items. func mergeNamedSlice[T any](base, overrides []T, nameFunc func(T) string) []T { if len(overrides) == 0 { return base } seen := make(map[string]int, len(base)) result := make([]T, len(base)) copy(result, base) for i, item := range result { seen[nameFunc(item)] = i } for _, item := range overrides { if idx, exists := seen[nameFunc(item)]; exists { result[idx] = item } else { result = append(result, item) seen[nameFunc(item)] = len(result) - 1 } } return result }