diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 319a30b..d592afc 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -29,8 +29,8 @@ rules: resources: - secrets verbs: - - list - get + - list - watch - apiGroups: - "" diff --git a/internal/controller/etcdcluster_controller.go b/internal/controller/etcdcluster_controller.go index 82cc706..e3c1485 100644 --- a/internal/controller/etcdcluster_controller.go +++ b/internal/controller/etcdcluster_controller.go @@ -37,6 +37,8 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -64,7 +66,7 @@ type EtcdClusterReconciler struct { // +kubebuilder:rbac:groups="",resources=endpoints,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;watch;delete;patch // +kubebuilder:rbac:groups="",resources=services,verbs=get;create;delete;update;patch;list;watch -// +kubebuilder:rbac:groups="",resources=secrets,verbs=view;list;watch +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch // +kubebuilder:rbac:groups="apps",resources=statefulsets,verbs=get;create;delete;update;patch;list;watch // +kubebuilder:rbac:groups="policy",resources=poddisruptionbudgets,verbs=get;create;delete;update;patch;list;watch @@ -133,17 +135,37 @@ func (r *EtcdClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) state.setClusterID() if state.inSplitbrain() { log.Error(ctx, fmt.Errorf("etcd cluster in splitbrain"), "etcd cluster in splitbrain, dropping from reconciliation queue") - factory.SetCondition(instance, factory.NewCondition(etcdaenixiov1alpha1.EtcdConditionError). - WithStatus(true). - WithReason(string(etcdaenixiov1alpha1.EtcdCondTypeSplitbrain)). - WithMessage(string(etcdaenixiov1alpha1.EtcdErrorCondSplitbrainMessage)). - Complete(), + meta.SetStatusCondition( + &instance.Status.Conditions, + metav1.Condition{ + Type: etcdaenixiov1alpha1.EtcdConditionError, + Status: metav1.ConditionTrue, + Reason: string(etcdaenixiov1alpha1.EtcdCondTypeSplitbrain), + Message: string(etcdaenixiov1alpha1.EtcdErrorCondSplitbrainMessage), + }, ) return r.updateStatus(ctx, instance) } // fill conditions if len(instance.Status.Conditions) == 0 { - factory.FillConditions(instance) + meta.SetStatusCondition( + &instance.Status.Conditions, + metav1.Condition{ + Type: etcdaenixiov1alpha1.EtcdConditionInitialized, + Status: metav1.ConditionFalse, + Reason: string(etcdaenixiov1alpha1.EtcdCondTypeInitStarted), + Message: string(etcdaenixiov1alpha1.EtcdInitCondNegMessage), + }, + ) + meta.SetStatusCondition( + &instance.Status.Conditions, + metav1.Condition{ + Type: etcdaenixiov1alpha1.EtcdConditionReady, + Status: metav1.ConditionFalse, + Reason: string(etcdaenixiov1alpha1.EtcdCondTypeWaitingForFirstQuorum), + Message: string(etcdaenixiov1alpha1.EtcdReadyCondNegWaitingForQuorum), + }, + ) } // ensure managed resources @@ -152,11 +174,15 @@ func (r *EtcdClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // set cluster initialization condition - factory.SetCondition(instance, factory.NewCondition(etcdaenixiov1alpha1.EtcdConditionInitialized). - WithStatus(true). - WithReason(string(etcdaenixiov1alpha1.EtcdCondTypeInitComplete)). - WithMessage(string(etcdaenixiov1alpha1.EtcdInitCondPosMessage)). - Complete()) + meta.SetStatusCondition( + &instance.Status.Conditions, + metav1.Condition{ + Type: etcdaenixiov1alpha1.EtcdConditionInitialized, + Status: metav1.ConditionTrue, + Reason: string(etcdaenixiov1alpha1.EtcdCondTypeInitComplete), + Message: string(etcdaenixiov1alpha1.EtcdInitCondPosMessage), + }, + ) // check sts condition clusterReady, err := r.isStatefulSetReady(ctx, instance) @@ -173,7 +199,7 @@ func (r *EtcdClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // set cluster readiness condition - existingCondition := factory.GetCondition(instance, etcdaenixiov1alpha1.EtcdConditionReady) + existingCondition := meta.FindStatusCondition(instance.Status.Conditions, etcdaenixiov1alpha1.EtcdConditionReady) if existingCondition != nil && existingCondition.Reason == string(etcdaenixiov1alpha1.EtcdCondTypeWaitingForFirstQuorum) && !clusterReady { @@ -186,16 +212,22 @@ func (r *EtcdClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) // StatefulSet is or isn't ready. reason := etcdaenixiov1alpha1.EtcdCondTypeStatefulSetNotReady message := etcdaenixiov1alpha1.EtcdReadyCondNegMessage + ready := metav1.ConditionFalse if clusterReady { reason = etcdaenixiov1alpha1.EtcdCondTypeStatefulSetReady message = etcdaenixiov1alpha1.EtcdReadyCondPosMessage + ready = metav1.ConditionTrue } - factory.SetCondition(instance, factory.NewCondition(etcdaenixiov1alpha1.EtcdConditionReady). - WithStatus(clusterReady). - WithReason(string(reason)). - WithMessage(string(message)). - Complete()) + meta.SetStatusCondition( + &instance.Status.Conditions, + metav1.Condition{ + Type: etcdaenixiov1alpha1.EtcdConditionReady, + Status: ready, + Reason: string(reason), + Message: string(message), + }, + ) return r.updateStatus(ctx, instance) } diff --git a/internal/controller/factory/condition.go b/internal/controller/factory/condition.go deleted file mode 100644 index f340623..0000000 --- a/internal/controller/factory/condition.go +++ /dev/null @@ -1,110 +0,0 @@ -/* -Copyright 2024 The etcd-operator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package factory - -import ( - "slices" - - etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// Condition is a wrapper over metav1.Condition type. -type Condition struct { - metav1.Condition -} - -// NewCondition returns Condition object with provided condition type. -func NewCondition(conditionType etcdaenixiov1alpha1.EtcdCondType) Condition { - return Condition{metav1.Condition{Type: string(conditionType)}} -} - -// WithStatus converts boolean value passed as an argument to metav1.ConditionStatus type and sets condition status. -func (c Condition) WithStatus(status bool) Condition { - c.Status = metav1.ConditionFalse - if status { - c.Status = metav1.ConditionTrue - } - return c -} - -// WithReason sets condition reason. -func (c Condition) WithReason(reason string) Condition { - c.Reason = reason - return c -} - -// WithMessage sets condition message. -func (c Condition) WithMessage(message string) Condition { - c.Message = message - return c -} - -// Complete finalizes condition building by setting transition timestamp and returns wrapped metav1.Condition object. -func (c Condition) Complete() metav1.Condition { - c.LastTransitionTime = metav1.Now() - return c.Condition -} - -// FillConditions fills EtcdCluster .status.Conditions list with all available conditions in "False" status. -func FillConditions(cluster *etcdaenixiov1alpha1.EtcdCluster) { - SetCondition(cluster, NewCondition(etcdaenixiov1alpha1.EtcdConditionInitialized). - WithStatus(false). - WithReason(string(etcdaenixiov1alpha1.EtcdCondTypeInitStarted)). - WithMessage(string(etcdaenixiov1alpha1.EtcdInitCondNegMessage)). - Complete()) - SetCondition(cluster, NewCondition(etcdaenixiov1alpha1.EtcdConditionReady). - WithStatus(false). - WithReason(string(etcdaenixiov1alpha1.EtcdCondTypeWaitingForFirstQuorum)). - WithMessage(string(etcdaenixiov1alpha1.EtcdReadyCondNegWaitingForQuorum)). - Complete()) -} - -// SetCondition sets either replaces corresponding existing condition in the .status.Conditions list or appends -// one passed as an argument. In case operation will not result into condition status change, return. -func SetCondition( - cluster *etcdaenixiov1alpha1.EtcdCluster, - condition metav1.Condition, -) { - condition.ObservedGeneration = cluster.GetGeneration() - idx := slices.IndexFunc(cluster.Status.Conditions, func(c metav1.Condition) bool { - return c.Type == condition.Type - }) - - if idx == -1 { - cluster.Status.Conditions = append(cluster.Status.Conditions, condition) - return - } - statusNotChanged := cluster.Status.Conditions[idx].Status == condition.Status - reasonNotChanged := cluster.Status.Conditions[idx].Reason == condition.Reason - if statusNotChanged && reasonNotChanged { - return - } - cluster.Status.Conditions[idx] = condition -} - -// GetCondition returns condition from cluster status conditions by type or nil if not present. -func GetCondition(cluster *etcdaenixiov1alpha1.EtcdCluster, condType string) *metav1.Condition { - idx := slices.IndexFunc(cluster.Status.Conditions, func(c metav1.Condition) bool { - return c.Type == condType - }) - if idx == -1 { - return nil - } - - return &cluster.Status.Conditions[idx] -} diff --git a/internal/controller/factory/condition_test.go b/internal/controller/factory/condition_test.go deleted file mode 100644 index eadd9f4..0000000 --- a/internal/controller/factory/condition_test.go +++ /dev/null @@ -1,129 +0,0 @@ -/* -Copyright 2024 The etcd-operator Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package factory - -import ( - "slices" - "time" - - etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -var _ = Describe("Condition builder", func() { - Context("When updating condition", func() { - etcdCluster := &etcdaenixiov1alpha1.EtcdCluster{} - - It("should fill conditions", func() { - FillConditions(etcdCluster) - Expect(etcdCluster.Status.Conditions).NotTo(BeEmpty()) - for _, c := range etcdCluster.Status.Conditions { - Expect(c.Status).To(Equal(metav1.ConditionFalse)) - Expect(c.LastTransitionTime).NotTo(BeZero()) - } - }) - - It("should update existing condition", func() { - conditionsLength := len(etcdCluster.Status.Conditions) - SetCondition(etcdCluster, NewCondition(etcdaenixiov1alpha1.EtcdConditionInitialized). - WithStatus(false). - WithReason(string(etcdaenixiov1alpha1.EtcdCondTypeInitStarted)). - WithMessage("test"). - Complete()) - Expect(len(etcdCluster.Status.Conditions)).To(Equal(conditionsLength)) - }) - - It("should keep last transition timestamp without status change, otherwise update", func() { - idx := slices.IndexFunc(etcdCluster.Status.Conditions, func(condition metav1.Condition) bool { - return condition.Type == etcdaenixiov1alpha1.EtcdConditionInitialized - }) - timestamp := etcdCluster.Status.Conditions[idx].LastTransitionTime - - By("setting condition without status change", func() { - SetCondition(etcdCluster, NewCondition(etcdaenixiov1alpha1.EtcdConditionInitialized). - WithStatus(false). - WithReason(string(etcdaenixiov1alpha1.EtcdCondTypeInitStarted)). - WithMessage("test"). - Complete()) - Expect(etcdCluster.Status.Conditions[idx].LastTransitionTime).To(Equal(timestamp)) - }) - - By("setting condition with status changed", func() { - SetCondition(etcdCluster, NewCondition(etcdaenixiov1alpha1.EtcdConditionInitialized). - WithStatus(true). - WithReason(string(etcdaenixiov1alpha1.EtcdCondTypeInitStarted)). - WithMessage("test"). - Complete()) - Expect(etcdCluster.Status.Conditions[idx].LastTransitionTime).NotTo(Equal(timestamp)) - }) - }) - }) - - Context("when retrieving conditions", func() { - It("should return nil if condition of such type is not present", func() { - etcdCluster := &etcdaenixiov1alpha1.EtcdCluster{ - Status: etcdaenixiov1alpha1.EtcdClusterStatus{ - Conditions: []metav1.Condition{ - { - Type: etcdaenixiov1alpha1.EtcdConditionInitialized, - Status: metav1.ConditionTrue, - ObservedGeneration: 0, - LastTransitionTime: metav1.NewTime(time.Now()), - Reason: string(etcdaenixiov1alpha1.EtcdCondTypeInitComplete), - Message: string(etcdaenixiov1alpha1.EtcdInitCondPosMessage), - }, - }, - }, - } - - Expect(GetCondition(etcdCluster, etcdaenixiov1alpha1.EtcdConditionReady)).To(BeNil()) - }) - - It("should return correct condition from the list", func() { - expectedCond := metav1.Condition{ - Type: etcdaenixiov1alpha1.EtcdConditionReady, - Status: metav1.ConditionTrue, - ObservedGeneration: 0, - LastTransitionTime: metav1.NewTime(time.Now()), - Reason: string(etcdaenixiov1alpha1.EtcdCondTypeStatefulSetReady), - Message: string(etcdaenixiov1alpha1.EtcdReadyCondPosMessage), - } - - etcdCluster := &etcdaenixiov1alpha1.EtcdCluster{ - Status: etcdaenixiov1alpha1.EtcdClusterStatus{ - Conditions: []metav1.Condition{ - expectedCond, - { - Type: etcdaenixiov1alpha1.EtcdConditionInitialized, - Status: metav1.ConditionTrue, - ObservedGeneration: 0, - LastTransitionTime: metav1.NewTime(time.Now()), - Reason: string(etcdaenixiov1alpha1.EtcdCondTypeInitComplete), - Message: string(etcdaenixiov1alpha1.EtcdInitCondPosMessage), - }, - }, - }, - } - foundCond := GetCondition(etcdCluster, etcdaenixiov1alpha1.EtcdConditionReady) - if Expect(foundCond).NotTo(BeNil()) { - Expect(*foundCond).To(Equal(expectedCond)) - } - }) - }) -}) diff --git a/internal/controller/factory/configMap.go b/internal/controller/factory/configmap.go similarity index 95% rename from internal/controller/factory/configMap.go rename to internal/controller/factory/configmap.go index 66862cf..a2d282c 100644 --- a/internal/controller/factory/configMap.go +++ b/internal/controller/factory/configmap.go @@ -23,6 +23,7 @@ import ( etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1" "github.com/aenix-io/etcd-operator/internal/log" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -83,7 +84,7 @@ func CreateOrUpdateClusterStateConfigMap( // isEtcdClusterReady returns true if condition "Ready" has progressed // from reason v1alpha1.EtcdCondTypeWaitingForFirstQuorum. func isEtcdClusterReady(cluster *etcdaenixiov1alpha1.EtcdCluster) bool { - cond := GetCondition(cluster, etcdaenixiov1alpha1.EtcdConditionReady) + cond := meta.FindStatusCondition(cluster.Status.Conditions, etcdaenixiov1alpha1.EtcdConditionReady) return cond != nil && (cond.Reason == string(etcdaenixiov1alpha1.EtcdCondTypeStatefulSetReady) || cond.Reason == string(etcdaenixiov1alpha1.EtcdCondTypeStatefulSetNotReady)) } diff --git a/internal/controller/factory/configmap_test.go b/internal/controller/factory/configmap_test.go index 2d74460..237f265 100644 --- a/internal/controller/factory/configmap_test.go +++ b/internal/controller/factory/configmap_test.go @@ -25,6 +25,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -94,20 +95,28 @@ var _ = Describe("CreateOrUpdateClusterStateConfigMap handlers", func() { }) By("processing ready etcd cluster", func() { - SetCondition(&etcdcluster, NewCondition(etcdaenixiov1alpha1.EtcdConditionReady). - WithReason(string(etcdaenixiov1alpha1.EtcdCondTypeStatefulSetReady)). - WithStatus(true). - Complete()) + meta.SetStatusCondition( + &etcdcluster.Status.Conditions, + metav1.Condition{ + Type: etcdaenixiov1alpha1.EtcdConditionReady, + Status: metav1.ConditionTrue, + Reason: string(etcdaenixiov1alpha1.EtcdCondTypeStatefulSetReady), + }, + ) Expect(CreateOrUpdateClusterStateConfigMap(ctx, &etcdcluster, k8sClient)).To(Succeed()) Eventually(Object(&configMap)).Should(HaveField("ObjectMeta.UID", Equal(configMapUID))) Expect(configMap.Data["ETCD_INITIAL_CLUSTER_STATE"]).To(Equal("existing")) }) By("updating the configmap for updated cluster", func() { - SetCondition(&etcdcluster, NewCondition(etcdaenixiov1alpha1.EtcdConditionReady). - WithReason(string(etcdaenixiov1alpha1.EtcdCondTypeWaitingForFirstQuorum)). - WithStatus(true). - Complete()) + meta.SetStatusCondition( + &etcdcluster.Status.Conditions, + metav1.Condition{ + Type: etcdaenixiov1alpha1.EtcdConditionReady, + Status: metav1.ConditionTrue, + Reason: string(etcdaenixiov1alpha1.EtcdCondTypeWaitingForFirstQuorum), + }, + ) Expect(CreateOrUpdateClusterStateConfigMap(ctx, &etcdcluster, k8sClient)).To(Succeed()) Eventually(Object(&configMap)).Should(HaveField("ObjectMeta.UID", Equal(configMapUID))) Expect(configMap.Data["ETCD_INITIAL_CLUSTER_STATE"]).To(Equal("new"))