From db301cec138e1df322504fd6eb8ad1eaee68c18e Mon Sep 17 00:00:00 2001 From: Adam Cattermole Date: Fri, 11 Aug 2023 14:58:06 +0100 Subject: [PATCH] Enable configuring PDB from Limitador CR Co-authored-by: Grzegorz Piotrowski --- Makefile | 11 +++ api/v1alpha1/limitador_types.go | 4 + api/v1alpha1/zz_generated.deepcopy.go | 12 ++- ...itador-operator.clusterserviceversion.yaml | 11 +++ .../limitador.kuadrant.io_limitadors.yaml | 72 +++++++++++++++ .../limitador.kuadrant.io_limitadors.yaml | 72 +++++++++++++++ config/manager/manager.yaml | 60 ++++++------ config/rbac/role.yaml | 11 +++ controllers/limitador_controller.go | 42 +++++++++ controllers/limitador_controller_test.go | 92 +++++++++++++++++-- pkg/limitador/k8s_objects.go | 12 +++ pkg/limitador/k8s_objects_test.go | 26 ++++++ pkg/reconcilers/base_reconciler.go | 5 + pkg/reconcilers/poddisruptionbudget.go | 34 +++++++ 14 files changed, 425 insertions(+), 39 deletions(-) create mode 100644 pkg/reconcilers/poddisruptionbudget.go diff --git a/Makefile b/Makefile index 7f408b43..9424846a 100644 --- a/Makefile +++ b/Makefile @@ -311,6 +311,17 @@ local-setup: ## Deploy operator in local kind cluster local-cleanup: ## Clean up local kind cluster $(MAKE) kind-delete-cluster +.PHONY: local-deploy +local-deploy: export IMG := limitador-operator:dev +local-deploy: ## re-deploy operator in local kind cluster + $(MAKE) docker-build + @echo "Deploying Limitador control plane" + $(KIND) load docker-image ${IMG} --name ${KIND_CLUSTER_NAME} + make deploy-develmode + kubectl rollout restart deployment -n limitador-operator-system limitador-operator-controller-manager + @echo "Wait for all deployments to be up" + kubectl -n limitador-operator-system wait --timeout=300s --for=condition=Available deployments --all + ##@ Code Style .PHONY: run-lint diff --git a/api/v1alpha1/limitador_types.go b/api/v1alpha1/limitador_types.go index 308bfbda..5dd5c2bc 100644 --- a/api/v1alpha1/limitador_types.go +++ b/api/v1alpha1/limitador_types.go @@ -23,6 +23,7 @@ import ( "github.com/go-logr/logr" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/kuadrant/limitador-operator/pkg/helpers" @@ -61,6 +62,9 @@ type LimitadorSpec struct { // +optional Limits []RateLimit `json:"limits,omitempty"` + + // +optional + PodDisruptionBudget *policyv1.PodDisruptionBudgetSpec `json:"pdb,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index bf092bd7..8ad17129 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,8 @@ limitations under the License. package v1alpha1 import ( - "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -137,6 +138,11 @@ func (in *LimitadorSpec) DeepCopyInto(out *LimitadorSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.PodDisruptionBudget != nil { + in, out := &in.PodDisruptionBudget, &out.PodDisruptionBudget + *out = new(v1.PodDisruptionBudgetSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LimitadorSpec. @@ -246,7 +252,7 @@ func (in *Redis) DeepCopyInto(out *Redis) { *out = *in if in.ConfigSecretRef != nil { in, out := &in.ConfigSecretRef, &out.ConfigSecretRef - *out = new(v1.ObjectReference) + *out = new(corev1.ObjectReference) **out = **in } } @@ -266,7 +272,7 @@ func (in *RedisCached) DeepCopyInto(out *RedisCached) { *out = *in if in.ConfigSecretRef != nil { in, out := &in.ConfigSecretRef, &out.ConfigSecretRef - *out = new(v1.ObjectReference) + *out = new(corev1.ObjectReference) **out = **in } if in.Options != nil { diff --git a/bundle/manifests/limitador-operator.clusterserviceversion.yaml b/bundle/manifests/limitador-operator.clusterserviceversion.yaml index 74148f32..5761bef0 100644 --- a/bundle/manifests/limitador-operator.clusterserviceversion.yaml +++ b/bundle/manifests/limitador-operator.clusterserviceversion.yaml @@ -118,6 +118,17 @@ spec: - get - patch - update + - apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - update + - watch serviceAccountName: limitador-operator-controller-manager deployments: - label: diff --git a/bundle/manifests/limitador.kuadrant.io_limitadors.yaml b/bundle/manifests/limitador.kuadrant.io_limitadors.yaml index a4b2eeb8..808ac45e 100644 --- a/bundle/manifests/limitador.kuadrant.io_limitadors.yaml +++ b/bundle/manifests/limitador.kuadrant.io_limitadors.yaml @@ -75,6 +75,78 @@ spec: type: integer type: object type: object + pdb: + description: PodDisruptionBudgetSpec is a description of a PodDisruptionBudget. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: An eviction is allowed if at most "maxUnavailable" + pods selected by "selector" are unavailable after the eviction, + i.e. even in absence of the evicted pod. For example, one can + prevent all voluntary evictions by specifying 0. This is a mutually + exclusive setting with "minAvailable". + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: An eviction is allowed if at least "minAvailable" + pods selected by "selector" will still be available after the + eviction, i.e. even in the absence of the evicted pod. So for + example you can prevent all voluntary evictions by specifying + "100%". + x-kubernetes-int-or-string: true + selector: + description: Label query over pods whose evictions are managed + by the disruption budget. A null selector will match no pods, + while an empty ({}) selector will select all pods within the + namespace. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If + the operator is In or NotIn, the values array must + be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A + single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is "key", + the operator is "In", and the values array contains only + "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object rateLimitHeaders: description: RateLimitHeadersType defines the valid options for the --rate-limit-headers arg diff --git a/config/crd/bases/limitador.kuadrant.io_limitadors.yaml b/config/crd/bases/limitador.kuadrant.io_limitadors.yaml index c0670825..874ad1c7 100644 --- a/config/crd/bases/limitador.kuadrant.io_limitadors.yaml +++ b/config/crd/bases/limitador.kuadrant.io_limitadors.yaml @@ -76,6 +76,78 @@ spec: type: integer type: object type: object + pdb: + description: PodDisruptionBudgetSpec is a description of a PodDisruptionBudget. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: An eviction is allowed if at most "maxUnavailable" + pods selected by "selector" are unavailable after the eviction, + i.e. even in absence of the evicted pod. For example, one can + prevent all voluntary evictions by specifying 0. This is a mutually + exclusive setting with "minAvailable". + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: An eviction is allowed if at least "minAvailable" + pods selected by "selector" will still be available after the + eviction, i.e. even in the absence of the evicted pod. So for + example you can prevent all voluntary evictions by specifying + "100%". + x-kubernetes-int-or-string: true + selector: + description: Label query over pods whose evictions are managed + by the disruption budget. A null selector will match no pods, + while an empty ({}) selector will select all pods within the + namespace. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If + the operator is In or NotIn, the values array must + be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A + single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is "key", + the operator is "In", and the values array contains only + "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object rateLimitHeaders: description: RateLimitHeadersType defines the valid options for the --rate-limit-headers arg diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 9ba31a43..a9f76bab 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -25,35 +25,35 @@ spec: securityContext: runAsNonRoot: true containers: - - command: - - /manager - args: - - --leader-elect - env: - - name: RELATED_IMAGE_LIMITADOR - value: "quay.io/kuadrant/limitador:latest" - image: controller:latest - name: manager - securityContext: - allowPrivilegeEscalation: false - livenessProbe: - httpGet: - path: /healthz - port: 8081 - initialDelaySeconds: 15 - periodSeconds: 20 - readinessProbe: - httpGet: - path: /readyz - port: 8081 - initialDelaySeconds: 5 - periodSeconds: 10 - resources: - limits: - cpu: 200m - memory: 300Mi - requests: - cpu: 200m - memory: 200Mi + - command: + - /manager + args: + - --leader-elect + env: + - name: RELATED_IMAGE_LIMITADOR + value: "quay.io/kuadrant/limitador:latest" + image: controller:latest + name: manager + securityContext: + allowPrivilegeEscalation: false + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 200m + memory: 300Mi + requests: + cpu: 200m + memory: 200Mi serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 5d5243ac..9b27788a 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -65,3 +65,14 @@ rules: - get - patch - update +- apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - delete + - get + - list + - update + - watch diff --git a/controllers/limitador_controller.go b/controllers/limitador_controller.go index cab383aa..1cac9c74 100644 --- a/controllers/limitador_controller.go +++ b/controllers/limitador_controller.go @@ -25,7 +25,9 @@ import ( "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -45,6 +47,7 @@ type LimitadorReconciler struct { //+kubebuilder:rbac:groups=limitador.kuadrant.io,resources=limitadors/status,verbs=get;update;patch //+kubebuilder:rbac:groups=limitador.kuadrant.io,resources=limitadors/finalizers,verbs=update //+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;delete +//+kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=get;list;watch;create;update;delete //+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;delete //+kubebuilder:rbac:groups="",resources=configmaps;secrets,verbs=get;list;watch;create;update;delete @@ -116,9 +119,47 @@ func (r *LimitadorReconciler) reconcileSpec(ctx context.Context, limitadorObj *l return ctrl.Result{}, err } + if err := r.reconcilePdb(ctx, limitadorObj); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil } +func (r *LimitadorReconciler) reconcilePdb(ctx context.Context, limitadorObj *limitadorv1alpha1.Limitador) error { + if limitadorObj.Spec.PodDisruptionBudget == nil { + return nil + } + + limitadorPdb := limitadorObj.Spec.PodDisruptionBudget + if err := limitador.ValidatePDB(limitadorPdb); err != nil { + return err + } + + limitadorPdb.Selector = &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "limitador"}, + } + + pdb := &policyv1.PodDisruptionBudget{ + ObjectMeta: metav1.ObjectMeta{ + Name: limitador.PodDisruptionBudgetName(limitadorObj), + Namespace: limitadorObj.ObjectMeta.Namespace, + }, + Spec: *limitadorPdb, + } + + // controller reference + if err := r.SetOwnerReference(limitadorObj, pdb); err != nil { + return err + } + err := r.ReconcilePodDisruptionBudget(ctx, pdb, reconcilers.PodDisruptionBudgetMutator) + r.Logger().Info("reconcile pdb", "error", err) + if err != nil { + return err + } + return nil +} + func (r *LimitadorReconciler) reconcileDeployment(ctx context.Context, limitadorObj *limitadorv1alpha1.Limitador) error { logger, err := logr.FromContext(ctx) if err != nil { @@ -212,6 +253,7 @@ func (r *LimitadorReconciler) SetupWithManager(mgr ctrl.Manager) error { For(&limitadorv1alpha1.Limitador{}). Owns(&appsv1.Deployment{}). Owns(&v1.ConfigMap{}). + Owns(&policyv1.PodDisruptionBudget{}). Complete(r) } diff --git a/controllers/limitador_controller_test.go b/controllers/limitador_controller_test.go index a740e573..fbe7d77e 100644 --- a/controllers/limitador_controller_test.go +++ b/controllers/limitador_controller_test.go @@ -9,9 +9,11 @@ import ( . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/uuid" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" @@ -22,12 +24,14 @@ import ( var _ = Describe("Limitador controller", func() { const ( - LimitadorNamespace = "default" - LimitadorReplicas = 2 - LimitadorImage = "quay.io/kuadrant/limitador" - LimitadorVersion = "0.3.0" - LimitadorHTTPPort = 8000 - LimitadorGRPCPort = 8001 + LimitadorNamespace = "default" + LimitadorReplicas = 2 + LimitadorImage = "quay.io/kuadrant/limitador" + LimitadorVersion = "0.3.0" + LimitadorHTTPPort = 8000 + LimitadorGRPCPort = 8001 + LimitadorMaxUnavailable = 1 + LimitdaorUpdatedMaxUnavailable = 3 timeout = time.Second * 10 interval = time.Millisecond * 250 @@ -36,6 +40,15 @@ var _ = Describe("Limitador controller", func() { httpPortNumber := int32(LimitadorHTTPPort) grpcPortNumber := int32(LimitadorGRPCPort) + maxUnavailable := &intstr.IntOrString{ + Type: 0, + IntVal: LimitadorMaxUnavailable, + } + updatedMaxUnavailable := &intstr.IntOrString{ + Type: 0, + IntVal: LimitdaorUpdatedMaxUnavailable, + } + replicas := LimitadorReplicas version := LimitadorVersion httpPort := &limitadorv1alpha1.TransportProtocol{Port: &httpPortNumber} @@ -79,6 +92,9 @@ var _ = Describe("Limitador controller", func() { GRPC: grpcPort, }, Limits: limits, + PodDisruptionBudget: &policyv1.PodDisruptionBudgetSpec{ + MaxUnavailable: maxUnavailable, + }, }, } } @@ -228,6 +244,23 @@ var _ = Describe("Limitador controller", func() { Expect(err == nil) Expect(cmLimits).To(Equal(limits)) }) + + It("Should create a PodDisruptionBudget", func() { + createdPdb := policyv1.PodDisruptionBudget{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: LimitadorNamespace, + Name: limitador.PodDisruptionBudgetName(limitadorObj), + }, + &createdPdb) + + return err == nil + }, timeout, interval).Should(BeTrue()) + Expect(createdPdb.Spec.MaxUnavailable).To(Equal(maxUnavailable)) + Expect(createdPdb.Spec.Selector.MatchLabels).To(Equal(map[string]string{"app": "limitador"})) + }) }) Context("Updating a limitador object", func() { @@ -339,6 +372,53 @@ var _ = Describe("Limitador controller", func() { Expect(err == nil) Expect(cmLimits).To(Equal(newLimits)) }) + + It("Updates the PodDisruptionBudget accordingly", func() { + originalPdb := policyv1.PodDisruptionBudget{} + + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: LimitadorNamespace, + Name: limitador.PodDisruptionBudgetName(limitadorObj), + }, + &originalPdb) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + updatedLimitador := limitadorv1alpha1.Limitador{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: LimitadorNamespace, + Name: limitadorObj.Name, + }, + &updatedLimitador) + + return err == nil + }, timeout, interval).Should(BeTrue()) + + updatedLimitador.Spec.PodDisruptionBudget.MaxUnavailable = updatedMaxUnavailable + Expect(k8sClient.Update(context.TODO(), &updatedLimitador)).Should(Succeed()) + + updatedPdb := policyv1.PodDisruptionBudget{} + Eventually(func() bool { + err := k8sClient.Get( + context.TODO(), + types.NamespacedName{ + Namespace: LimitadorNamespace, + Name: limitador.PodDisruptionBudgetName(limitadorObj), + }, + &updatedPdb) + + return err == nil && updatedPdb.ResourceVersion != originalPdb.ResourceVersion + }, timeout, interval).Should(BeTrue()) + + Expect(updatedPdb.Spec.MaxUnavailable).To(Equal(updatedMaxUnavailable)) + }) }) Context("Creating a new Limitador object with rate limit headers", func() { diff --git a/pkg/limitador/k8s_objects.go b/pkg/limitador/k8s_objects.go index d72b2b8b..828babe1 100644 --- a/pkg/limitador/k8s_objects.go +++ b/pkg/limitador/k8s_objects.go @@ -5,6 +5,7 @@ import ( appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/yaml" @@ -179,6 +180,17 @@ func ServiceName(limitadorObj *limitadorv1alpha1.Limitador) string { return fmt.Sprintf("limitador-%s", limitadorObj.Name) } +func PodDisruptionBudgetName(limitadorObj *limitadorv1alpha1.Limitador) string { + return fmt.Sprintf("limitador-%s", limitadorObj.Name) +} + +func ValidatePDB(pdb *policyv1.PodDisruptionBudgetSpec) error { + if pdb.MaxUnavailable != nil && pdb.MinAvailable != nil { + return fmt.Errorf("pdb spec invalid, maxunavailable and minavailable are mutually exclusive") + } + return nil +} + func labels() map[string]string { return map[string]string{"app": "limitador"} } diff --git a/pkg/limitador/k8s_objects_test.go b/pkg/limitador/k8s_objects_test.go index ff7ac504..3647fe77 100644 --- a/pkg/limitador/k8s_objects_test.go +++ b/pkg/limitador/k8s_objects_test.go @@ -5,7 +5,9 @@ import ( limitadorv1alpha1 "github.com/kuadrant/limitador-operator/api/v1alpha1" "gotest.tools/assert" + policyv1 "k8s.io/api/policy/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" ) func TestConstants(t *testing.T) { @@ -42,6 +44,12 @@ func newTestLimitadorObj(name, namespace string, limits []limitadorv1alpha1.Rate GRPC: &limitadorv1alpha1.TransportProtocol{Port: &grpcPort}, }, Limits: limits, + PodDisruptionBudget: &policyv1.PodDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: 0, + IntVal: 1, + }, + }, }, } } @@ -88,3 +96,21 @@ func TestDeployment(t *testing.T) { }) }) } + +func TestPodDisruptionBudgetName(t *testing.T) { + name := PodDisruptionBudgetName(newTestLimitadorObj("my-limitador-instance", "default", nil)) + assert.Equal(t, name, "limitador-my-limitador-instance") +} + +func TestValidatePdb(t *testing.T) { + intStrOne := &intstr.IntOrString{ + Type: 0, + IntVal: 1, + } + limitadorPdb := &policyv1.PodDisruptionBudgetSpec{ + MaxUnavailable: intStrOne, + MinAvailable: intStrOne, + } + err := ValidatePDB(limitadorPdb) + assert.Error(t, err, "pdb spec invalid, maxunavailable and minavailable are mutually exclusive") +} diff --git a/pkg/reconcilers/base_reconciler.go b/pkg/reconcilers/base_reconciler.go index e7a34506..495e7e47 100644 --- a/pkg/reconcilers/base_reconciler.go +++ b/pkg/reconcilers/base_reconciler.go @@ -22,6 +22,7 @@ import ( "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -150,6 +151,10 @@ func (b *BaseReconciler) ReconcileConfigMap(ctx context.Context, desired *corev1 return b.ReconcileResource(ctx, &corev1.ConfigMap{}, desired, mutatefn) } +func (b *BaseReconciler) ReconcilePodDisruptionBudget(ctx context.Context, desired *policyv1.PodDisruptionBudget, mutatefn MutateFn) error { + return b.ReconcileResource(ctx, &policyv1.PodDisruptionBudget{}, desired, mutatefn) +} + func (b *BaseReconciler) GetResource(ctx context.Context, objKey types.NamespacedName, obj client.Object) error { logger, err := logr.FromContext(ctx) if err != nil { diff --git a/pkg/reconcilers/poddisruptionbudget.go b/pkg/reconcilers/poddisruptionbudget.go new file mode 100644 index 00000000..e96e3336 --- /dev/null +++ b/pkg/reconcilers/poddisruptionbudget.go @@ -0,0 +1,34 @@ +package reconcilers + +import ( + "fmt" + "reflect" + + policyv1 "k8s.io/api/policy/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func PodDisruptionBudgetMutator(existingObj, desiredObj client.Object) (bool, error) { + update := false + + existing, ok := existingObj.(*policyv1.PodDisruptionBudget) + if !ok { + return false, fmt.Errorf("%T is not a *policyv1.PodDisruptionBudget", existingObj) + } + desired, ok := desiredObj.(*policyv1.PodDisruptionBudget) + if !ok { + return false, fmt.Errorf("%T is not a *policyv1.PodDisruptionBudget", desiredObj) + } + + if !reflect.DeepEqual(existing.Spec.MaxUnavailable, desired.Spec.MaxUnavailable) { + existing.Spec.MaxUnavailable = desired.Spec.MaxUnavailable + update = true + } + + if !reflect.DeepEqual(existing.Spec.MinAvailable, desired.Spec.MinAvailable) { + existing.Spec.MinAvailable = desired.Spec.MinAvailable + update = true + } + + return update, nil +}