alpha ver. of deployments resource controller
This commit is contained in:
209
internal/controller/deploymentdefaults_controller.go
Normal file
209
internal/controller/deploymentdefaults_controller.go
Normal file
@@ -0,0 +1,209 @@
|
||||
// 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 {
|
||||
errMsg := fmt.Sprintf("Invalid resource quantity format in NodeTainterConfig %s: %s", config.Name, strings.Join(parseErrors, "; "))
|
||||
log.Error(fmt.Errorf(errMsg), "Default resource parsing failed")
|
||||
r.Recorder.Eventf(&deployment, corev1.EventTypeWarning, "ConfigError", "Invalid defaults in config %s: %s", config.Name, strings.Join(parseErrors, "; "))
|
||||
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).
|
||||
For(&appsv1.Deployment{}).
|
||||
Watches(
|
||||
&configv1alpha1.NodeTainterConfig{},
|
||||
handler.EnqueueRequestsFromMapFunc(r.mapConfigToDeployments),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
Reference in New Issue
Block a user