internal/controller/deployment_to_pdb_controller.go (139 lines of code) (raw):
package controllers
import (
"context"
"fmt"
"strconv"
myappsv1 "github.com/azure/eviction-autoscaler/api/v1"
v1 "k8s.io/api/apps/v1"
policyv1 "k8s.io/api/policy/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"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/event"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// DeploymentToPDBReconciler reconciles a Deployment object and ensures an associated PDB is created and deleted
type DeploymentToPDBReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;update;watch
// Reconcile watches for Deployment changes (created, updated, deleted) and creates or deletes the associated PDB.
// creates pdb with minAvailable to be same as replicas for any deployment
func (r *DeploymentToPDBReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
// Fetch the Deployment instance
var deployment v1.Deployment
if err := r.Get(ctx, req.NamespacedName, &deployment); err != nil {
return reconcile.Result{}, err
}
log.Info("Found: ", "deployment", deployment.Name, "namespace", deployment.Namespace)
// If the Deployment is created, ensure a PDB exists
return r.handleDeploymentReconcile(ctx, &deployment)
}
// handleDeploymentReconcile creates a PodDisruptionBudget when a Deployment is created or updated.
func (r *DeploymentToPDBReconciler) handleDeploymentReconcile(ctx context.Context, deployment *v1.Deployment) (reconcile.Result, error) {
log := log.FromContext(ctx)
// Check if PDB already exists for this Deployment
var pdbList policyv1.PodDisruptionBudgetList
err := r.List(ctx, &pdbList, &client.ListOptions{
Namespace: deployment.Namespace,
})
if err != nil {
return ctrl.Result{}, err
}
for _, pdb := range pdbList.Items {
selector, err := metav1.LabelSelectorAsSelector(pdb.Spec.Selector)
if err != nil {
return ctrl.Result{}, fmt.Errorf("error converting label selector: %w", err)
}
if selector.Matches(labels.Set(deployment.Spec.Template.Labels)) {
// PDB already exists, nothing to do
log.Info("PodDisruptionBudget already exists", "namespace", pdb.Namespace, "name", pdb.Name)
EvictionAutoScaler := &myappsv1.EvictionAutoScaler{}
e := r.Get(ctx, types.NamespacedName{Name: pdb.Name, Namespace: pdb.Namespace}, EvictionAutoScaler)
if e == nil {
// if pdb exists get EvictionAutoScaler --> compare targetGeneration field for deployment if both not same deployment was not changed by pdb watcher
// update pdb minReplicas to current deployment replicas
return r.updateMinAvailableAsNecessary(ctx, deployment, EvictionAutoScaler, pdb)
}
return reconcile.Result{}, nil
}
}
//variables
controller := true
blockOwnerDeletion := true
// Create a new PDB for the Deployment
pdb := &policyv1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Name: r.generatePDBName(deployment.Name),
Namespace: deployment.Namespace,
Annotations: map[string]string{
"createdBy": "DeploymentToPDBController",
"target": deployment.Name,
},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: deployment.Name,
UID: deployment.UID,
Controller: &controller, // Mark as managed by this controller
BlockOwnerDeletion: &blockOwnerDeletion, // Prevent deletion of the PDB until the deployment is deleted
},
},
},
Spec: policyv1.PodDisruptionBudgetSpec{
MinAvailable: &intstr.IntOrString{IntVal: *deployment.Spec.Replicas},
Selector: &metav1.LabelSelector{MatchLabels: deployment.Spec.Selector.MatchLabels},
},
}
if err := r.Create(ctx, pdb); err != nil {
return reconcile.Result{}, err
}
log.Info("Created PodDisruptionBudget", "namespace", pdb.Namespace, "name", pdb.Name)
return reconcile.Result{}, nil
}
func (r *DeploymentToPDBReconciler) updateMinAvailableAsNecessary(ctx context.Context,
deployment *v1.Deployment, EvictionAutoScaler *myappsv1.EvictionAutoScaler, pdb policyv1.PodDisruptionBudget) (reconcile.Result, error) {
logger := log.FromContext(ctx)
logger.Info("deployment replicas got updated", " EvictionAutoScaler.Status.TargetGeneration", EvictionAutoScaler.Status.TargetGeneration, "deployment.Generation", deployment.GetGeneration())
if EvictionAutoScaler.Status.TargetGeneration != deployment.GetGeneration() {
//EvictionAutoScaler can fail between updating deployment and EvictionAutoScaler targetGeneration;
//hence we need to rely on checking if annotation exists and compare with deployment.Spec.Replicas
// no surge happened but customer already increased deployment replicas, then annotation would not exist
if surgeReplicas, scaleUpAnnotationExists := deployment.Annotations[EvictionSurgeReplicasAnnotationKey]; scaleUpAnnotationExists {
newReplicas, err := strconv.Atoi(surgeReplicas)
if err != nil {
logger.Error(err, "unable to parse surge replicas from annotation NOT updating",
"namespace", deployment.Namespace, "name", deployment.Name, "replicas", surgeReplicas)
return reconcile.Result{}, nil
}
if int32(newReplicas) == *deployment.Spec.Replicas {
return reconcile.Result{}, nil
}
}
//someone else changed deployment num of replicas
pdb.Spec.MinAvailable = &intstr.IntOrString{IntVal: *deployment.Spec.Replicas}
e := r.Update(ctx, &pdb)
if e != nil {
logger.Error(e, "unable to update pdb minAvailable to deployment replicas ",
"namespace", pdb.Namespace, "name", pdb.Name, "replicas", *deployment.Spec.Replicas)
return reconcile.Result{}, e
}
logger.Info("Successfully updated pdb minAvailable to deployment replicas ",
"namespace", pdb.Namespace, "name", pdb.Name, "replicas", *deployment.Spec.Replicas)
}
return reconcile.Result{}, nil
}
func (r *DeploymentToPDBReconciler) generatePDBName(deploymentName string) string {
return deploymentName
}
// SetupWithManager sets up the controller with the Manager.
func (r *DeploymentToPDBReconciler) SetupWithManager(mgr ctrl.Manager) error {
logger := mgr.GetLogger()
// Set up the controller to watch Deployments and trigger the reconcile function
// when controller restarts everything is seen as a create event
return ctrl.NewControllerManagedBy(mgr).
For(&v1.Deployment{}).
WithEventFilter(predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
//logger.Info("Update event detected, no action will be taken")
//ToDo: distinguish scales from our EvictionAutoScaler from scales from other owners and keep minAvailable up near replicas.
// Like if I start a deployment at 3 but then later say this is popular let me bump it to 5 should our pdb change.
if oldDeployment, ok := e.ObjectOld.(*v1.Deployment); ok {
newDeployment := e.ObjectNew.(*v1.Deployment)
logger.Info("Update event detected, num of replicas changed", "newReplicas", newDeployment.Spec.Replicas)
return oldDeployment.Spec.Replicas != newDeployment.Spec.Replicas
}
return false
//return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration()
},
}).
Owns(&policyv1.PodDisruptionBudget{}). // Watch PDBs for ownership
Complete(r)
}