diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 22df8a34c..d1784d6fc 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -159,3 +159,13 @@ rules: - get - patch - update +- apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - create + - get + - list + - update + - watch diff --git a/controllers/mysqlcluster_controller.go b/controllers/mysqlcluster_controller.go index dad2566e1..1b4fe0e50 100644 --- a/controllers/mysqlcluster_controller.go +++ b/controllers/mysqlcluster_controller.go @@ -19,11 +19,13 @@ import ( appsv1 "k8s.io/api/apps/v1" batchv1beta1 "k8s.io/api/batch/v1beta1" corev1 "k8s.io/api/core/v1" + policyv1beta1 "k8s.io/api/policy/v1beta1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "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" @@ -86,6 +88,7 @@ type MySQLClusterReconciler struct { // +kubebuilder:rbac:groups="batch",resources=cronjobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups="batch",resources=cronjobs/status,verbs=get // +kubebuilder:rbac:groups="",resources=events,verbs=get;list;watch;create;patch +// +kubebuilder:rbac:groups="policy",resources=poddisruptionbudgets,verbs=get;list;watch;create;update // Reconcile reconciles MySQLCluster. func (r *MySQLClusterReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { @@ -232,6 +235,12 @@ func (r *MySQLClusterReconciler) reconcileInitialize(ctx context.Context, log lo return false, err } + isUpdated, err = r.createOrUpdatePodDisruptionBudget(ctx, log, cluster) + isUpdatedAtLeastOnce = isUpdatedAtLeastOnce || isUpdated + if err != nil { + return false, err + } + return isUpdatedAtLeastOnce, nil } @@ -264,6 +273,7 @@ func (r *MySQLClusterReconciler) SetupWithManager(mgr ctrl.Manager, watcherInter Owns(&corev1.ServiceAccount{}). Owns(&corev1.ConfigMap{}). Owns(&batchv1beta1.CronJob{}). + Owns(&policyv1beta1.PodDisruptionBudget{}). Watches(&src, &handler.EnqueueRequestForObject{}). WithOptions( controller.Options{MaxConcurrentReconciles: 8}, @@ -1121,6 +1131,43 @@ func (r *MySQLClusterReconciler) createOrUpdateService(ctx context.Context, clus return isUpdated, nil } +func (r *MySQLClusterReconciler) createOrUpdatePodDisruptionBudget(ctx context.Context, log logr.Logger, cluster *mocov1alpha1.MySQLCluster) (bool, error) { + isUpdated := false + + pdb := &policyv1beta1.PodDisruptionBudget{} + pdb.SetNamespace(cluster.Namespace) + pdb.SetName(moco.UniqueName(cluster)) + + op, err := ctrl.CreateOrUpdate(ctx, r.Client, pdb, func() error { + setStandardLabels(&pdb.ObjectMeta, cluster) + pdb.Spec.MaxUnavailable = &intstr.IntOrString{} + pdb.Spec.MaxUnavailable.Type = intstr.Int + if cluster.Spec.Replicas > 1 { + pdb.Spec.MaxUnavailable.IntVal = cluster.Spec.Replicas / 2 + } else { + pdb.Spec.MaxUnavailable.IntVal = 1 + } + pdb.Spec.Selector = &metav1.LabelSelector{} + pdb.Spec.Selector.MatchLabels = map[string]string{ + moco.ClusterKey: moco.UniqueName(cluster), + moco.ManagedByKey: moco.MyName, + moco.AppNameKey: moco.AppName, + } + + return ctrl.SetControllerReference(cluster, pdb, r.Scheme) + }) + if err != nil { + log.Error(err, "unable to create-or-update PodDisruptionBudget") + return false, err + } + if op != controllerutil.OperationResultNone { + log.Info("reconcile PodDisruptionBudget successfully", "op", op) + isUpdated = true + } + + return isUpdated, nil +} + func (r *MySQLClusterReconciler) generateAgentToken(ctx context.Context, log logr.Logger, cluster *mocov1alpha1.MySQLCluster) (bool, error) { if len(cluster.Status.AgentToken) != 0 { return false, nil diff --git a/controllers/mysqlcluster_controller_test.go b/controllers/mysqlcluster_controller_test.go index 640084565..672fa8592 100644 --- a/controllers/mysqlcluster_controller_test.go +++ b/controllers/mysqlcluster_controller_test.go @@ -15,6 +15,7 @@ import ( appsv1 "k8s.io/api/apps/v1" batchv1beta1 "k8s.io/api/batch/v1beta1" corev1 "k8s.io/api/core/v1" + policyv1beta1 "k8s.io/api/policy/v1beta1" k8serror "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -737,4 +738,78 @@ var _ = Describe("MySQLCluster controller", func() { Expect(isUpdated).Should(BeFalse()) }) }) + + Context("PodDisruptionBudget", func() { + It("should create pod disruption budget", func() { + expectedSpec := policyv1beta1.PodDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + moco.ClusterKey: moco.UniqueName(cluster), + moco.AppNameKey: moco.AppName, + moco.ManagedByKey: moco.MyName, + }, + }, + } + + cluster.Spec.Replicas = 1 + + isUpdated, err := reconciler.createOrUpdatePodDisruptionBudget(ctx, reconciler.Log, cluster) + Expect(err).ShouldNot(HaveOccurred()) + Expect(isUpdated).Should(BeTrue()) + + pdb := &policyv1beta1.PodDisruptionBudget{} + err = k8sClient.Get(ctx, client.ObjectKey{Name: moco.UniqueName(cluster), Namespace: cluster.Namespace}, pdb) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(pdb.Spec).Should(Equal(expectedSpec)) + + isUpdated, err = reconciler.createOrUpdatePodDisruptionBudget(ctx, reconciler.Log, cluster) + Expect(err).ShouldNot(HaveOccurred()) + Expect(isUpdated).Should(BeFalse()) + }) + + It("should fill appropriate MaxUnavailable", func() { + expectedSpec := policyv1beta1.PodDisruptionBudgetSpec{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + moco.ClusterKey: moco.UniqueName(cluster), + moco.AppNameKey: moco.AppName, + moco.ManagedByKey: moco.MyName, + }, + }, + } + + By("checking in case of 5 replicas") + cluster.Spec.Replicas = 5 + isUpdated, err := reconciler.createOrUpdatePodDisruptionBudget(ctx, reconciler.Log, cluster) + Expect(err).ShouldNot(HaveOccurred()) + Expect(isUpdated).Should(BeTrue()) + + pdb := &policyv1beta1.PodDisruptionBudget{} + err = k8sClient.Get(ctx, client.ObjectKey{Name: moco.UniqueName(cluster), Namespace: cluster.Namespace}, pdb) + Expect(err).ShouldNot(HaveOccurred()) + expectedSpec.MaxUnavailable.IntVal = 2 + Expect(pdb.Spec).Should(Equal(expectedSpec)) + + By("checking in case of 3 replicas") + cluster.Spec.Replicas = 3 + isUpdated, err = reconciler.createOrUpdatePodDisruptionBudget(ctx, reconciler.Log, cluster) + Expect(err).ShouldNot(HaveOccurred()) + Expect(isUpdated).Should(BeTrue()) + + pdb = &policyv1beta1.PodDisruptionBudget{} + err = k8sClient.Get(ctx, client.ObjectKey{Name: moco.UniqueName(cluster), Namespace: cluster.Namespace}, pdb) + Expect(err).ShouldNot(HaveOccurred()) + expectedSpec.MaxUnavailable.IntVal = 1 + Expect(pdb.Spec).Should(Equal(expectedSpec)) + }) + }) })