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
|
(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.
|
@@ -4840,7 +4844,6 @@ ComponentAvailableWithProbe
(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.
|
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(),