diff --git a/apis/apps/v1alpha1/cluster_types.go b/apis/apps/v1alpha1/cluster_types.go index ed505b5c7e3..20cbe49fed5 100644 --- a/apis/apps/v1alpha1/cluster_types.go +++ b/apis/apps/v1alpha1/cluster_types.go @@ -589,8 +589,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/clusterdefinition_types.go b/apis/apps/v1alpha1/clusterdefinition_types.go index 3612f577700..58b89233b3e 100644 --- a/apis/apps/v1alpha1/clusterdefinition_types.go +++ b/apis/apps/v1alpha1/clusterdefinition_types.go @@ -74,14 +74,15 @@ type ClusterTopologyComponent struct { // +kubebuilder:validation:Pattern:=`^[a-z]([a-z0-9\-]*[a-z0-9])?$` Name string `json:"name"` - // Specifies the name or prefix of the ComponentDefinition custom resource(CR) that - // defines the Component's characteristics and behavior. + // 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. // - // When a prefix is used, the system selects the ComponentDefinition CR with the latest version that matches the prefix. + // The system selects the ComponentDefinition CR with the latest version that matches the pattern. // This approach allows: // // 1. Precise selection by providing the exact name of a ComponentDefinition CR. - // 2. Flexible and automatic selection of the most up-to-date ComponentDefinition CR by specifying a prefix. + // 2. Flexible and automatic selection of the most up-to-date ComponentDefinition CR + // by specifying a name prefix or regular expression pattern. // // Once set, this field cannot be updated. // diff --git a/apis/apps/v1alpha1/componentversion_types.go b/apis/apps/v1alpha1/componentversion_types.go index 33f8a0f145b..c9e6d674470 100644 --- a/apis/apps/v1alpha1/componentversion_types.go +++ b/apis/apps/v1alpha1/componentversion_types.go @@ -40,12 +40,13 @@ type ComponentVersionSpec struct { // ComponentVersionCompatibilityRule defines the compatibility between a set of component definitions and a set of releases. type ComponentVersionCompatibilityRule struct { // CompDefs specifies names for the component definitions associated with this ComponentVersion. - // Each name in the list can represent an exact name, or a name prefix. + // Each name in the list can represent an exact name, a name prefix, or a regular expression pattern. // // For example: // // - "mysql-8.0.30-v1alpha1": Matches the exact name "mysql-8.0.30-v1alpha1" // - "mysql-8.0.30": Matches all names starting with "mysql-8.0.30" + // - "^mysql-8.0.\d{1,2}$": Matches all names starting with "mysql-8.0." followed by one or two digits. // // +kubebuilder:validation:Required // +kubebuilder:validation:MinItems=1 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_clusterdefinitions.yaml b/config/crd/bases/apps.kubeblocks.io_clusterdefinitions.yaml index a626e4fb376..22c1da83248 100644 --- a/config/crd/bases/apps.kubeblocks.io_clusterdefinitions.yaml +++ b/config/crd/bases/apps.kubeblocks.io_clusterdefinitions.yaml @@ -85,20 +85,17 @@ spec: within a ClusterTopology. properties: compDef: - description: |- - Specifies the name or prefix of the ComponentDefinition custom resource(CR) that - defines the Component's characteristics and behavior. - - - When a prefix is used, the system selects the ComponentDefinition CR with the latest version that matches the prefix. - This approach allows: - - - 1. Precise selection by providing the exact name of a ComponentDefinition CR. - 2. Flexible and automatic selection of the most up-to-date ComponentDefinition CR by specifying a prefix. - - - Once set, this field cannot be updated. + description: "Specifies the exact name, name prefix, or + regular expression pattern for matching the name of + the ComponentDefinition\ncustom resource (CR) that defines + the Component's characteristics and behavior.\n\n\nThe + system selects the ComponentDefinition CR with the latest + version that matches the pattern.\nThis approach allows:\n\n\n1. + Precise selection by providing the exact name of a ComponentDefinition + CR.\n2. Flexible and automatic selection of the most + up-to-date ComponentDefinition CR\n\t by specifying + a name prefix or regular expression pattern.\n\n\nOnce + set, this field cannot be updated." maxLength: 64 type: string name: diff --git a/config/crd/bases/apps.kubeblocks.io_clusters.yaml b/config/crd/bases/apps.kubeblocks.io_clusters.yaml index e0766e1fe10..965355675fd 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/config/crd/bases/apps.kubeblocks.io_componentversions.yaml b/config/crd/bases/apps.kubeblocks.io_componentversions.yaml index f5fbe385600..2a6f91047b8 100644 --- a/config/crd/bases/apps.kubeblocks.io_componentversions.yaml +++ b/config/crd/bases/apps.kubeblocks.io_componentversions.yaml @@ -66,7 +66,7 @@ spec: compDefs: description: |- CompDefs specifies names for the component definitions associated with this ComponentVersion. - Each name in the list can represent an exact name, or a name prefix. + Each name in the list can represent an exact name, a name prefix, or a regular expression pattern. For example: @@ -74,6 +74,7 @@ spec: - "mysql-8.0.30-v1alpha1": Matches the exact name "mysql-8.0.30-v1alpha1" - "mysql-8.0.30": Matches all names starting with "mysql-8.0.30" + - "^mysql-8.0.\d{1,2}$": Matches all names starting with "mysql-8.0." followed by one or two digits. items: type: string maxItems: 128 diff --git a/controllers/apps/clusterdefinition_controller.go b/controllers/apps/clusterdefinition_controller.go index 7c604ec77da..27d6779f52f 100644 --- a/controllers/apps/clusterdefinition_controller.go +++ b/controllers/apps/clusterdefinition_controller.go @@ -35,6 +35,7 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" appsconfig "github.com/apecloud/kubeblocks/controllers/apps/configuration" "github.com/apecloud/kubeblocks/pkg/constant" + "github.com/apecloud/kubeblocks/pkg/controller/component" intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" ) @@ -190,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 @@ -246,7 +255,7 @@ func (r *ClusterDefinitionReconciler) loadTopologyCompDefs(ctx context.Context, for _, comp := range topology.Components { defs := make([]*appsv1alpha1.ComponentDefinition, 0) for compDefName := range compDefs { - if strings.HasPrefix(compDefName, comp.CompDef) { + if component.CompDefMatched(compDefName, comp.CompDef) { defs = append(defs, compDefs[compDefName]) } } diff --git a/controllers/apps/componentdefinition_controller.go b/controllers/apps/componentdefinition_controller.go index 88b1f9caaac..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" @@ -40,6 +41,7 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" appsconfig "github.com/apecloud/kubeblocks/controllers/apps/configuration" "github.com/apecloud/kubeblocks/pkg/constant" + "github.com/apecloud/kubeblocks/pkg/controller/component" intctrlutil "github.com/apecloud/kubeblocks/pkg/controllerutil" ) @@ -211,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 } @@ -411,26 +445,26 @@ func getNCheckCompDefinition(ctx context.Context, cli client.Reader, name string return compDef, nil } -// listCompDefinitionsWithPrefix returns all component definitions whose names have prefix @namePrefix. -func listCompDefinitionsWithPrefix(ctx context.Context, cli client.Reader, namePrefix string) ([]*appsv1alpha1.ComponentDefinition, error) { +// listCompDefinitionsWithPattern returns all component definitions whose names match the given pattern (namePrefix or regular expression) +func listCompDefinitionsWithPattern(ctx context.Context, cli client.Reader, namePattern string) ([]*appsv1alpha1.ComponentDefinition, error) { compDefList := &appsv1alpha1.ComponentDefinitionList{} if err := cli.List(ctx, compDefList); err != nil { return nil, err } compDefsFullyMatched := make([]*appsv1alpha1.ComponentDefinition, 0) - compDefsPrefixMatched := make([]*appsv1alpha1.ComponentDefinition, 0) + compDefsPatternMatched := make([]*appsv1alpha1.ComponentDefinition, 0) for i, item := range compDefList.Items { - if item.Name == namePrefix { + if item.Name == namePattern { compDefsFullyMatched = append(compDefsFullyMatched, &compDefList.Items[i]) } - if strings.HasPrefix(item.Name, namePrefix) { - compDefsPrefixMatched = append(compDefsPrefixMatched, &compDefList.Items[i]) + if component.CompDefMatched(item.Name, namePattern) { + compDefsPatternMatched = append(compDefsPatternMatched, &compDefList.Items[i]) } } if len(compDefsFullyMatched) > 0 { return compDefsFullyMatched, nil } - return compDefsPrefixMatched, 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 b78320dc8d1..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, "") @@ -178,7 +186,7 @@ func (r *ComponentVersionReconciler) buildReleaseToCompDefinitionMapping(cli cli continue } var err error - compDefs[compDef], err = listCompDefinitionsWithPrefix(rctx.Ctx, cli, compDef) + compDefs[compDef], err = listCompDefinitionsWithPattern(rctx.Ctx, cli, compDef) if err != nil { return nil, err } @@ -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 := listCompDefinitionsWithPrefix(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..7afe2b8e589 100644 --- a/controllers/apps/componentversion_controller_test.go +++ b/controllers/apps/componentversion_controller_test.go @@ -235,6 +235,21 @@ var _ = Describe("ComponentVersion Controller", func() { })).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 +509,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_clusterdefinitions.yaml b/deploy/helm/crds/apps.kubeblocks.io_clusterdefinitions.yaml index a626e4fb376..22c1da83248 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_clusterdefinitions.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_clusterdefinitions.yaml @@ -85,20 +85,17 @@ spec: within a ClusterTopology. properties: compDef: - description: |- - Specifies the name or prefix of the ComponentDefinition custom resource(CR) that - defines the Component's characteristics and behavior. - - - When a prefix is used, the system selects the ComponentDefinition CR with the latest version that matches the prefix. - This approach allows: - - - 1. Precise selection by providing the exact name of a ComponentDefinition CR. - 2. Flexible and automatic selection of the most up-to-date ComponentDefinition CR by specifying a prefix. - - - Once set, this field cannot be updated. + description: "Specifies the exact name, name prefix, or + regular expression pattern for matching the name of + the ComponentDefinition\ncustom resource (CR) that defines + the Component's characteristics and behavior.\n\n\nThe + system selects the ComponentDefinition CR with the latest + version that matches the pattern.\nThis approach allows:\n\n\n1. + Precise selection by providing the exact name of a ComponentDefinition + CR.\n2. Flexible and automatic selection of the most + up-to-date ComponentDefinition CR\n\t by specifying + a name prefix or regular expression pattern.\n\n\nOnce + set, this field cannot be updated." maxLength: 64 type: string name: diff --git a/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml b/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml index e0766e1fe10..965355675fd 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/deploy/helm/crds/apps.kubeblocks.io_componentversions.yaml b/deploy/helm/crds/apps.kubeblocks.io_componentversions.yaml index f5fbe385600..2a6f91047b8 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_componentversions.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_componentversions.yaml @@ -66,7 +66,7 @@ spec: compDefs: description: |- CompDefs specifies names for the component definitions associated with this ComponentVersion. - Each name in the list can represent an exact name, or a name prefix. + Each name in the list can represent an exact name, a name prefix, or a regular expression pattern. For example: @@ -74,6 +74,7 @@ spec: - "mysql-8.0.30-v1alpha1": Matches the exact name "mysql-8.0.30-v1alpha1" - "mysql-8.0.30": Matches all names starting with "mysql-8.0.30" + - "^mysql-8.0.\d{1,2}$": Matches all names starting with "mysql-8.0." followed by one or two digits. items: type: string maxItems: 128 diff --git a/docs/developer_docs/api-reference/cluster.md b/docs/developer_docs/api-reference/cluster.md index f22e8d21d47..3d35db7aa3e 100644 --- a/docs/developer_docs/api-reference/cluster.md +++ b/docs/developer_docs/api-reference/cluster.md @@ -4030,9 +4030,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.

@@ -4929,8 +4929,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.

@@ -5743,13 +5744,14 @@ string -

Specifies the name or prefix of the ComponentDefinition custom resource(CR) that -defines the Component’s characteristics and behavior.

-

When a prefix is used, the system selects the ComponentDefinition CR with the latest version that matches the prefix. +

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.

+

The system selects the ComponentDefinition CR with the latest version that matches the pattern. This approach allows:

  1. Precise selection by providing the exact name of a ComponentDefinition CR.
  2. -
  3. Flexible and automatic selection of the most up-to-date ComponentDefinition CR by specifying a prefix.
  4. +
  5. Flexible and automatic selection of the most up-to-date ComponentDefinition CR +by specifying a name prefix or regular expression pattern.

Once set, this field cannot be updated.

@@ -8169,11 +8171,12 @@ The value will be presented in the following format: FQDN1,FQDN2,…

CompDefs specifies names for the component definitions associated with this ComponentVersion. -Each name in the list can represent an exact name, or a name prefix.

+Each name in the list can represent an exact name, a name prefix, or a regular expression pattern.

For example:

diff --git a/pkg/controller/component/utils.go b/pkg/controller/component/utils.go index 05bb2da6134..ff6b8b76a24 100644 --- a/pkg/controller/component/utils.go +++ b/pkg/controller/component/utils.go @@ -21,6 +21,7 @@ package component import ( "context" + "regexp" "slices" "strings" @@ -37,6 +38,35 @@ 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 + } + + isRegexpPattern := func(pattern string) bool { + escapedPattern := regexp.QuoteMeta(pattern) + return escapedPattern != pattern + } + + isRegex := false + regex, err := regexp.Compile(compDefPattern) + if err == nil { + // distinguishing between regular expressions and ordinary strings. + if isRegexpPattern(compDefPattern) { + isRegex = true + } + } + if !isRegex { + return false + } + return regex.MatchString(compDef) +} + func IsHostNetworkEnabled(synthesizedComp *SynthesizedComponent) bool { if !hasHostNetworkCapability(synthesizedComp, nil) { return false diff --git a/pkg/controller/component/utils_test.go b/pkg/controller/component/utils_test.go new file mode 100644 index 00000000000..1ed813fa932 --- /dev/null +++ b/pkg/controller/component/utils_test.go @@ -0,0 +1,156 @@ +/* +Copyright (C) 2022-2024 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 ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +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 { + compDefPattern string + compDef string + } + tests := []struct { + name string + fields compDefMatch + want bool + }{{ + name: "version string test true", + fields: compDefMatch{ + compDefPattern: "mysql-8.0.30-v1alpha1", + compDef: "mysql-8.0.30-v1alpha1", + }, + want: true, + }, { + name: "version string test true", + fields: compDefMatch{ + compDefPattern: "mysql-8.0.30", + compDef: "mysql-8.0.30-v1alpha1", + }, + want: true, + }, { + name: "version string test false", + fields: compDefMatch{ + compDefPattern: "mysql-8.0.30", + compDef: "mysql-8.0.29", + }, + want: false, + }, { + name: "version string test false", + fields: compDefMatch{ + compDefPattern: "^8.0.8$", + compDef: "v8.0.8", + }, + want: false, + }, { + name: "version string test true", + fields: compDefMatch{ + compDefPattern: "8.0.\\d{1,2}$", + compDef: "8.0.6", + }, + want: true, + }, { + name: "version string test false", + fields: compDefMatch{ + compDefPattern: "8.0.\\d{1,2}$", + compDef: "8.0.8.8.8", + }, + want: false, + }, { + name: "version string test true", + fields: compDefMatch{ + compDefPattern: "^[v\\-]*?(\\d{1,2}\\.){0,3}\\d{1,2}$", + compDef: "v-8.0.8.0", + }, + want: true, + }, { + name: "version string test false", + fields: compDefMatch{ + compDefPattern: "^[v\\-]*?(\\d{1,2}\\.){0,3}\\d{1,2}$", + compDef: "mysql-8.0.8", + }, + want: false, + }, { + name: "version string test true", + fields: compDefMatch{ + compDefPattern: "^mysql-8.0.\\d{1,2}$", + compDef: "mysql-8.0.8", + }, + want: true, + }, { + name: "version string test false", + fields: compDefMatch{ + compDefPattern: "mysql", + compDef: "abcmysql", + }, + want: false, + }, { + name: "version string test false", + fields: compDefMatch{ + compDefPattern: "mysql-", + compDef: "abc-mysql-", + }, + want: false, + }} + for _, tt := range tests { + match := CompDefMatched(tt.fields.compDef, tt.fields.compDefPattern) + Expect(match).Should(Equal(tt.want)) + } + }) + }) +}) diff --git a/pkg/controller/component/vars.go b/pkg/controller/component/vars.go index 20a16cab2d2..afb8a36abcc 100644 --- a/pkg/controller/component/vars.go +++ b/pkg/controller/component/vars.go @@ -1324,19 +1324,14 @@ func resolveReferentObjects(synthesizedComp *SynthesizedComponent, } func resolveReferentComponents(synthesizedComp *SynthesizedComponent, objRef appsv1alpha1.ClusterObjectReference) ([]string, error) { - // nolint:gocritic - compDefMatched := func(def, defRef string) bool { - return strings.HasPrefix(def, defRef) // prefix match - } - // match the current component when the multiple cluster object option not set - if len(objRef.CompDef) == 0 || (compDefMatched(synthesizedComp.CompDefName, objRef.CompDef) && objRef.MultipleClusterObjectOption == nil) { + if len(objRef.CompDef) == 0 || (CompDefMatched(synthesizedComp.CompDefName, objRef.CompDef) && objRef.MultipleClusterObjectOption == nil) { return []string{synthesizedComp.Name}, nil } compNames := make([]string, 0) for k, v := range synthesizedComp.Comp2CompDefs { - if compDefMatched(v, objRef.CompDef) { + if CompDefMatched(v, objRef.CompDef) { compNames = append(compNames, k) } } diff --git a/pkg/controller/component/vars_test.go b/pkg/controller/component/vars_test.go index e6837d21616..76c5a381d77 100644 --- a/pkg/controller/component/vars_test.go +++ b/pkg/controller/component/vars_test.go @@ -2169,6 +2169,30 @@ var _ = Describe("vars", func() { checkEnvVarWithValue(envVars, "service-host", svcName1) }) + It("w/o option - comp def name with regex", func() { + vars := []appsv1alpha1.EnvVar{ + { + Name: "service-host", + ValueFrom: &appsv1alpha1.VarSource{ + ServiceVarRef: &appsv1alpha1.ServiceVarSelector{ + ClusterObjectReference: appsv1alpha1.ClusterObjectReference{ + CompDef: "^" + synthesizedComp.CompDefName + "$", // compDef name with regex + Name: "service", + Optional: required(), + }, + ServiceVars: appsv1alpha1.ServiceVars{ + Host: &appsv1alpha1.VarRequired, + }, + }, + }, + }, + } + templateVars, envVars, err := ResolveTemplateNEnvVars(testCtx.Ctx, reader, synthesizedComp, vars) + Expect(err).Should(Succeed()) + Expect(templateVars).Should(HaveKeyWithValue("service-host", svcName1)) + checkEnvVarWithValue(envVars, "service-host", svcName1) + }) + It("w/ option - ref others", func() { vars := []appsv1alpha1.EnvVar{ { diff --git a/pkg/testutil/apps/component_version_util.go b/pkg/testutil/apps/component_version_util.go index 11f9d5f3a39..8da499656c6 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(\\.\\d+)?$", 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