// internal/controller/deploymentdefaults_controller.go package controller import ( "context" "fmt" "strings" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" configv1alpha1 "git.vendetti.ru/andy/operator/api/v1alpha1" ) // DeploymentDefaultsReconciler reconciles Deployment objects to apply default resources. type DeploymentDefaultsReconciler struct { client.Client Scheme *runtime.Scheme Recorder record.EventRecorder } // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups=operator.andy.vendetti.ru,resources=nodetainterconfigs,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch func (r *DeploymentDefaultsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx).WithValues("deployment", req.NamespacedName) var deployment appsv1.Deployment if err := r.Get(ctx, req.NamespacedName, &deployment); err != nil { if errors.IsNotFound(err) { log.Info("Deployment not found. Ignoring.") return ctrl.Result{}, nil } log.Error(err, "Failed to get Deployment") return ctrl.Result{}, err // Requeue on error } var config configv1alpha1.NodeTainterConfig configKey := types.NamespacedName{Name: GlobalTaintConfigName} if err := r.Get(ctx, configKey, &config); err != nil { if errors.IsNotFound(err) { log.Info("Global NodeTainterConfig not found, skipping resource defaulting", "configName", GlobalTaintConfigName) return ctrl.Result{}, nil } log.Error(err, "Failed to get NodeTainterConfig for defaults", "configName", GlobalTaintConfigName) r.Recorder.Eventf(&deployment, corev1.EventTypeWarning, "ConfigError", "Failed to get config %s: %v", GlobalTaintConfigName, err) return ctrl.Result{}, err } if config.Spec.ResourceDefaults == nil { log.V(1).Info("Resource defaulting is disabled in NodeTainterConfig.") return ctrl.Result{}, nil } optOutKey := strings.TrimSpace(config.Spec.OptOutLabelKey) if optOutKey != "" { labels := deployment.GetLabels() if _, exists := labels[optOutKey]; exists { log.Info("Deployment has opt-out label, skipping resource defaulting", "labelKey", optOutKey) r.Recorder.Eventf(&deployment, corev1.EventTypeNormal, "OptedOut", "Skipping resource defaulting due to label %s", optOutKey) return ctrl.Result{}, nil } } defaults := config.Spec.ResourceDefaults defaultCPUReq, errCPUReq := parseQuantity(defaults.CPURequest) defaultMemReq, errMemReq := parseQuantity(defaults.MemoryRequest) defaultCPULim, errCPULim := parseQuantity(defaults.CPULimit) defaultMemLim, errMemLim := parseQuantity(defaults.MemoryLimit) var parseErrors []string if errCPUReq != nil { parseErrors = append(parseErrors, fmt.Sprintf("CPURequest: %v", errCPUReq)) } if errMemReq != nil { parseErrors = append(parseErrors, fmt.Sprintf("MemoryRequest: %v", errMemReq)) } if errCPULim != nil { parseErrors = append(parseErrors, fmt.Sprintf("CPULimit: %v", errCPULim)) } if errMemLim != nil { parseErrors = append(parseErrors, fmt.Sprintf("MemoryLimit: %v", errMemLim)) } if len(parseErrors) > 0 { parsingError := fmt.Errorf("invalid resource quantity format in NodeTainterConfig %s: %s", config.Name, strings.Join(parseErrors, "; ")) log.Error(parsingError, "Default resource parsing failed", "configName", config.Name, "parsingErrors", parseErrors) r.Recorder.Eventf(&deployment, corev1.EventTypeWarning, "ConfigError", parsingError.Error()) return ctrl.Result{}, nil } deploymentCopy := deployment.DeepCopy() mutated := false for i, container := range deploymentCopy.Spec.Template.Spec.Containers { containerName := container.Name log := log.WithValues("container", containerName) if deploymentCopy.Spec.Template.Spec.Containers[i].Resources.Requests == nil { deploymentCopy.Spec.Template.Spec.Containers[i].Resources.Requests = corev1.ResourceList{} } if deploymentCopy.Spec.Template.Spec.Containers[i].Resources.Limits == nil { deploymentCopy.Spec.Template.Spec.Containers[i].Resources.Limits = corev1.ResourceList{} } requests := deploymentCopy.Spec.Template.Spec.Containers[i].Resources.Requests limits := deploymentCopy.Spec.Template.Spec.Containers[i].Resources.Limits if _, exists := requests[corev1.ResourceCPU]; !exists && defaultCPUReq != nil { requests[corev1.ResourceCPU] = *defaultCPUReq log.V(1).Info("Applied default CPU request", "value", defaultCPUReq.String()) mutated = true } if _, exists := requests[corev1.ResourceMemory]; !exists && defaultMemReq != nil { requests[corev1.ResourceMemory] = *defaultMemReq log.V(1).Info("Applied default Memory request", "value", defaultMemReq.String()) mutated = true } if _, exists := limits[corev1.ResourceCPU]; !exists && defaultCPULim != nil { limits[corev1.ResourceCPU] = *defaultCPULim log.V(1).Info("Applied default CPU limit", "value", defaultCPULim.String()) mutated = true } if _, exists := limits[corev1.ResourceMemory]; !exists && defaultMemLim != nil { limits[corev1.ResourceMemory] = *defaultMemLim log.V(1).Info("Applied default Memory limit", "value", defaultMemLim.String()) mutated = true } } if mutated { log.Info("Applying default resource requests/limits to Deployment") if err := r.Patch(ctx, deploymentCopy, client.MergeFrom(&deployment)); err != nil { log.Error(err, "Failed to patch Deployment with default resources") r.Recorder.Eventf(&deployment, corev1.EventTypeWarning, "UpdateFailed", "Failed to apply default resources: %v", err) return ctrl.Result{}, err } log.Info("Successfully applied default resources") r.Recorder.Eventf(&deployment, corev1.EventTypeNormal, "DefaultsApplied", "Default resource requests/limits applied") } else { log.V(1).Info("Deployment already has necessary resource requests/limits or no defaults configured.") } return ctrl.Result{}, nil } func parseQuantity(s string) (*resource.Quantity, error) { s = strings.TrimSpace(s) if s == "" { return nil, nil } q, err := resource.ParseQuantity(s) if err != nil { return nil, fmt.Errorf("invalid quantity format '%s': %w", s, err) } return &q, nil } // Map function for NodeTainterConfig: Trigger reconcile for ALL Deployments when the specific config changes func (r *DeploymentDefaultsReconciler) mapConfigToDeployments(ctx context.Context, obj client.Object) []reconcile.Request { config, ok := obj.(*configv1alpha1.NodeTainterConfig) log := log.FromContext(ctx) if !ok || config.Name != GlobalTaintConfigName { return nil } log.Info("Global NodeTainterConfig changed, queuing reconciliation for all deployments potentially affected by resource defaults", "configName", config.Name) var deploymentList appsv1.DeploymentList if err := r.List(ctx, &deploymentList, client.InNamespace("")); err != nil { log.Error(err, "Failed to list deployments for config change") return nil } requests := make([]reconcile.Request, 0, len(deploymentList.Items)) optOutKey := strings.TrimSpace(config.Spec.OptOutLabelKey) for _, deployment := range deploymentList.Items { if optOutKey != "" { labels := deployment.GetLabels() if _, exists := labels[optOutKey]; exists { continue } } requests = append(requests, reconcile.Request{ NamespacedName: types.NamespacedName{ Name: deployment.Name, Namespace: deployment.Namespace, }, }) } log.Info("Queued deployment reconcile requests", "count", len(requests)) return requests } func (r *DeploymentDefaultsReconciler) SetupWithManager(mgr ctrl.Manager) error { r.Recorder = mgr.GetEventRecorderFor("deploymentdefaults-controller") return ctrl.NewControllerManagedBy(mgr). Named("deploymentdefaults"). For(&appsv1.Deployment{}). Watches( &configv1alpha1.NodeTainterConfig{}, handler.EnqueueRequestsFromMapFunc(r.mapConfigToDeployments), ). Complete(r) }