diff --git a/apis/apps/v1/component_types.go b/apis/apps/v1/component_types.go index e68c605ab4b..6ef9c1d990a 100644 --- a/apis/apps/v1/component_types.go +++ b/apis/apps/v1/component_types.go @@ -460,3 +460,18 @@ const ( // FailedComponentPhase indicates that there are some pods of the component not in a 'Running' state. FailedComponentPhase ComponentPhase = "Failed" ) + +// component condition types +const ( + // ComponentConditionProgressing indicates component controller is applying updates, or workload resource is being updated. + ComponentConditionProgressing = "Progressing" + + // ComponentConditionHealthy indicates its workload resource is running and ready. + ComponentConditionHealthy = "Healthy" + + // ComponentConditionAvailable indicates the component can serve requests normally. + ComponentConditionAvailable = "Available" + + // ComponentConditionProvisioningStarted indicates the operator starts resource provisioning to create or change the cluster. + ComponentConditionProvisioningStarted = "ProvisioningStarted" +) diff --git a/apis/apps/v1/componentdefinition_types.go b/apis/apps/v1/componentdefinition_types.go index f46d8455f14..d89876c02da 100644 --- a/apis/apps/v1/componentdefinition_types.go +++ b/apis/apps/v1/componentdefinition_types.go @@ -1273,8 +1273,13 @@ type ReplicasLimit struct { } // ComponentAvailable defines the strategies for determining whether the component is available. +// +// If both `WithPhases` and `WithRole` are specified, the component will be considered +// unavailable if any of them fail. +// If `WithProbe` is specified, `WithPhases` and `WithRole` fields are ignored. type ComponentAvailable struct { // Specifies the phases that the component will go through to be considered available. + // Multiple phases are separated by comma. // // This field is immutable once set. // @@ -1290,8 +1295,6 @@ type ComponentAvailable struct { // Specifies the strategies for determining whether the component is available based on the available probe. // - // If specified, it will take precedence over the WithPhases and WithRole fields. - // // This field is immutable once set. // // +optional diff --git a/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml b/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml index 4bb2bddfb74..01062289792 100644 --- a/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml +++ b/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml @@ -110,6 +110,7 @@ spec: withPhases: description: |- Specifies the phases that the component will go through to be considered available. + Multiple phases are separated by comma. This field is immutable once set. @@ -119,9 +120,6 @@ spec: Specifies the strategies for determining whether the component is available based on the available probe. - If specified, it will take precedence over the WithPhases and WithRole fields. - - This field is immutable once set. properties: condition: diff --git a/controllers/apps/cluster/transformer_cluster_component_status.go b/controllers/apps/cluster/transformer_cluster_component_status.go index 218cda30f2b..7fd2360f386 100644 --- a/controllers/apps/cluster/transformer_cluster_component_status.go +++ b/controllers/apps/cluster/transformer_cluster_component_status.go @@ -27,11 +27,9 @@ import ( "golang.org/x/exp/maps" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/sets" - "sigs.k8s.io/controller-runtime/pkg/client" appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" "github.com/apecloud/kubeblocks/pkg/constant" - "github.com/apecloud/kubeblocks/pkg/controller/component" "github.com/apecloud/kubeblocks/pkg/controller/graph" ) @@ -54,7 +52,7 @@ func (t *clusterComponentStatusTransformer) Transform(ctx graph.TransformContext } func (t *clusterComponentStatusTransformer) transform(transCtx *clusterTransformContext) error { - comps, shardingComps, err := t.listClusterComponents(transCtx) + comps, shardingComps, err := listClusterComponents(transCtx.Context, transCtx.Client, transCtx.Cluster) if err != nil { return err } @@ -65,55 +63,6 @@ func (t *clusterComponentStatusTransformer) transform(transCtx *clusterTransform return nil } -func (t *clusterComponentStatusTransformer) listClusterComponents( - transCtx *clusterTransformContext) (map[string]*appsv1.Component, map[string][]*appsv1.Component, error) { - var ( - cluster = transCtx.Cluster - ) - - compList := &appsv1.ComponentList{} - ml := client.MatchingLabels(constant.GetClusterLabels(cluster.Name)) - if err := transCtx.Client.List(transCtx.Context, compList, client.InNamespace(cluster.Namespace), ml); err != nil { - return nil, nil, err - } - - if len(compList.Items) == 0 { - return nil, nil, nil - } - - comps := make(map[string]*appsv1.Component) - shardingComps := make(map[string][]*appsv1.Component) - - sharding := func(comp *appsv1.Component) bool { - shardingName := shardingCompNName(comp) - if len(shardingName) == 0 { - return false - } - - if _, ok := shardingComps[shardingName]; !ok { - shardingComps[shardingName] = []*appsv1.Component{comp} - } else { - shardingComps[shardingName] = append(shardingComps[shardingName], comp) - } - return true - } - - for i, comp := range compList.Items { - if sharding(&compList.Items[i]) { - continue - } - compName, err := component.ShortName(cluster.Name, comp.Name) - if err != nil { - return nil, nil, err - } - if _, ok := comps[compName]; ok { - return nil, nil, fmt.Errorf("duplicate component name: %s", compName) - } - comps[compName] = &compList.Items[i] - } - return comps, shardingComps, nil -} - func (t *clusterComponentStatusTransformer) transformCompStatus(transCtx *clusterTransformContext, comps map[string]*appsv1.Component) { var ( cluster = transCtx.Cluster diff --git a/controllers/apps/cluster/transformer_cluster_status.go b/controllers/apps/cluster/transformer_cluster_status.go index ccf42ccdf3c..2339ed306a2 100644 --- a/controllers/apps/cluster/transformer_cluster_status.go +++ b/controllers/apps/cluster/transformer_cluster_status.go @@ -20,10 +20,14 @@ along with this program. If not, see . package cluster import ( + "context" + "fmt" "slices" "golang.org/x/exp/maps" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" "github.com/apecloud/kubeblocks/pkg/controller/graph" @@ -41,7 +45,7 @@ func (t *clusterStatusTransformer) Transform(ctx graph.TransformContext, dag *gr graphCli, _ := transCtx.Client.(model.GraphClient) defer func() { t.markClusterDagStatusAction(graphCli, dag, origCluster, cluster) }() - if err := t.reconcileClusterStatus(cluster); err != nil { + if err := t.reconcileClusterStatus(transCtx.Context, transCtx.Client, cluster); err != nil { return err } return nil @@ -53,13 +57,12 @@ func (t *clusterStatusTransformer) markClusterDagStatusAction(graphCli model.Gra } } -func (t *clusterStatusTransformer) reconcileClusterStatus(cluster *appsv1.Cluster) error { +func (t *clusterStatusTransformer) reconcileClusterStatus(ctx context.Context, cli client.Reader, cluster *appsv1.Cluster) error { if len(cluster.Status.Components) == 0 && len(cluster.Status.Shardings) == 0 { return nil } - oldPhase := t.reconcileClusterPhase(cluster) - t.syncClusterConditions(cluster, oldPhase) - return nil + t.reconcileClusterPhase(cluster) + return t.syncClusterConditions(ctx, cli, cluster) } func (t *clusterStatusTransformer) reconcileClusterPhase(cluster *appsv1.Cluster) appsv1.ClusterPhase { @@ -89,29 +92,99 @@ func (t *clusterStatusTransformer) reconcileClusterPhase(cluster *appsv1.Cluster return phase } -func (t *clusterStatusTransformer) syncClusterConditions(cluster *appsv1.Cluster, oldPhase appsv1.ClusterPhase) { - if cluster.Status.Phase == appsv1.RunningClusterPhase && oldPhase != cluster.Status.Phase { +func (t *clusterStatusTransformer) syncClusterConditions(ctx context.Context, cli client.Reader, cluster *appsv1.Cluster) error { + if cluster.Status.Phase == appsv1.RunningClusterPhase { meta.SetStatusCondition(&cluster.Status.Conditions, newClusterReadyCondition(cluster.Name)) - return + } else { + kindNames := map[string][]string{} + for kind, statusMap := range map[string]map[string]appsv1.ClusterComponentStatus{ + "component": cluster.Status.Components, + "sharding": t.shardingToCompStatus(cluster.Status.Shardings), + } { + for name, status := range statusMap { + if status.Phase == appsv1.FailedComponentPhase { + if _, ok := kindNames[kind]; !ok { + kindNames[kind] = []string{} + } + kindNames[kind] = append(kindNames[kind], name) + } + } + } + if len(kindNames) > 0 { + meta.SetStatusCondition(&cluster.Status.Conditions, newClusterNotReadyCondition(cluster.Name, kindNames)) + } } - kindNames := map[string][]string{} - for kind, statusMap := range map[string]map[string]appsv1.ClusterComponentStatus{ - "component": cluster.Status.Components, - "sharding": t.shardingToCompStatus(cluster.Status.Shardings), - } { - for name, status := range statusMap { - if status.Phase == appsv1.FailedComponentPhase { - if _, ok := kindNames[kind]; !ok { - kindNames[kind] = []string{} + setAvailableCondition := func() error { + comps, shardingComps, err := listClusterComponents(ctx, cli, cluster) + if err != nil { + return err + } + available := true + aggregatedMessage := "" + defer func() { + var condition metav1.Condition + if available { + condition = metav1.Condition{ + Type: appsv1.ConditionTypeAvailable, + Status: metav1.ConditionTrue, + Message: "All components are available", + Reason: "Available", + } + } else { + condition = metav1.Condition{ + Type: appsv1.ConditionTypeAvailable, + Status: metav1.ConditionFalse, + Message: aggregatedMessage, + Reason: "Unavailable", } - kindNames[kind] = append(kindNames[kind], name) } + + meta.SetStatusCondition(&cluster.Status.Conditions, condition) + }() + + if len(comps) == 0 && len(shardingComps) == 0 { + available = false + aggregatedMessage = "no component exists; " + return nil } + + for _, comp := range comps { + compCond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ConditionTypeAvailable) + if compCond != nil { + if compCond.Status != metav1.ConditionTrue { + available = false + message := fmt.Sprintf("component %s is not available", comp.Name) + aggregatedMessage += message + "; " + } + } else { + available = false + message := fmt.Sprintf("component %s has no available condition", comp.Name) + aggregatedMessage += message + "; " + } + } + + for shardingName, comps := range shardingComps { + for _, comp := range comps { + compCond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ConditionTypeAvailable) + if compCond != nil { + if compCond.Status != metav1.ConditionTrue { + available = false + message := fmt.Sprintf("component %s of sharding %s is not available", comp.Name, shardingName) + aggregatedMessage += message + "; " + } + } else { + available = false + message := fmt.Sprintf("component %s of sharding %s has no available condition", comp.Name, shardingName) + aggregatedMessage += message + "; " + } + } + } + + return nil } - if len(kindNames) > 0 { - meta.SetStatusCondition(&cluster.Status.Conditions, newClusterNotReadyCondition(cluster.Name, kindNames)) - } + + return setAvailableCondition() } func (t *clusterStatusTransformer) shardingToCompStatus(shardingStatus map[string]appsv1.ClusterShardingStatus) map[string]appsv1.ClusterComponentStatus { diff --git a/controllers/apps/cluster/transformer_cluster_status_test.go b/controllers/apps/cluster/transformer_cluster_status_test.go new file mode 100644 index 00000000000..84bdc453644 --- /dev/null +++ b/controllers/apps/cluster/transformer_cluster_status_test.go @@ -0,0 +1,229 @@ +/* +Copyright (C) 2022-2025 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package cluster + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" + appsutil "github.com/apecloud/kubeblocks/controllers/apps/util" + "github.com/apecloud/kubeblocks/pkg/constant" +) + +var _ = Describe("syncClusterConditions", func() { + const ( + clusterName = "test-cluster" + namespace = "default" + ) + + var ( + transformer clusterStatusTransformer + cluster *appsv1.Cluster + reader *appsutil.MockReader + ) + + newComponent := func(name string, available *metav1.ConditionStatus, shardingName string) *appsv1.Component { + comp := &appsv1.Component{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: clusterName + "-" + name, + Labels: map[string]string{ + constant.AppManagedByLabelKey: constant.AppName, + constant.AppInstanceLabelKey: clusterName, + }, + }, + } + if shardingName != "" { + comp.Labels[constant.KBAppShardingNameLabelKey] = shardingName + } + if available != nil { + comp.Status.Conditions = []metav1.Condition{ + { + Type: appsv1.ConditionTypeAvailable, + Status: *available, + Reason: "test", + }, + } + } + return comp + } + + BeforeEach(func() { + cluster = &appsv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterName, + Namespace: namespace, + }, + Status: appsv1.ClusterStatus{ + Components: map[string]appsv1.ClusterComponentStatus{ + "comp1": {Phase: appsv1.RunningComponentPhase}, + }, + }, + } + reader = &appsutil.MockReader{Objects: []client.Object{}} + }) + + It("should set Ready condition when phase is Running", func() { + cluster.Status.Phase = appsv1.RunningClusterPhase + available := metav1.ConditionTrue + reader.Objects = []client.Object{newComponent("comp1", &available, "")} + + err := transformer.syncClusterConditions(context.Background(), reader, cluster) + Expect(err).Should(BeNil()) + + readyCond := meta.FindStatusCondition(cluster.Status.Conditions, appsv1.ConditionTypeReady) + Expect(readyCond).ShouldNot(BeNil()) + Expect(readyCond.Status).Should(Equal(metav1.ConditionTrue)) + Expect(readyCond.Reason).Should(Equal(ReasonClusterReady)) + }) + + It("should set NotReady condition when components have failed", func() { + cluster.Status.Components["comp1"] = appsv1.ClusterComponentStatus{Phase: appsv1.FailedComponentPhase} + cluster.Status.Phase = appsv1.FailedClusterPhase + available := metav1.ConditionTrue + reader.Objects = []client.Object{newComponent("comp1", &available, "")} + + err := transformer.syncClusterConditions(context.Background(), reader, cluster) + Expect(err).Should(BeNil()) + + readyCond := meta.FindStatusCondition(cluster.Status.Conditions, appsv1.ConditionTypeReady) + Expect(readyCond).ShouldNot(BeNil()) + Expect(readyCond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(readyCond.Reason).Should(Equal(ReasonComponentsNotReady)) + }) + + It("should set NotReady condition when shardings have failed", func() { + cluster.Status.Shardings = map[string]appsv1.ClusterShardingStatus{ + "shard1": {Phase: appsv1.FailedComponentPhase}, + } + cluster.Status.Phase = appsv1.FailedClusterPhase + available := metav1.ConditionTrue + reader.Objects = []client.Object{newComponent("comp1", &available, "")} + + err := transformer.syncClusterConditions(context.Background(), reader, cluster) + Expect(err).Should(BeNil()) + + readyCond := meta.FindStatusCondition(cluster.Status.Conditions, appsv1.ConditionTypeReady) + Expect(readyCond).ShouldNot(BeNil()) + Expect(readyCond.Status).Should(Equal(metav1.ConditionFalse)) + }) + + It("should set Available=True when all components are available", func() { + cluster.Status.Phase = appsv1.RunningClusterPhase + available := metav1.ConditionTrue + reader.Objects = []client.Object{ + newComponent("comp1", &available, ""), + } + + err := transformer.syncClusterConditions(context.Background(), reader, cluster) + Expect(err).Should(BeNil()) + + availCond := meta.FindStatusCondition(cluster.Status.Conditions, appsv1.ConditionTypeAvailable) + Expect(availCond).ShouldNot(BeNil()) + Expect(availCond.Status).Should(Equal(metav1.ConditionTrue)) + Expect(availCond.Reason).Should(Equal("Available")) + }) + + It("should set Available=False when a component is not available", func() { + cluster.Status.Phase = appsv1.RunningClusterPhase + unavailable := metav1.ConditionFalse + reader.Objects = []client.Object{ + newComponent("comp1", &unavailable, ""), + } + + err := transformer.syncClusterConditions(context.Background(), reader, cluster) + Expect(err).Should(BeNil()) + + availCond := meta.FindStatusCondition(cluster.Status.Conditions, appsv1.ConditionTypeAvailable) + Expect(availCond).ShouldNot(BeNil()) + Expect(availCond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(availCond.Reason).Should(Equal("Unavailable")) + Expect(availCond.Message).Should(ContainSubstring("comp1")) + }) + + It("should set Available=False when a component has no available condition", func() { + cluster.Status.Phase = appsv1.RunningClusterPhase + reader.Objects = []client.Object{ + newComponent("comp1", nil, ""), + } + + err := transformer.syncClusterConditions(context.Background(), reader, cluster) + Expect(err).Should(BeNil()) + + availCond := meta.FindStatusCondition(cluster.Status.Conditions, appsv1.ConditionTypeAvailable) + Expect(availCond).ShouldNot(BeNil()) + Expect(availCond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(availCond.Message).Should(ContainSubstring("has no available condition")) + }) + + It("should set Available=False when a sharding component is not available", func() { + cluster.Status.Phase = appsv1.RunningClusterPhase + unavailable := metav1.ConditionFalse + available := metav1.ConditionTrue + reader.Objects = []client.Object{ + newComponent("comp1", &available, ""), + newComponent("shard1-0", &unavailable, "shard1"), + } + + err := transformer.syncClusterConditions(context.Background(), reader, cluster) + Expect(err).Should(BeNil()) + + availCond := meta.FindStatusCondition(cluster.Status.Conditions, appsv1.ConditionTypeAvailable) + Expect(availCond).ShouldNot(BeNil()) + Expect(availCond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(availCond.Message).Should(ContainSubstring("shard1")) + }) + + It("should set Available=True with mixed regular and sharding components all available", func() { + cluster.Status.Phase = appsv1.RunningClusterPhase + available := metav1.ConditionTrue + reader.Objects = []client.Object{ + newComponent("comp1", &available, ""), + newComponent("shard1-0", &available, "shard1"), + newComponent("shard1-1", &available, "shard1"), + } + + err := transformer.syncClusterConditions(context.Background(), reader, cluster) + Expect(err).Should(BeNil()) + + availCond := meta.FindStatusCondition(cluster.Status.Conditions, appsv1.ConditionTypeAvailable) + Expect(availCond).ShouldNot(BeNil()) + Expect(availCond.Status).Should(Equal(metav1.ConditionTrue)) + }) + + It("should set Available=False when no components exist", func() { + cluster.Status.Phase = appsv1.RunningClusterPhase + reader.Objects = []client.Object{} + + err := transformer.syncClusterConditions(context.Background(), reader, cluster) + Expect(err).Should(BeNil()) + + availCond := meta.FindStatusCondition(cluster.Status.Conditions, appsv1.ConditionTypeAvailable) + Expect(availCond).ShouldNot(BeNil()) + Expect(availCond.Status).Should(Equal(metav1.ConditionFalse)) + }) +}) diff --git a/controllers/apps/cluster/utils.go b/controllers/apps/cluster/utils.go index 15a81ae49b3..fcfad1475ae 100644 --- a/controllers/apps/cluster/utils.go +++ b/controllers/apps/cluster/utils.go @@ -21,6 +21,7 @@ package cluster import ( "context" + "fmt" "reflect" "k8s.io/apimachinery/pkg/runtime" @@ -31,6 +32,7 @@ import ( appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" "github.com/apecloud/kubeblocks/pkg/constant" + "github.com/apecloud/kubeblocks/pkg/controller/component" "github.com/apecloud/kubeblocks/pkg/controller/model" dptypes "github.com/apecloud/kubeblocks/pkg/dataprotection/types" ) @@ -139,3 +141,47 @@ func isOwnedByComp(obj client.Object) bool { } return false } + +func listClusterComponents(ctx context.Context, cli client.Reader, cluster *appsv1.Cluster) (map[string]*appsv1.Component, map[string][]*appsv1.Component, error) { + compList := &appsv1.ComponentList{} + ml := client.MatchingLabels(constant.GetClusterLabels(cluster.Name)) + if err := cli.List(ctx, compList, client.InNamespace(cluster.Namespace), ml); err != nil { + return nil, nil, err + } + + if len(compList.Items) == 0 { + return nil, nil, nil + } + + comps := make(map[string]*appsv1.Component) + shardingComps := make(map[string][]*appsv1.Component) + + sharding := func(comp *appsv1.Component) bool { + shardingName := shardingCompNName(comp) + if len(shardingName) == 0 { + return false + } + + if _, ok := shardingComps[shardingName]; !ok { + shardingComps[shardingName] = []*appsv1.Component{comp} + } else { + shardingComps[shardingName] = append(shardingComps[shardingName], comp) + } + return true + } + + for i, comp := range compList.Items { + if sharding(&compList.Items[i]) { + continue + } + compName, err := component.ShortName(cluster.Name, comp.Name) + if err != nil { + return nil, nil, err + } + if _, ok := comps[compName]; ok { + return nil, nil, fmt.Errorf("duplicate component name: %s", compName) + } + comps[compName] = &compList.Items[i] + } + return comps, shardingComps, nil +} diff --git a/controllers/apps/component/component_controller_test.go b/controllers/apps/component/component_controller_test.go index 7c0d348d8d9..1fcdbd42d33 100644 --- a/controllers/apps/component/component_controller_test.go +++ b/controllers/apps/component/component_controller_test.go @@ -818,7 +818,7 @@ var _ = Describe("Component Controller", func() { Eventually(testapps.CheckObj(&testCtx, compKey, func(g Gomega, comp *kbappsv1.Component) { g.Expect(comp.Spec.Replicas).Should(BeEquivalentTo(replicas)) g.Expect(comp.Status.Conditions).Should(HaveLen(1)) - g.Expect(comp.Status.Conditions[0].Type).Should(BeEquivalentTo(kbappsv1.ConditionTypeProvisioningStarted)) + g.Expect(comp.Status.Conditions[0].Type).Should(BeEquivalentTo(kbappsv1.ComponentConditionProvisioningStarted)) g.Expect(comp.Status.Conditions[0].Status).Should(BeEquivalentTo(metav1.ConditionFalse)) g.Expect(comp.Status.Conditions[0].Message).Should(ContainSubstring(replicasOutOfLimitError(replicas, *replicasLimit).Error())) })).Should(Succeed()) diff --git a/controllers/apps/component/transformer_component_status.go b/controllers/apps/component/transformer_component_status.go index 970e59bfbbd..5cd45aa616d 100644 --- a/controllers/apps/component/transformer_component_status.go +++ b/controllers/apps/component/transformer_component_status.go @@ -20,6 +20,7 @@ along with this program. If not, see . package component import ( + "errors" "fmt" "slices" "strconv" @@ -30,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" @@ -159,10 +161,6 @@ func (t *componentStatusTransformer) reconcileStatus(transCtx *componentTransfor }, phase) }() - transCtx.Logger.Info( - fmt.Sprintf("status conditions, creating: %v, its running: %v, has failure: %v, updating: %v", - isInCreatingPhase, isITSUpdatedNRunning, hasFailure, hasRunningScaleOut || hasRunningVolumeExpansion)) - switch { case isDeleting: t.setComponentStatusPhase(transCtx, appsv1.DeletingComponentPhase, nil, "component is Deleting") @@ -204,8 +202,25 @@ func (t *componentStatusTransformer) isWorkloadUpdated() bool { if t.comp == nil || t.runningITS == nil { return false } - generation := t.runningITS.GetAnnotations()[constant.KubeBlocksGenerationKey] - return generation == strconv.FormatInt(t.comp.Generation, 10) + its := t.runningITS + generation := its.GetAnnotations()[constant.KubeBlocksGenerationKey] + if generation != strconv.FormatInt(t.comp.Generation, 10) { + return false + } + + // check whether the underlying workload is updated + if its.Status.ObservedGeneration != its.Generation { + return false + } + if its.Spec.Replicas == nil { + return false + } + replicas := *its.Spec.Replicas + if its.Status.Replicas != replicas || + its.Status.UpdatedReplicas != replicas { + return false + } + return true } // isRunning checks if the component's underlying workload is running. @@ -242,6 +257,9 @@ func (t *componentStatusTransformer) hasScaleOutRunning(transCtx *componentTrans } func (t *componentStatusTransformer) hasVolumeExpansionRunning() bool { + if t.runningITS == nil { + return false + } for _, inst := range t.runningITS.Status.InstanceStatus { if inst.VolumeExpansion { return true @@ -252,6 +270,10 @@ func (t *componentStatusTransformer) hasVolumeExpansionRunning() bool { // hasFailedPod checks if the instance set has failed pod. func (t *componentStatusTransformer) hasFailedPod() (bool, appsv1alpha1.ComponentMessageMap) { + if t.runningITS == nil { + return false, nil + } + messages := appsv1alpha1.ComponentMessageMap{} // check InstanceFailure condition hasFailedPod := meta.IsStatusConditionTrue(t.runningITS.Status.Conditions, string(workloads.InstanceFailure)) @@ -322,80 +344,148 @@ func (t *componentStatusTransformer) updateComponentStatus(transCtx *componentTr } func (t *componentStatusTransformer) reconcileStatusCondition(transCtx *componentTransformContext) error { - return t.reconcileAvailableCondition(transCtx) -} - -func (t *componentStatusTransformer) reconcileAvailableCondition(transCtx *componentTransformContext) error { - policy := component.GetComponentAvailablePolicy(transCtx.CompDef) - if policy.WithPhases == nil && policy.WithRole == nil { - return nil - } - - var ( - comp = transCtx.Component - status, status1, status2 metav1.ConditionStatus - reason, reason1, reason2 string - message, message1, message2 string + return errors.Join( + t.reconcileAvailableCondition(transCtx), + t.reconcileProgressingCondition(transCtx), + t.reconcileHealthyCondition(transCtx), ) - if policy.WithPhases != nil { - status1, reason1, message1 = t.availableWithPhases(transCtx, comp, policy) - } - if policy.WithRole != nil { - status2, reason2, message2 = t.availableWithRole(transCtx, comp, policy) - } +} - // merge conditions - switch { - case policy.WithPhases != nil && policy.WithRole == nil: - status, reason, message = status1, reason1, message1 - case policy.WithPhases == nil && policy.WithRole != nil: - status, reason, message = status2, reason2, message2 - default: // both are not nil - if status1 != metav1.ConditionTrue { - status, reason, message = status1, reason1, message1 - } else { - status, reason, message = status2, reason2, message2 - } +func (t *componentStatusTransformer) checkNSetCondition( + eventRecorder record.EventRecorder, + conditionType string, + checker func() (status metav1.ConditionStatus, reason, message string, err error), +) error { + status, reason, message, err := checker() + if err != nil { + return err } - cond := metav1.Condition{ - Type: appsv1.ConditionTypeAvailable, + Type: conditionType, Status: status, - ObservedGeneration: comp.Generation, - LastTransitionTime: metav1.Now(), + ObservedGeneration: t.comp.Generation, Reason: reason, Message: message, } - if meta.SetStatusCondition(&comp.Status.Conditions, cond) { - transCtx.EventRecorder.Event(comp, corev1.EventTypeNormal, reason, message) + if meta.SetStatusCondition(&t.comp.Status.Conditions, cond) { + eventRecorder.Event(t.comp, corev1.EventTypeNormal, reason, message) } return nil } +func (t *componentStatusTransformer) reconcileProgressingCondition(transCtx *componentTransformContext) error { + return t.checkNSetCondition( + transCtx.EventRecorder, + appsv1.ComponentConditionProgressing, + func() (status metav1.ConditionStatus, reason string, message string, err error) { + if !t.isWorkloadUpdated() { + return metav1.ConditionTrue, "WorkloadNotUpdated", "observed workload's generation not matching component's", nil + } + + hasRunningScaleOut, _, err := t.hasScaleOutRunning(transCtx) + if err != nil { + return "", "", "", err + } + if hasRunningScaleOut { + return metav1.ConditionTrue, "ScaleOut", "component scale out is running", nil + } + + hasRunningVolumeExpansion := t.hasVolumeExpansionRunning() + if hasRunningVolumeExpansion { + return metav1.ConditionTrue, "VolumeExpansion", "component volume expansion is running", nil + } + + if !checkPostProvisionDone(transCtx) { + return metav1.ConditionTrue, "PostProvision", "component is running post-provision action", nil + } + + return metav1.ConditionFalse, "Completed", "", nil + }, + ) +} + +func (t *componentStatusTransformer) reconcileHealthyCondition(transCtx *componentTransformContext) error { + return t.checkNSetCondition( + transCtx.EventRecorder, + appsv1.ComponentConditionHealthy, + func() (status metav1.ConditionStatus, reason string, message string, err error) { + if t.runningITS == nil { + return metav1.ConditionFalse, "WorkloadNotExist", "waiting for workload to be created", nil + } + if !t.runningITS.IsInstancesReady() { + return metav1.ConditionFalse, "WorkloadNotReady", "some instances are not ready", nil + } + if !t.runningITS.IsRoleProbeDone() { + return metav1.ConditionFalse, "RoleProbeNotDone", "some instances do not have roles", nil + } + + _, hasFailedScaleOut, err := t.hasScaleOutRunning(transCtx) + if err != nil { + return "", "", "", err + } + if hasFailedScaleOut { + return metav1.ConditionFalse, "ScaleOutFailure", "component scale out has failure", nil + } + + return metav1.ConditionTrue, "Healthy", "component is healthy", nil + }, + ) +} + +func (t *componentStatusTransformer) reconcileAvailableCondition(transCtx *componentTransformContext) error { + policy := component.GetComponentAvailablePolicy(transCtx.CompDef) + if policy.WithPhases == nil && policy.WithRole == nil { + return nil + } + + return t.checkNSetCondition( + transCtx.EventRecorder, + appsv1.ComponentConditionAvailable, + func() (status metav1.ConditionStatus, reason string, message string, err error) { + if policy.WithPhases != nil { + status, message1 := t.availableWithPhases(transCtx, transCtx.Component, policy) + if status != metav1.ConditionTrue { + return status, "PhaseCheckFail", message1, nil + } + message += message1 + "; " + } + if policy.WithRole != nil { + status, message2 := t.availableWithRole(transCtx, transCtx.Component, policy) + if status != metav1.ConditionTrue { + return status, "RoleCheckFail", message2, nil + } + message += message2 + "; " + } + + return metav1.ConditionTrue, "Available", message, nil + }, + ) +} + func (t *componentStatusTransformer) availableWithPhases(_ *componentTransformContext, - comp *appsv1.Component, policy appsv1.ComponentAvailable) (metav1.ConditionStatus, string, string) { + comp *appsv1.Component, policy appsv1.ComponentAvailable) (metav1.ConditionStatus, string) { if comp.Status.Phase == "" { - return metav1.ConditionUnknown, "Unknown", "the component phase is unknown" + return metav1.ConditionUnknown, "the component phase is unknown" } - phases := sets.New[string](strings.Split(strings.ToLower(*policy.WithPhases), ",")...) + phases := sets.New(strings.Split(strings.ToLower(*policy.WithPhases), ",")...) if phases.Has(strings.ToLower(string(comp.Status.Phase))) { - return metav1.ConditionTrue, "Available", fmt.Sprintf("the component phase is %s", comp.Status.Phase) + return metav1.ConditionTrue, fmt.Sprintf("the component phase is %s", comp.Status.Phase) } - return metav1.ConditionFalse, "Unavailable", fmt.Sprintf("the component phase is %s", comp.Status.Phase) + return metav1.ConditionFalse, fmt.Sprintf("the component phase is %s", comp.Status.Phase) } func (t *componentStatusTransformer) availableWithRole(transCtx *componentTransformContext, - _ *appsv1.Component, policy appsv1.ComponentAvailable) (metav1.ConditionStatus, string, string) { + _ *appsv1.Component, policy appsv1.ComponentAvailable) (metav1.ConditionStatus, string) { its := transCtx.RunningWorkload if its == nil { - return metav1.ConditionFalse, "Unavailable", "the workload is not present" + return metav1.ConditionFalse, "the workload is not present" } for _, inst := range its.Status.InstanceStatus { if len(inst.Role) > 0 { if strings.EqualFold(inst.Role, *policy.WithRole) { - return metav1.ConditionTrue, "Available", fmt.Sprintf("the role %s is present", *policy.WithRole) + return metav1.ConditionTrue, fmt.Sprintf("the role %s is present", *policy.WithRole) } } } - return metav1.ConditionFalse, "Unavailable", fmt.Sprintf("the role %s is not present", *policy.WithRole) + return metav1.ConditionFalse, fmt.Sprintf("the role %s is not present", *policy.WithRole) } diff --git a/controllers/apps/component/transformer_component_status_test.go b/controllers/apps/component/transformer_component_status_test.go new file mode 100644 index 00000000000..6a0a7dd945e --- /dev/null +++ b/controllers/apps/component/transformer_component_status_test.go @@ -0,0 +1,466 @@ +/* +Copyright (C) 2022-2025 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package component + +import ( + "strconv" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/uuid" + "k8s.io/client-go/tools/record" + "k8s.io/utils/ptr" + + appsv1 "github.com/apecloud/kubeblocks/apis/apps/v1" + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1" + appsutil "github.com/apecloud/kubeblocks/controllers/apps/util" + "github.com/apecloud/kubeblocks/pkg/constant" + "github.com/apecloud/kubeblocks/pkg/controller/component" + "github.com/apecloud/kubeblocks/pkg/controller/model" +) + +var _ = Describe("component status transformer conditions", func() { + const ( + compDefName = "test-compdef-status" + clusterName = "test-cluster-status" + compName = "comp-status" + ) + + var ( + transCtx *componentTransformContext + transformer *componentStatusTransformer + comp *appsv1.Component + compDef *appsv1.ComponentDefinition + runningITS *workloads.InstanceSet + protoITS *workloads.InstanceSet + eventRecorder record.EventRecorder + ) + + newReadyITS := func(generation int64, replicas int32, roles []workloads.ReplicaRole) *workloads.InstanceSet { + its := &workloads.InstanceSet{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testCtx.DefaultNamespace, + Name: constant.GenerateWorkloadNamePattern(clusterName, compName), + Generation: generation, + Annotations: map[string]string{ + constant.KubeBlocksGenerationKey: strconv.FormatInt(comp.Generation, 10), + }, + }, + Spec: workloads.InstanceSetSpec{ + Replicas: ptr.To(replicas), + Roles: roles, + }, + Status: workloads.InstanceSetStatus{ + ObservedGeneration: generation, + Replicas: replicas, + ReadyReplicas: replicas, + UpdatedReplicas: replicas, + InitReplicas: replicas, + ReadyInitReplicas: replicas, + }, + } + if len(roles) > 0 { + for i := int32(0); i < replicas; i++ { + its.Status.InstanceStatus = append(its.Status.InstanceStatus, workloads.InstanceStatus{ + PodName: "pod-" + strconv.Itoa(int(i)), + Role: roles[i%int32(len(roles))].Name, + }) + } + } + return its + } + + BeforeEach(func() { + eventRecorder = record.NewFakeRecorder(100) + + compDef = &appsv1.ComponentDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: compDefName, + }, + Spec: appsv1.ComponentDefinitionSpec{}, + } + + comp = &appsv1.Component{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testCtx.DefaultNamespace, + Name: constant.GenerateClusterComponentName(clusterName, compName), + Generation: 1, + Labels: map[string]string{ + constant.AppManagedByLabelKey: constant.AppName, + constant.AppInstanceLabelKey: clusterName, + constant.KBAppComponentLabelKey: compName, + }, + Annotations: map[string]string{ + constant.KBAppClusterUIDKey: string(uuid.NewUUID()), + }, + }, + Spec: appsv1.ComponentSpec{ + CompDef: compDef.Name, + Replicas: 3, + }, + Status: appsv1.ComponentStatus{ + Phase: appsv1.RunningComponentPhase, + }, + } + + runningITS = newReadyITS(1, 3, nil) + protoITS = runningITS.DeepCopy() + + reader := &appsutil.MockReader{ + Objects: []client.Object{compDef, comp}, + } + graphCli := model.NewGraphClient(reader) + + transCtx = &componentTransformContext{ + Context: ctx, + Client: graphCli, + EventRecorder: eventRecorder, + Logger: logger, + CompDef: compDef, + Component: comp, + ComponentOrig: comp.DeepCopy(), + SynthesizeComponent: &component.SynthesizedComponent{ + Namespace: testCtx.DefaultNamespace, + ClusterName: clusterName, + Name: compName, + }, + RunningWorkload: runningITS, + ProtoWorkload: protoITS, + } + + transformer = &componentStatusTransformer{} + transformer.comp = comp + transformer.runningITS = runningITS + transformer.protoITS = protoITS + transformer.synthesizeComp = transCtx.SynthesizeComponent + }) + + Context("reconcileHealthyCondition", func() { + It("should be unhealthy when runningITS is nil", func() { + transformer.runningITS = nil + err := transformer.reconcileHealthyCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionHealthy) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).Should(Equal("WorkloadNotExist")) + }) + + It("should be unhealthy when instances are not ready", func() { + runningITS.Status.ReadyReplicas = 1 + err := transformer.reconcileHealthyCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionHealthy) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).Should(Equal("WorkloadNotReady")) + }) + + It("should be unhealthy when role probe not done", func() { + roles := []workloads.ReplicaRole{{Name: "leader"}, {Name: "follower"}} + runningITS.Spec.Roles = roles + // no instance status with roles -> role probe not done + runningITS.Status.InstanceStatus = nil + err := transformer.reconcileHealthyCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionHealthy) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).Should(Equal("RoleProbeNotDone")) + }) + + It("should be healthy when everything is ready (no roles)", func() { + err := transformer.reconcileHealthyCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionHealthy) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).Should(Equal("Healthy")) + }) + + It("should be healthy when everything is ready (with roles)", func() { + roles := []workloads.ReplicaRole{{Name: "leader"}, {Name: "follower"}} + its := newReadyITS(1, 3, roles) + transformer.runningITS = its + transCtx.RunningWorkload = its + err := transformer.reconcileHealthyCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionHealthy) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).Should(Equal("Healthy")) + }) + }) + + Context("reconcileProgressingCondition", func() { + It("should not be progressing when nothing is in progress", func() { + err := transformer.reconcileProgressingCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionProgressing) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).Should(Equal("Completed")) + }) + + It("should be progressing when workload generation not matching", func() { + runningITS.Annotations[constant.KubeBlocksGenerationKey] = "999" + err := transformer.reconcileProgressingCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionProgressing) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).Should(Equal("WorkloadNotUpdated")) + }) + + It("should be progressing when volume expansion is running", func() { + runningITS.Status.InstanceStatus = []workloads.InstanceStatus{ + {PodName: "pod-0", VolumeExpansion: true}, + } + err := transformer.reconcileProgressingCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionProgressing) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).Should(Equal("VolumeExpansion")) + }) + + It("should be progressing when post-provision is not done", func() { + transCtx.SynthesizeComponent.LifecycleActions = component.SynthesizedLifecycleActions{ + ComponentLifecycleActions: &appsv1.ComponentLifecycleActions{ + PostProvision: &appsv1.Action{ + Exec: &appsv1.ExecAction{ + Command: []string{"echo", "hello"}, + }, + }, + }, + } + + err := transformer.reconcileProgressingCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionProgressing) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).Should(Equal("PostProvision")) + + // set post-provision-done annotation + comp.Annotations[kbCompPostProvisionDoneKey] = time.Now().Format(time.RFC3339Nano) + err = transformer.reconcileProgressingCondition(transCtx) + Expect(err).Should(BeNil()) + cond = meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionProgressing) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).Should(Equal("Completed")) + }) + + It("should be progressing when scale out is running", func() { + err := component.NewReplicasStatus(protoITS, []string{"pod-3"}, true, false) + Expect(err).Should(BeNil()) + transformer.protoITS = protoITS + + err = transformer.reconcileProgressingCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionProgressing) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).Should(Equal("ScaleOut")) + + // when scale out is done + // TODO + }) + }) + + Context("reconcileAvailableCondition", func() { + It("should be available when no available policy is defined", func() { + comp.Status.Phase = appsv1.RunningComponentPhase + err := transformer.reconcileAvailableCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionAvailable) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).Should(Equal("Available")) + }) + + Context("WithPhases policy", func() { + BeforeEach(func() { + compDef.Spec.Available = &appsv1.ComponentAvailable{ + WithPhases: ptr.To("Running,Updating"), + } + }) + + It("should be available when phase matches", func() { + comp.Status.Phase = appsv1.RunningComponentPhase + err := transformer.reconcileAvailableCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionAvailable) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).Should(Equal("Available")) + }) + + It("should not be available when phase does not match", func() { + comp.Status.Phase = appsv1.FailedComponentPhase + err := transformer.reconcileAvailableCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionAvailable) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).Should(Equal("PhaseCheckFail")) + }) + + It("should be unknown when phase is empty", func() { + comp.Status.Phase = "" + err := transformer.reconcileAvailableCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionAvailable) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionUnknown)) + }) + }) + + Context("WithRole policy", func() { + BeforeEach(func() { + compDef.Spec.Available = &appsv1.ComponentAvailable{ + WithRole: ptr.To("leader"), + } + }) + + It("should be available when role is present", func() { + runningITS.Status.InstanceStatus = []workloads.InstanceStatus{ + {PodName: "pod-0", Role: "leader"}, + {PodName: "pod-1", Role: "follower"}, + } + transCtx.RunningWorkload = runningITS + err := transformer.reconcileAvailableCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionAvailable) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).Should(Equal("Available")) + }) + + It("should not be available when role is not present", func() { + runningITS.Status.InstanceStatus = []workloads.InstanceStatus{ + {PodName: "pod-0", Role: "follower"}, + } + transCtx.RunningWorkload = runningITS + err := transformer.reconcileAvailableCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionAvailable) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).Should(Equal("RoleCheckFail")) + }) + + It("should not be available when workload is nil", func() { + transCtx.RunningWorkload = nil + err := transformer.reconcileAvailableCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionAvailable) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).Should(Equal("RoleCheckFail")) + }) + }) + + Context("WithPhases and WithRole combined policy", func() { + BeforeEach(func() { + compDef.Spec.Available = &appsv1.ComponentAvailable{ + WithPhases: ptr.To("Running"), + WithRole: ptr.To("leader"), + } + }) + + It("should be available when both checks pass", func() { + comp.Status.Phase = appsv1.RunningComponentPhase + runningITS.Status.InstanceStatus = []workloads.InstanceStatus{ + {PodName: "pod-0", Role: "leader"}, + } + transCtx.RunningWorkload = runningITS + err := transformer.reconcileAvailableCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionAvailable) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionTrue)) + }) + + It("should not be available when phase check fails", func() { + comp.Status.Phase = appsv1.FailedComponentPhase + runningITS.Status.InstanceStatus = []workloads.InstanceStatus{ + {PodName: "pod-0", Role: "leader"}, + } + transCtx.RunningWorkload = runningITS + err := transformer.reconcileAvailableCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionAvailable) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).Should(Equal("PhaseCheckFail")) + }) + + It("should not be available when role check fails", func() { + comp.Status.Phase = appsv1.RunningComponentPhase + runningITS.Status.InstanceStatus = []workloads.InstanceStatus{ + {PodName: "pod-0", Role: "follower"}, + } + transCtx.RunningWorkload = runningITS + err := transformer.reconcileAvailableCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionAvailable) + Expect(cond).ShouldNot(BeNil()) + Expect(cond.Status).Should(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).Should(Equal("RoleCheckFail")) + }) + }) + + It("should skip setting condition when neither WithPhases nor WithRole is set", func() { + compDef.Spec.Available = &appsv1.ComponentAvailable{} + err := transformer.reconcileAvailableCondition(transCtx) + Expect(err).Should(BeNil()) + + cond := meta.FindStatusCondition(comp.Status.Conditions, appsv1.ComponentConditionAvailable) + Expect(cond).Should(BeNil()) + }) + }) +}) diff --git a/controllers/apps/component/utils.go b/controllers/apps/component/utils.go index 7d85e972f67..cec58ed1ebb 100644 --- a/controllers/apps/component/utils.go +++ b/controllers/apps/component/utils.go @@ -51,7 +51,7 @@ func setProvisioningStartedCondition(conditions *[]metav1.Condition, clusterName // newProvisioningStartedCondition creates the provisioning started condition in cluster conditions. func newProvisioningStartedCondition(clusterName string, clusterGeneration int64) metav1.Condition { return metav1.Condition{ - Type: appsv1.ConditionTypeProvisioningStarted, + Type: appsv1.ComponentConditionProvisioningStarted, ObservedGeneration: clusterGeneration, Status: metav1.ConditionTrue, Message: fmt.Sprintf("The operator has started the provisioning of Cluster: %s", clusterName), @@ -73,7 +73,7 @@ func getConditionReasonWithError(defaultReason string, err error) string { // newApplyResourcesCondition creates a condition when applied resources succeed. func newFailedProvisioningStartedCondition(err error) metav1.Condition { return metav1.Condition{ - Type: appsv1.ConditionTypeProvisioningStarted, + Type: appsv1.ComponentConditionProvisioningStarted, Status: metav1.ConditionFalse, Message: err.Error(), Reason: getConditionReasonWithError(reasonPreCheckFailed, err), diff --git a/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml b/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml index 4bb2bddfb74..01062289792 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml @@ -110,6 +110,7 @@ spec: withPhases: description: |- Specifies the phases that the component will go through to be considered available. + Multiple phases are separated by comma. This field is immutable once set. @@ -119,9 +120,6 @@ spec: Specifies the strategies for determining whether the component is available based on the available probe. - If specified, it will take precedence over the WithPhases and WithRole fields. - - This field is immutable once set. properties: condition: diff --git a/docs/developer_docs/api-reference/cluster.md b/docs/developer_docs/api-reference/cluster.md index a847cb08f6f..cc95e6b757d 100644 --- a/docs/developer_docs/api-reference/cluster.md +++ b/docs/developer_docs/api-reference/cluster.md @@ -4793,6 +4793,9 @@ VarOption

ComponentAvailable defines the strategies for determining whether the component is available.

+

If both WithPhases and WithRole are specified, the component will be considered +unavailable if any of them fail. +If WithProbe is specified, WithPhases and WithRole fields are ignored.

@@ -4811,7 +4814,8 @@ string @@ -4840,7 +4844,6 @@ ComponentAvailableWithProbe diff --git a/pkg/controller/component/available.go b/pkg/controller/component/available.go index 7b2ccf5f713..81448e10254 100644 --- a/pkg/controller/component/available.go +++ b/pkg/controller/component/available.go @@ -109,14 +109,14 @@ func (h *AvailableEventHandler) available(ctx context.Context, cli client.Client func (h *AvailableEventHandler) unavailable(ctx context.Context, cli client.Client, recorder record.EventRecorder, compCopy, comp *appsv1.Component, message string) error { - return h.status(ctx, cli, recorder, compCopy, comp, metav1.ConditionFalse, "Unavailable", message) + return h.status(ctx, cli, recorder, compCopy, comp, metav1.ConditionFalse, "ProbeCheckFail", message) } func (h *AvailableEventHandler) status(ctx context.Context, cli client.Client, recorder record.EventRecorder, compCopy, comp *appsv1.Component, status metav1.ConditionStatus, reason, message string) error { var ( cond = metav1.Condition{ - Type: appsv1.ConditionTypeAvailable, + Type: appsv1.ComponentConditionAvailable, Status: status, ObservedGeneration: comp.Generation, // TODO: ??? LastTransitionTime: metav1.Now(),
(Optional) -

Specifies the phases that the component will go through to be considered available.

+

Specifies the phases that the component will go through to be considered available. +Multiple phases are separated by comma.

This field is immutable once set.

(Optional)

Specifies the strategies for determining whether the component is available based on the available probe.

-

If specified, it will take precedence over the WithPhases and WithRole fields.

This field is immutable once set.