219 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			219 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // 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)
 | |
| }
 |