diff --git a/apis/apps/v1alpha1/cluster_types.go b/apis/apps/v1alpha1/cluster_types.go index 549ac293327..3957f396586 100644 --- a/apis/apps/v1alpha1/cluster_types.go +++ b/apis/apps/v1alpha1/cluster_types.go @@ -588,8 +588,9 @@ type ClusterComponentSpec struct { // +optional ComponentDefRef string `json:"componentDefRef,omitempty"` - // References the name of a ComponentDefinition object. - // The ComponentDefinition specifies the behavior and characteristics of the Component. + // Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + // custom resource (CR) that defines the Component's characteristics and behavior. + // // If both `componentDefRef` and `componentDef` are provided, // the `componentDef` will take precedence over `componentDefRef`. // diff --git a/apis/apps/v1alpha1/type.go b/apis/apps/v1alpha1/type.go index 886a47f20e1..e6dfa80e07f 100644 --- a/apis/apps/v1alpha1/type.go +++ b/apis/apps/v1alpha1/type.go @@ -1096,7 +1096,9 @@ type ClusterVars struct { // ClusterObjectReference defines information to let you locate the referenced object inside the same Cluster. type ClusterObjectReference struct { - // CompDef specifies the definition used by the component that the referent object resident in. + // Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + // custom resource (CR) used by the component that the referent object resident in. + // // If not specified, the component itself will be used. // // +optional diff --git a/config/crd/bases/apps.kubeblocks.io_clusters.yaml b/config/crd/bases/apps.kubeblocks.io_clusters.yaml index 696a0ca9ca2..ef56c56d1f0 100644 --- a/config/crd/bases/apps.kubeblocks.io_clusters.yaml +++ b/config/crd/bases/apps.kubeblocks.io_clusters.yaml @@ -405,8 +405,10 @@ spec: type: object componentDef: description: |- - References the name of a ComponentDefinition object. - The ComponentDefinition specifies the behavior and characteristics of the Component. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) that defines the Component's characteristics and behavior. + + If both `componentDefRef` and `componentDef` are provided, the `componentDef` will take precedence over `componentDefRef`. maxLength: 64 @@ -9525,8 +9527,10 @@ spec: type: object componentDef: description: |- - References the name of a ComponentDefinition object. - The ComponentDefinition specifies the behavior and characteristics of the Component. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) that defines the Component's characteristics and behavior. + + If both `componentDefRef` and `componentDef` are provided, the `componentDef` will take precedence over `componentDefRef`. maxLength: 64 diff --git a/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml b/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml index bd9064e8e9f..7ad3e446840 100644 --- a/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml +++ b/config/crd/bases/apps.kubeblocks.io_componentdefinitions.yaml @@ -12493,7 +12493,10 @@ spec: properties: compDef: description: |- - CompDef specifies the definition used by the component that the referent object resident in. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) used by the component that the referent object resident in. + + If not specified, the component itself will be used. type: string componentName: @@ -12644,7 +12647,10 @@ spec: properties: compDef: description: |- - CompDef specifies the definition used by the component that the referent object resident in. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) used by the component that the referent object resident in. + + If not specified, the component itself will be used. type: string multipleClusterObjectOption: @@ -12723,7 +12729,10 @@ spec: properties: compDef: description: |- - CompDef specifies the definition used by the component that the referent object resident in. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) used by the component that the referent object resident in. + + If not specified, the component itself will be used. type: string container: @@ -12832,7 +12841,10 @@ spec: properties: compDef: description: |- - CompDef specifies the definition used by the component that the referent object resident in. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) used by the component that the referent object resident in. + + If not specified, the component itself will be used. type: string endpoint: @@ -12932,7 +12944,10 @@ spec: properties: compDef: description: |- - CompDef specifies the definition used by the component that the referent object resident in. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) used by the component that the referent object resident in. + + If not specified, the component itself will be used. type: string host: diff --git a/controllers/apps/clusterdefinition_controller.go b/controllers/apps/clusterdefinition_controller.go index 242b88f37a9..27d6779f52f 100644 --- a/controllers/apps/clusterdefinition_controller.go +++ b/controllers/apps/clusterdefinition_controller.go @@ -191,6 +191,14 @@ func (r *ClusterDefinitionReconciler) validateTopology(rctx intctrlutil.RequestC return err } } + + // validate topology reference component definitions name pattern + for _, comp := range topology.Components { + if err := component.ValidateCompDefRegexp(comp.CompDef); err != nil { + return fmt.Errorf("invalid component definition reference pattern: %s", comp.CompDef) + } + } + compDefs, err := r.loadTopologyCompDefs(rctx.Ctx, topology) if err != nil { return err diff --git a/controllers/apps/componentdefinition_controller.go b/controllers/apps/componentdefinition_controller.go index 34da6c8de5e..7dd158be74c 100644 --- a/controllers/apps/componentdefinition_controller.go +++ b/controllers/apps/componentdefinition_controller.go @@ -27,6 +27,7 @@ import ( "reflect" "strings" + "github.com/pkg/errors" "golang.org/x/exp/slices" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" @@ -212,6 +213,38 @@ func (r *ComponentDefinitionReconciler) validateRuntime(cli client.Client, rctx func (r *ComponentDefinitionReconciler) validateVars(cli client.Client, rctx intctrlutil.RequestCtx, cmpd *appsv1alpha1.ComponentDefinition) error { + if !checkUniqueItemWithValue(cmpd.Spec.Vars, "Name", nil) { + return fmt.Errorf("duplicate names of component vars are not allowed") + } + + // validate the reference to component definition name pattern + var compDef string + for _, cVar := range cmpd.Spec.Vars { + if cVar.ValueFrom == nil { + continue + } + switch { + case cVar.ValueFrom.HostNetworkVarRef != nil: + compDef = cVar.ValueFrom.HostNetworkVarRef.CompDef + case cVar.ValueFrom.ServiceVarRef != nil: + compDef = cVar.ValueFrom.ServiceVarRef.CompDef + case cVar.ValueFrom.ServiceRefVarRef != nil: + compDef = cVar.ValueFrom.ServiceRefVarRef.CompDef + case cVar.ValueFrom.ComponentVarRef != nil: + compDef = cVar.ValueFrom.ComponentVarRef.CompDef + case cVar.ValueFrom.CredentialVarRef != nil: + compDef = cVar.ValueFrom.CredentialVarRef.CompDef + default: + continue + } + + if len(compDef) == 0 { + continue + } + if err := component.ValidateCompDefRegexp(compDef); err != nil { + return errors.Wrapf(err, "invalid reference to component definition name pattern: %s", compDef) + } + } return nil } @@ -419,26 +452,19 @@ func listCompDefinitionsWithPattern(ctx context.Context, cli client.Reader, name return nil, err } compDefsFullyMatched := make([]*appsv1alpha1.ComponentDefinition, 0) - compDefsPrefixMatched := make([]*appsv1alpha1.ComponentDefinition, 0) - compDefsRegexMatched := make([]*appsv1alpha1.ComponentDefinition, 0) + compDefsPatternMatched := make([]*appsv1alpha1.ComponentDefinition, 0) for i, item := range compDefList.Items { if item.Name == namePattern { compDefsFullyMatched = append(compDefsFullyMatched, &compDefList.Items[i]) } - if strings.HasPrefix(item.Name, namePattern) { - compDefsPrefixMatched = append(compDefsPrefixMatched, &compDefList.Items[i]) - } if component.CompDefMatched(item.Name, namePattern) { - compDefsRegexMatched = append(compDefsRegexMatched, &compDefList.Items[i]) + compDefsPatternMatched = append(compDefsPatternMatched, &compDefList.Items[i]) } } if len(compDefsFullyMatched) > 0 { return compDefsFullyMatched, nil } - if len(compDefsPrefixMatched) > 0 { - return compDefsPrefixMatched, nil - } - return compDefsRegexMatched, nil + return compDefsPatternMatched, nil } func checkUniqueItemWithValue(slice any, fieldName string, val any) bool { diff --git a/controllers/apps/componentdefinition_controller_test.go b/controllers/apps/componentdefinition_controller_test.go index 97916a3000b..05fdc44ed5d 100644 --- a/controllers/apps/componentdefinition_controller_test.go +++ b/controllers/apps/componentdefinition_controller_test.go @@ -336,6 +336,75 @@ var _ = Describe("ComponentDefinition Controller", func() { }) }) + Context("vars", func() { + It("ok", func() { + By("create a ComponentDefinition obj") + componentDefObj := testapps.NewComponentDefinitionFactory(componentDefName). + SetRuntime(nil). + AddVar(appsv1alpha1.EnvVar{ + Name: "VAR1", + Value: "value1", + }). + Create(&testCtx).GetObject() + + checkObjectStatus(componentDefObj, appsv1alpha1.AvailablePhase) + }) + + It("duplicate vars name", func() { + By("create a ComponentDefinition obj") + componentDefObj := testapps.NewComponentDefinitionFactory(componentDefName). + SetRuntime(nil). + AddVar(appsv1alpha1.EnvVar{ + Name: "VAR1", + Value: "value1", + }). + AddVar(appsv1alpha1.EnvVar{ + Name: "VAR1", + Value: "value2", + }). + Create(&testCtx).GetObject() + checkObjectStatus(componentDefObj, appsv1alpha1.UnavailablePhase) + }) + + It("valid var component definition name pattern", func() { + By("create a ComponentDefinition obj") + componentDefObj := testapps.NewComponentDefinitionFactory(componentDefName). + SetRuntime(nil). + AddVar(appsv1alpha1.EnvVar{ + Name: "VAR1", + ValueFrom: &appsv1alpha1.VarSource{ + ServiceRefVarRef: &appsv1alpha1.ServiceRefVarSelector{ + ClusterObjectReference: appsv1alpha1.ClusterObjectReference{ + Name: "service", + CompDef: "valid", + }, + }, + }, + }). + Create(&testCtx).GetObject() + checkObjectStatus(componentDefObj, appsv1alpha1.AvailablePhase) + }) + + It("invalid var component definition name pattern", func() { + By("create a ComponentDefinition obj") + componentDefObj := testapps.NewComponentDefinitionFactory(componentDefName). + SetRuntime(nil). + AddVar(appsv1alpha1.EnvVar{ + Name: "VAR1", + ValueFrom: &appsv1alpha1.VarSource{ + ServiceVarRef: &appsv1alpha1.ServiceVarSelector{ + ClusterObjectReference: appsv1alpha1.ClusterObjectReference{ + Name: "service", + CompDef: "(invalid", + }, + }, + }, + }). + Create(&testCtx).GetObject() + checkObjectStatus(componentDefObj, appsv1alpha1.UnavailablePhase) + }) + }) + Context("immutable", func() { newCmpdFn := func(processor func(*testapps.MockComponentDefinitionFactory)) *appsv1alpha1.ComponentDefinition { By("create a ComponentDefinition obj") diff --git a/controllers/apps/componentversion_controller.go b/controllers/apps/componentversion_controller.go index 04e8da26e0a..2ff2f3f3bc7 100644 --- a/controllers/apps/componentversion_controller.go +++ b/controllers/apps/componentversion_controller.go @@ -26,6 +26,7 @@ import ( "slices" "strings" + "github.com/pkg/errors" "golang.org/x/exp/maps" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" @@ -141,6 +142,13 @@ func (r *ComponentVersionReconciler) reconcile(rctx intctrlutil.RequestCtx, // return intctrlutil.Reconciled() // } + if err = validateCompatibilityRulesCompDef(compVersion); err != nil { + if err1 := r.unavailable(r.Client, rctx, compVersion, err); err1 != nil { + return intctrlutil.CheckedRequeueWithError(err1, rctx.Log, "") + } + return intctrlutil.CheckedRequeueWithError(err, rctx.Log, "") + } + releaseToCompDefinitions, err := r.buildReleaseToCompDefinitionMapping(r.Client, rctx, compVersion) if err != nil { return intctrlutil.CheckedRequeueWithError(err, rctx.Log, "") @@ -340,12 +348,24 @@ func (r *ComponentVersionReconciler) validateImages(release appsv1alpha1.Compone return nil } +// validateCompDef validates the reference component definition name pattern defined in compatibility rules. +func validateCompatibilityRulesCompDef(compVersion *appsv1alpha1.ComponentVersion) error { + for _, rule := range compVersion.Spec.CompatibilityRules { + for _, compDefName := range rule.CompDefs { + if err := component.ValidateCompDefRegexp(compDefName); err != nil { + return errors.Wrapf(err, "invalid reference to component definition name pattern: %s in compatibility rules", compDefName) + } + } + } + return nil +} + // resolveCompDefinitionNServiceVersion resolves and returns the specific component definition object and the service version supported. -func resolveCompDefinitionNServiceVersion(ctx context.Context, cli client.Reader, compDefName, serviceVersion string) (*appsv1alpha1.ComponentDefinition, string, error) { +func resolveCompDefinitionNServiceVersion(ctx context.Context, cli client.Reader, compDefNamePattern, serviceVersion string) (*appsv1alpha1.ComponentDefinition, string, error) { var ( compDef *appsv1alpha1.ComponentDefinition ) - compDefs, err := listCompDefinitionsWithPattern(ctx, cli, compDefName) + compDefs, err := listCompDefinitionsWithPattern(ctx, cli, compDefNamePattern) if err != nil { return compDef, serviceVersion, err } @@ -368,7 +388,7 @@ func resolveCompDefinitionNServiceVersion(ctx context.Context, cli client.Reader // component definitions that support the service version compatibleCompDefs := serviceVersionToCompDefs[serviceVersion] if len(compatibleCompDefs) == 0 { - return compDef, serviceVersion, fmt.Errorf("no matched component definition found: %s", compDefName) + return compDef, serviceVersion, fmt.Errorf("no matched component definition found: %s", compDefNamePattern) } // choose the latest one diff --git a/controllers/apps/componentversion_controller_test.go b/controllers/apps/componentversion_controller_test.go index 16e1a063d4a..d0ab6a025d4 100644 --- a/controllers/apps/componentversion_controller_test.go +++ b/controllers/apps/componentversion_controller_test.go @@ -235,6 +235,36 @@ var _ = Describe("ComponentVersion Controller", func() { })).Should(Succeed()) }) + It("update component definition with valid regexp", func() { + By("update component version to reference a valid regexp component definition") + compVersionKey := client.ObjectKeyFromObject(compVersionObj) + Eventually(testapps.GetAndChangeObj(&testCtx, compVersionKey, func(compVersion *appsv1alpha1.ComponentVersion) { + compVersion.Spec.CompatibilityRules[1].CompDefs = []string{testapps.CompDefName("^v3$")} + })).Should(Succeed()) + + By("checking the object available") + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(compVersionObj), + func(g Gomega, cmpv *appsv1alpha1.ComponentVersion) { + g.Expect(cmpv.Status.ObservedGeneration).Should(Equal(cmpv.GetGeneration())) + g.Expect(cmpv.Status.Phase).Should(Equal(appsv1alpha1.AvailablePhase)) + })).Should(Succeed()) + }) + + It("update component definition with invalid regexp", func() { + By("update component version to reference an invalid regexp component definition") + compVersionKey := client.ObjectKeyFromObject(compVersionObj) + Eventually(testapps.GetAndChangeObj(&testCtx, compVersionKey, func(compVersion *appsv1alpha1.ComponentVersion) { + compVersion.Spec.CompatibilityRules[1].CompDefs = []string{testapps.CompDefName("(invalid-v3")} + })).Should(Succeed()) + + By("checking the object unavailable") + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(compVersionObj), + func(g Gomega, cmpv *appsv1alpha1.ComponentVersion) { + g.Expect(cmpv.Status.ObservedGeneration).Should(Equal(cmpv.GetGeneration())) + g.Expect(cmpv.Status.Phase).Should(Equal(appsv1alpha1.UnavailablePhase)) + })).Should(Succeed()) + }) + It("delete component definition", func() { By("update component version to delete definition v1.*") compVersionKey := client.ObjectKeyFromObject(compVersionObj) @@ -494,6 +524,66 @@ var _ = Describe("ComponentVersion Controller", func() { updateNCheckCompDefinitionImages(compDef, resolvedServiceVersion, "r3", "r2") }) + It("regular expression match definition", func() { + By("with definition exact regex and service version 1") + compDef, resolvedServiceVersion, err := resolveCompDefinitionNServiceVersion(testCtx.Ctx, testCtx.Cli, testapps.CompDefNameWithExactRegex("v2.0"), testapps.ServiceVersion("v1")) + Expect(err).Should(Succeed()) + Expect(compDef.Name).Should(Equal(testapps.CompDefName("v2.0"))) + Expect(resolvedServiceVersion).Should(Equal(testapps.ServiceVersion("v1"))) + updateNCheckCompDefinitionImages(compDef, resolvedServiceVersion, "r4", "r4") + + By("with definition exact regex and service version v2") + compDef, resolvedServiceVersion, err = resolveCompDefinitionNServiceVersion(testCtx.Ctx, testCtx.Cli, testapps.CompDefNameWithExactRegex("v2.0"), testapps.ServiceVersion("v2")) + Expect(err).Should(Succeed()) + Expect(compDef.Name).Should(Equal(testapps.CompDefName("v2.0"))) + Expect(resolvedServiceVersion).Should(Equal(testapps.ServiceVersion("v2"))) + updateNCheckCompDefinitionImages(compDef, resolvedServiceVersion, "r3", "r2") + + By("with definition exact regex and service version v3") + compDef, resolvedServiceVersion, err = resolveCompDefinitionNServiceVersion(testCtx.Ctx, testCtx.Cli, testapps.CompDefNameWithExactRegex("v3.0"), testapps.ServiceVersion("v3")) + Expect(err).Should(Succeed()) + Expect(compDef.Name).Should(Equal(testapps.CompDefName("v3.0"))) + Expect(resolvedServiceVersion).Should(Equal(testapps.ServiceVersion("v3"))) + updateNCheckCompDefinitionImages(compDef, resolvedServiceVersion, "r5", "r5") + + By("with definition v1 fuzzy regex and service version v0") + compDef, resolvedServiceVersion, err = resolveCompDefinitionNServiceVersion(testCtx.Ctx, testCtx.Cli, testapps.CompDefNameWithFuzzyRegex("v1"), testapps.ServiceVersion("v1")) + Expect(err).Should(Succeed()) + Expect(compDef.Name).Should(Equal(testapps.CompDefName("v1.1"))) + Expect(resolvedServiceVersion).Should(Equal(testapps.ServiceVersion("v1"))) + updateNCheckCompDefinitionImages(compDef, resolvedServiceVersion, "r4", "r4") + + By("with definition v2 fuzzy regex and service version v1") + compDef, resolvedServiceVersion, err = resolveCompDefinitionNServiceVersion(testCtx.Ctx, testCtx.Cli, testapps.CompDefNameWithFuzzyRegex("v2"), testapps.ServiceVersion("v2")) + Expect(err).Should(Succeed()) + Expect(compDef.Name).Should(Equal(testapps.CompDefName("v2.0"))) + Expect(resolvedServiceVersion).Should(Equal(testapps.ServiceVersion("v2"))) + updateNCheckCompDefinitionImages(compDef, resolvedServiceVersion, "r3", "r2") + }) + + It("regular expression match definition and w/o service version", func() { + By("with definition regex") + compDef, resolvedServiceVersion, err := resolveCompDefinitionNServiceVersion(testCtx.Ctx, testCtx.Cli, "^"+testapps.CompDefinitionName, "") + Expect(err).Should(Succeed()) + Expect(compDef.Name).Should(Equal(testapps.CompDefName("v3.0"))) + Expect(resolvedServiceVersion).Should(Equal(testapps.ServiceVersion("v3"))) + updateNCheckCompDefinitionImages(compDef, resolvedServiceVersion, "r5", "r5") + + By("with definition v1 regex") + compDef, resolvedServiceVersion, err = resolveCompDefinitionNServiceVersion(testCtx.Ctx, testCtx.Cli, testapps.CompDefNameWithFuzzyRegex("v1"), "") + Expect(err).Should(Succeed()) + Expect(compDef.Name).Should(Equal(testapps.CompDefName("v1.1"))) + Expect(resolvedServiceVersion).Should(Equal(testapps.ServiceVersion("v2"))) + updateNCheckCompDefinitionImages(compDef, resolvedServiceVersion, "r3", "r2") + + By("with definition v2 regex") + compDef, resolvedServiceVersion, err = resolveCompDefinitionNServiceVersion(testCtx.Ctx, testCtx.Cli, testapps.CompDefNameWithFuzzyRegex("v2"), "") + Expect(err).Should(Succeed()) + Expect(compDef.Name).Should(Equal(testapps.CompDefName("v2.0"))) + Expect(resolvedServiceVersion).Should(Equal(testapps.ServiceVersion("v2"))) + updateNCheckCompDefinitionImages(compDef, resolvedServiceVersion, "r3", "r2") + }) + It("match from definition", func() { By("with definition v1.0 and service version v0") compDef, resolvedServiceVersion, err := resolveCompDefinitionNServiceVersion(testCtx.Ctx, testCtx.Cli, testapps.CompDefName("v1.0"), testapps.ServiceVersion("v0")) diff --git a/controllers/apps/transformer_cluster_load_resources.go b/controllers/apps/transformer_cluster_load_resources.go index a7c093281a4..f79a524f750 100644 --- a/controllers/apps/transformer_cluster_load_resources.go +++ b/controllers/apps/transformer_cluster_load_resources.go @@ -22,9 +22,11 @@ package apps import ( "fmt" + "github.com/pkg/errors" "k8s.io/apimachinery/pkg/types" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/pkg/controller/component" "github.com/apecloud/kubeblocks/pkg/controller/graph" "github.com/apecloud/kubeblocks/pkg/generics" ) @@ -64,14 +66,27 @@ func (t *clusterLoadRefResourcesTransformer) Transform(ctx graph.TransformContex } func (t *clusterLoadRefResourcesTransformer) apiValidation(cluster *appsv1alpha1.Cluster) error { - if withClusterTopology(cluster) || - withClusterUserDefined(cluster) || - withClusterLegacyDefinition(cluster) || - withClusterSimplifiedAPI(cluster) { - return nil + if !withClusterTopology(cluster) && + !withClusterUserDefined(cluster) && + !withClusterLegacyDefinition(cluster) && + !withClusterSimplifiedAPI(cluster) { + return fmt.Errorf("cluster API validate error, clusterDef: %s, topology: %s, comps: %d, legacy comps: %d, simplified API: %v", + cluster.Spec.ClusterDefRef, cluster.Spec.Topology, clusterCompCnt(cluster), legacyClusterCompCnt(cluster), withClusterSimplifiedAPI(cluster)) + } + + for _, compSpec := range cluster.Spec.ComponentSpecs { + if err := validateCompDef(&compSpec); err != nil { + return err + } + } + + for _, shardingSpec := range cluster.Spec.ShardingSpecs { + if err := validateCompDef(&shardingSpec.Template); err != nil { + return err + } } - return fmt.Errorf("cluster API validate error, clusterDef: %s, topology: %s, comps: %d, legacy comps: %d, simplified API: %v", - cluster.Spec.ClusterDefRef, cluster.Spec.Topology, clusterCompCnt(cluster), legacyClusterCompCnt(cluster), withClusterSimplifiedAPI(cluster)) + + return nil } func (t *clusterLoadRefResourcesTransformer) checkNUpdateClusterTopology(transCtx *clusterTransformContext, cluster *appsv1alpha1.Cluster) error { @@ -95,6 +110,16 @@ func (t *clusterLoadRefResourcesTransformer) checkNUpdateClusterTopology(transCt return nil } +func validateCompDef(compSpec *appsv1alpha1.ClusterComponentSpec) error { + if len(compSpec.ComponentDef) == 0 { + return nil + } + if err := component.ValidateCompDefRegexp(compSpec.ComponentDef); err != nil { + return errors.Wrapf(err, "invalid reference component definition name pattern: %s", compSpec.ComponentDef) + } + return nil +} + func loadNCheckClusterDefinition(transCtx *clusterTransformContext, cluster *appsv1alpha1.Cluster) error { var cd *appsv1alpha1.ClusterDefinition if len(cluster.Spec.ClusterDefRef) > 0 { diff --git a/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml b/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml index 696a0ca9ca2..ef56c56d1f0 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml @@ -405,8 +405,10 @@ spec: type: object componentDef: description: |- - References the name of a ComponentDefinition object. - The ComponentDefinition specifies the behavior and characteristics of the Component. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) that defines the Component's characteristics and behavior. + + If both `componentDefRef` and `componentDef` are provided, the `componentDef` will take precedence over `componentDefRef`. maxLength: 64 @@ -9525,8 +9527,10 @@ spec: type: object componentDef: description: |- - References the name of a ComponentDefinition object. - The ComponentDefinition specifies the behavior and characteristics of the Component. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) that defines the Component's characteristics and behavior. + + If both `componentDefRef` and `componentDef` are provided, the `componentDef` will take precedence over `componentDefRef`. maxLength: 64 diff --git a/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml b/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml index bd9064e8e9f..7ad3e446840 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_componentdefinitions.yaml @@ -12493,7 +12493,10 @@ spec: properties: compDef: description: |- - CompDef specifies the definition used by the component that the referent object resident in. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) used by the component that the referent object resident in. + + If not specified, the component itself will be used. type: string componentName: @@ -12644,7 +12647,10 @@ spec: properties: compDef: description: |- - CompDef specifies the definition used by the component that the referent object resident in. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) used by the component that the referent object resident in. + + If not specified, the component itself will be used. type: string multipleClusterObjectOption: @@ -12723,7 +12729,10 @@ spec: properties: compDef: description: |- - CompDef specifies the definition used by the component that the referent object resident in. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) used by the component that the referent object resident in. + + If not specified, the component itself will be used. type: string container: @@ -12832,7 +12841,10 @@ spec: properties: compDef: description: |- - CompDef specifies the definition used by the component that the referent object resident in. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) used by the component that the referent object resident in. + + If not specified, the component itself will be used. type: string endpoint: @@ -12932,7 +12944,10 @@ spec: properties: compDef: description: |- - CompDef specifies the definition used by the component that the referent object resident in. + Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition + custom resource (CR) used by the component that the referent object resident in. + + If not specified, the component itself will be used. type: string host: diff --git a/docs/developer_docs/api-reference/cluster.md b/docs/developer_docs/api-reference/cluster.md index f537c34a93a..79b2df0a6ed 100644 --- a/docs/developer_docs/api-reference/cluster.md +++ b/docs/developer_docs/api-reference/cluster.md @@ -4029,9 +4029,9 @@ string (Optional) -

References the name of a ComponentDefinition object. -The ComponentDefinition specifies the behavior and characteristics of the Component. -If both componentDefRef and componentDef are provided, +

Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition +custom resource (CR) that defines the Component’s characteristics and behavior.

+

If both componentDefRef and componentDef are provided, the componentDef will take precedence over componentDefRef.

@@ -4928,8 +4928,9 @@ string (Optional) -

CompDef specifies the definition used by the component that the referent object resident in. -If not specified, the component itself will be used.

+

Specifies the exact name, name prefix, or regular expression pattern for matching the name of the ComponentDefinition +custom resource (CR) used by the component that the referent object resident in.

+

If not specified, the component itself will be used.

diff --git a/pkg/controller/component/utils.go b/pkg/controller/component/utils.go index 98596b20b02..ff6b8b76a24 100644 --- a/pkg/controller/component/utils.go +++ b/pkg/controller/component/utils.go @@ -38,12 +38,17 @@ func inDataContext() *multicluster.ClientOption { return multicluster.InDataContext() } +func ValidateCompDefRegexp(compDefPattern string) error { + _, err := regexp.Compile(compDefPattern) + return err +} + func CompDefMatched(compDef, compDefPattern string) bool { if strings.HasPrefix(compDef, compDefPattern) { return true } - isRegexPattern := func(pattern string) bool { + isRegexpPattern := func(pattern string) bool { escapedPattern := regexp.QuoteMeta(pattern) return escapedPattern != pattern } @@ -52,7 +57,7 @@ func CompDefMatched(compDef, compDefPattern string) bool { regex, err := regexp.Compile(compDefPattern) if err == nil { // distinguishing between regular expressions and ordinary strings. - if isRegexPattern(compDefPattern) { + if isRegexpPattern(compDefPattern) { isRegex = true } } diff --git a/pkg/controller/component/utils_test.go b/pkg/controller/component/utils_test.go index d111c4bef63..1ed813fa932 100644 --- a/pkg/controller/component/utils_test.go +++ b/pkg/controller/component/utils_test.go @@ -25,6 +25,40 @@ import ( ) var _ = Describe("component utils", func() { + Context("component definition reference regex validate test", func() { + It("should return nil for valid regular expressions", func() { + validExpressions := []string{ + `mysql`, + `-mysql-`, + `mysql-8.0.30`, + `\d+`, + `[a-zA-Z]+`, + `^mysql-\d+\.\d+\.\d+$`, + `^[v\-]*?(\d{1,2}\.){0,3}\d{1,2}$`, + } + + for _, expr := range validExpressions { + err := ValidateCompDefRegexp(expr) + Expect(err).Should(BeNil()) + } + }) + + It("should return an error for invalid regular expressions", func() { + invalidExpressions := []string{ + `(*)`, + `(abc`, + `a**`, + `x[a-z`, + `[z-a]`, + } + + for _, expr := range invalidExpressions { + err := ValidateCompDefRegexp(expr) + Expect(err).ShouldNot(BeNil()) + } + }) + }) + Context("component definition reference matching test", func() { It("name, name prefix, regex expression matching", func() { type compDefMatch struct { diff --git a/pkg/testutil/apps/component_version_util.go b/pkg/testutil/apps/component_version_util.go index 11f9d5f3a39..5d687306014 100644 --- a/pkg/testutil/apps/component_version_util.go +++ b/pkg/testutil/apps/component_version_util.go @@ -37,12 +37,23 @@ const ( func AppImage(app, tag string) string { return fmt.Sprintf("%s:%s", app, tag) } + func CompDefName(r string) string { return fmt.Sprintf("%s-%s", CompDefinitionName, r) } + +func CompDefNameWithFuzzyRegex(r string) string { + return fmt.Sprintf("^%s-%s*", CompDefinitionName, r) +} + +func CompDefNameWithExactRegex(r string) string { + return fmt.Sprintf("^%s-%s$", CompDefinitionName, r) +} + func ReleaseID(r string) string { return fmt.Sprintf("%s-%s", ReleasePrefix, r) } + func ServiceVersion(r string) string { if len(r) == 0 { return ServiceVersionPrefix