/* Copyright 2021 Alibaba Group Holding Limited. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package polardbxcluster import ( "bytes" "context" "fmt" "strconv" "strings" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/validation/field" polardbxv1 "github.com/alibaba/polardbx-operator/api/v1" polardbxv1common "github.com/alibaba/polardbx-operator/api/v1/common" polardbxv1polardbx "github.com/alibaba/polardbx-operator/api/v1/polardbx" polardbxv1xstore "github.com/alibaba/polardbx-operator/api/v1/xstore" iniutil "github.com/alibaba/polardbx-operator/pkg/util/ini" "github.com/alibaba/polardbx-operator/pkg/webhook/extension" ) func validateImagePullPolicy(fldPath *field.Path, policy corev1.PullPolicy) *field.Error { switch policy { case "", corev1.PullIfNotPresent, corev1.PullAlways, corev1.PullNever: return nil default: return field.NotSupported(fldPath, policy, []string{ "", string(corev1.PullIfNotPresent), string(corev1.PullAlways), string(corev1.PullNever), }) } } func validateResources(ctx context.Context, fldPath *field.Path, resources *corev1.ResourceRequirements) field.ErrorList { var errList field.ErrorList // If no limits and requests are set, there's a default value for the resource in CRD. if resources.Limits == nil && resources.Requests == nil { return nil } if resources.Limits == nil { errList = append(errList, field.Required(fldPath.Child("limits"), "limits is required")) } cpuLimits, cpuLimitsExists := resources.Limits[corev1.ResourceCPU] if !cpuLimitsExists { errList = append(errList, field.Required(fldPath.Child("limits", "cpu"), "limits.cpu is required")) } memoryLimits, memoryLimitsExists := resources.Limits[corev1.ResourceMemory] if !memoryLimitsExists { errList = append(errList, field.Required(fldPath.Child("limits", "memory"), "limits.memory is required")) } if resources.Requests != nil { if cpuLimitsExists { if cpuRequests, ok := resources.Requests[corev1.ResourceCPU]; ok { if cpuRequests.MilliValue() > cpuLimits.MilliValue() { errList = append(errList, field.Invalid(fldPath.Child("requests", "cpu"), cpuRequests.String(), "must be less than limits: "+cpuLimits.String())) } } } if memoryLimitsExists { if memoryRequests, ok := resources.Requests[corev1.ResourceMemory]; ok { if memoryRequests.Value() > memoryLimits.Value() { errList = append(errList, field.Invalid(fldPath.Child("requests", "memory"), memoryRequests.String(), "must be less than limits: "+memoryLimits.String())) } } } } return errList } func validateExtendResources(ctx context.Context, fldPath *field.Path, resources *polardbxv1common.ExtendedResourceRequirements) field.ErrorList { return validateResources(ctx, fldPath, &resources.ResourceRequirements) } type PolarDBXClusterV1Validator struct { configLoader func() *ValidatorConfig } func (v *PolarDBXClusterV1Validator) validateSecurity(ctx context.Context, security *polardbxv1polardbx.Security) field.ErrorList { var errList field.ErrorList if security == nil { return errList } if security.EncodeKey != nil { if security.EncodeKey.Name == "" { errList = append(errList, field.NotFound( field.NewPath("spec", "security", "encodeKey", "name"), ""), ) } if security.EncodeKey.Key == "" { errList = append(errList, field.NotFound( field.NewPath("spec", "security", "encodeKey", "key"), ""), ) } } return errList } func (v *PolarDBXClusterV1Validator) validateNodeSelector(fieldPath *field.Path, ns *corev1.NodeSelector) *field.Error { return nil } func (v *PolarDBXClusterV1Validator) validateStatelessTopologyRuleItems(ctx context.Context, fieldPath *field.Path, items []polardbxv1polardbx.StatelessTopologyRuleItem, validSelectors map[string]int) field.ErrorList { var errList field.ErrorList names := make(map[string]int) for index, item := range items { _, ok := names[item.Name] if ok { errList = append(errList, field.Duplicate( fieldPath.Index(index).Child("name"), item.Name), ) } else { names[item.Name] = index } // Defer validation on replicas. ns := item.NodeSelector if ns != nil { if ns.Reference != "" { _, found := validSelectors[ns.Reference] if !found { errList = append(errList, field.Invalid( fieldPath.Index(index).Child("selector", "reference"), ns.Reference, "invalid selector, not predefined", )) } } } } return errList } func (v *PolarDBXClusterV1Validator) validateXStoreTopologyRule(ctx context.Context, fieldPath *field.Path, rule *polardbxv1polardbx.XStoreTopologyRule, validSelectors map[string]int) field.ErrorList { var errList field.ErrorList if rule == nil { return errList } if rule.Rolling != nil && len(rule.NodeSets) > 0 { errList = append(errList, field.Invalid(fieldPath, rule, "rolling and nodeSets can not be both defined")) return errList } if rule.Rolling != nil { if rule.Rolling.Replicas%2 == 0 { errList = append(errList, field.Invalid( fieldPath.Child("rolling", "replicas"), rule.Rolling.Replicas, "must be odd")) } ns := rule.Rolling.NodeSelector if ns != nil { if ns.Reference != "" { _, found := validSelectors[ns.Reference] if !found { errList = append(errList, field.Invalid( fieldPath.Child("rolling", "selector", "reference"), ns.Reference, "invalid selector, not predefined", )) } } } } if len(rule.NodeSets) > 0 { nodeSetNames := make(map[string]int) for index, nodeSet := range rule.NodeSets { if nodeSet.Name == "" { errList = append(errList, field.NotFound( fieldPath.Child("nodeSets").Index(index).Child("name"), "", )) } else { _, found := nodeSetNames[nodeSet.Name] if found { errList = append(errList, field.Duplicate( fieldPath.Child("nodeSets").Index(index).Child("name"), nodeSet.Name)) } else { nodeSetNames[nodeSet.Name] = index } } switch nodeSet.Role { case polardbxv1xstore.RoleCandidate, polardbxv1xstore.RoleVoter, polardbxv1xstore.RoleLearner: // break default: errList = append(errList, field.NotSupported( fieldPath.Child("nodeSets").Index(index).Child("role"), nodeSet.Role, []string{ string(polardbxv1xstore.RoleCandidate), string(polardbxv1xstore.RoleVoter), string(polardbxv1xstore.RoleLearner), }), ) } ns := nodeSet.NodeSelector if ns != nil { if ns.Reference != "" { _, found := validSelectors[ns.Reference] if !found { errList = append(errList, field.Invalid( fieldPath.Index(index).Child("selector", "reference"), ns.Reference, "invalid selector, not predefined", )) } } } } } return errList } func (v *PolarDBXClusterV1Validator) validateTopologyRules(ctx context.Context, rules *polardbxv1polardbx.TopologyRules) field.ErrorList { var errList field.ErrorList fieldPath := field.NewPath("spec", "topology", "rules") validSelectors := make(map[string]int) for index, selector := range rules.Selectors { if selector.Name == "" { errList = append(errList, field.NotFound( fieldPath. Child("selectors"). Index(index). Child("name"), ""), ) } else { _, ok := validSelectors[selector.Name] if ok { errList = append(errList, field.Duplicate( fieldPath. Child("selectors"). Index(index). Child("name"), selector.Name), ) } else { validSelectors[selector.Name] = index } } err := v.validateNodeSelector(fieldPath. Child("selectors"). Index(index). Child("nodeSelector"), &selector.NodeSelector) if err != nil { errList = append(errList, err) } } errList = append(errList, v.validateStatelessTopologyRuleItems(ctx, fieldPath.Child("components", "cn"), rules.Components.CN, validSelectors)...) errList = append(errList, v.validateStatelessTopologyRuleItems(ctx, fieldPath.Child("components", "cdc"), rules.Components.CDC, validSelectors)...) errList = append(errList, v.validateStatelessTopologyRuleItems(ctx, fieldPath.Child("components", "columnar"), rules.Components.Columnar, validSelectors)...) errList = append(errList, v.validateXStoreTopologyRule(ctx, fieldPath.Child("components", "gms"), rules.Components.GMS, validSelectors)...) errList = append(errList, v.validateXStoreTopologyRule(ctx, fieldPath.Child("components", "dn"), rules.Components.DN, validSelectors)...) return errList } func (v *PolarDBXClusterV1Validator) validateXStoreTemplate(ctx context.Context, fldPath *field.Path, template *polardbxv1polardbx.XStoreTemplate) field.ErrorList { if template == nil { return nil } var errList field.ErrorList switch template.ServiceType { case corev1.ServiceTypeClusterIP, corev1.ServiceTypeNodePort, corev1.ServiceTypeLoadBalancer, corev1.ServiceTypeExternalName: // break default: errList = append(errList, field.NotSupported( fldPath.Child("serviceType"), template.ServiceType, []string{ string(corev1.ServiceTypeClusterIP), string(corev1.ServiceTypeNodePort), string(corev1.ServiceTypeLoadBalancer), string(corev1.ServiceTypeExternalName), })) } if err := validateImagePullPolicy(fldPath.Child("imagePullPolicy"), template.ImagePullPolicy); err != nil { errList = append(errList, err) } errList = append(errList, validateExtendResources(ctx, fldPath.Child("resources"), &template.Resources)...) return errList } func (v *PolarDBXClusterV1Validator) validateCNTemplate(ctx context.Context, fldPath *field.Path, template *polardbxv1polardbx.CNTemplate) field.ErrorList { var errList field.ErrorList errList = append(errList, validateResources(ctx, fldPath.Child("resources"), &template.Resources)...) if err := validateImagePullPolicy(fldPath.Child("imagePullPolicy"), template.ImagePullPolicy); err != nil { errList = append(errList, err) } return errList } func (v *PolarDBXClusterV1Validator) validateCDCTemplate(ctx context.Context, fldPath *field.Path, template *polardbxv1polardbx.CDCTemplate) field.ErrorList { var errList field.ErrorList errList = append(errList, validateResources(ctx, fldPath.Child("resources"), &template.Resources)...) if err := validateImagePullPolicy(fldPath.Child("imagePullPolicy"), template.ImagePullPolicy); err != nil { errList = append(errList, err) } return errList } func (v *PolarDBXClusterV1Validator) validateColumnarTemplate(ctx context.Context, fldPath *field.Path, template *polardbxv1polardbx.ColumnarTemplate) field.ErrorList { var errList field.ErrorList errList = append(errList, validateResources(ctx, fldPath.Child("resources"), &template.Resources)...) if err := validateImagePullPolicy(fldPath.Child("imagePullPolicy"), template.ImagePullPolicy); err != nil { errList = append(errList, err) } return errList } func (v *PolarDBXClusterV1Validator) validateTopologyNodes(ctx context.Context, nodes *polardbxv1polardbx.TopologyNodes) field.ErrorList { var errList field.ErrorList fldPath := field.NewPath("spec", "topology", "nodes") errList = append(errList, v.validateXStoreTemplate(ctx, fldPath.Child("gms", "template"), nodes.GMS.Template)...) errList = append(errList, v.validateXStoreTemplate(ctx, fldPath.Child("dn", "template"), &nodes.DN.Template)...) errList = append(errList, v.validateCNTemplate(ctx, fldPath.Child("cn", "template"), &nodes.CN.Template)...) if nodes.CDC != nil { errList = append(errList, v.validateCDCTemplate(ctx, fldPath.Child("cdc", "template"), &nodes.CDC.Template)...) } if nodes.Columnar != nil { errList = append(errList, v.validateColumnarTemplate(ctx, fldPath.Child("columnar", "template"), &nodes.Columnar.Template)...) } return errList } func getRuleReplicas(total int, replicas *intstr.IntOrString) (int, error) { if replicas.Type == intstr.Int { val := replicas.IntValue() return val, nil } else { s := replicas.StrVal if strings.HasSuffix(s, "%") || strings.HasSuffix(s, "%+") { var percentageStr string roundUp := false if s[len(s)-1] == '+' { percentageStr = s[:len(s)-2] roundUp = true } else { percentageStr = s[:len(s)-1] } percentage, err := strconv.Atoi(strings.TrimSpace(percentageStr)) if err != nil { return 0, fmt.Errorf("invalid replicas: not a percentage, %w", err) } if percentage >= 100 { return 0, fmt.Errorf("invalid replicas: not a valid percentage, should be less than 1") } if roundUp { return (total*percentage + 99) / 100, nil } else { return total * percentage / 100, nil } } else if strings.Contains(s, "/") { split := strings.SplitN(s, "/", 2) if len(split) < 2 { return 0, fmt.Errorf("invalid replicas: not a fraction") } a, err := strconv.Atoi(strings.TrimSpace(split[0])) if err != nil { return 0, fmt.Errorf("invalid replicas: not a fraction, %w", err) } b, err := strconv.Atoi(strings.TrimSpace(split[1])) if err != nil { return 0, fmt.Errorf("invalid replicas: not a fraction, %w", err) } if a < 0 { return 0, fmt.Errorf("invalid replicas: not a valid fraction, numerator should be non-negative integer") } if b <= 0 { return 0, fmt.Errorf("invalid replicas: not a valid fraction, denominator should be positive integer") } if a >= b { return 0, fmt.Errorf("invalid replicas: not a valid fraction, should be less than 1") } return total * a / b, nil } else { val, err := strconv.Atoi(replicas.StrVal) if err != nil { return 0, fmt.Errorf("invalid replicas: %w", err) } return val, nil } } } func (v *PolarDBXClusterV1Validator) validateReplicasInRule(fldPath *field.Path, replicas *intstr.IntOrString, total int) (int, *field.Error) { r, err := getRuleReplicas(total, replicas) if err != nil { return 0, field.Invalid(fldPath, replicas, err.Error()) } return r, nil } func (v *PolarDBXClusterV1Validator) validateReplicasOnStatelessComponent(ctx context.Context, rules []polardbxv1polardbx.StatelessTopologyRuleItem, fldPath *field.Path, replicas int) field.ErrorList { var errList field.ErrorList sum := 0 emptyReplicas := 0 for i, rule := range rules { rFldPath := fldPath.Index(i).Child("replicas") if rule.Replicas == nil { emptyReplicas++ if emptyReplicas > 1 { errList = append(errList, field.Forbidden(rFldPath, "multiple nil replicas found, only one is allowed")) } continue } else { r, err := v.validateReplicasInRule(rFldPath, rule.Replicas, replicas) if err != nil { errList = append(errList, err) } sum += r } } // It's allowed to be less or equal. if sum > replicas { errList = append(errList, field.Forbidden(fldPath, fmt.Sprintf("invalid rules, sum of replicas %d is larger than declared %d", sum, replicas))) } return errList } func (v *PolarDBXClusterV1Validator) validateReplicas(ctx context.Context, topology *polardbxv1polardbx.Topology) field.ErrorList { var errList field.ErrorList cnRules := topology.Rules.Components.CN cnNodes := topology.Nodes.CN // Skip if CN nodes is nil. errList = append(errList, v.validateReplicasOnStatelessComponent(ctx, cnRules, field.NewPath("spec", "topology", "rules", "cn"), int(*cnNodes.Replicas))...) cdcRules := topology.Rules.Components.CDC cdcNodes := topology.Nodes.CDC // Skip if CDC nodes is nil. if cdcNodes != nil { errList = append(errList, v.validateReplicasOnStatelessComponent(ctx, cdcRules, field.NewPath("spec", "topology", "rules", "cdc"), int(cdcNodes.Replicas+cdcNodes.XReplicas))...) } columnarRules := topology.Rules.Components.Columnar columnarNodes := topology.Nodes.Columnar // Skip if Columnar nodes is nil. if columnarNodes != nil { errList = append(errList, v.validateReplicasOnStatelessComponent(ctx, columnarRules, field.NewPath("spec", "topology", "rules", "columnar"), int(columnarNodes.Replicas))...) } return errList } func (v *PolarDBXClusterV1Validator) validateTopology(ctx context.Context, topology *polardbxv1polardbx.Topology) field.ErrorList { var errList field.ErrorList errList = append(errList, v.validateTopologyRules(ctx, &topology.Rules)...) errList = append(errList, v.validateTopologyNodes(ctx, &topology.Nodes)...) errList = append(errList, v.validateReplicas(ctx, topology)...) return errList } func (v *PolarDBXClusterV1Validator) validatePrivileges(ctx context.Context, privileges []polardbxv1polardbx.PrivilegeItem) field.ErrorList { var errList field.ErrorList usernames := make(map[string]int) fieldPath := field.NewPath("spec", "privileges") for index, priv := range privileges { if priv.Username == "" { errList = append(errList, field.NotFound( fieldPath.Index(index).Child("username"), ""), ) } else { _, ok := usernames[priv.Username] if ok { errList = append(errList, field.Duplicate( fieldPath.Index(index).Child("username"), priv.Username), ) } else { usernames[priv.Username] = index } } switch priv.Type { case polardbxv1polardbx.ReadWrite, polardbxv1polardbx.ReadOnly, polardbxv1polardbx.DDLOnly, polardbxv1polardbx.DMLOnly, polardbxv1polardbx.Super, "": // break default: errList = append(errList, field.NotSupported( fieldPath.Index(index).Child("type"), priv.Type, []string{ string(polardbxv1polardbx.ReadWrite), string(polardbxv1polardbx.ReadOnly), string(polardbxv1polardbx.DDLOnly), string(polardbxv1polardbx.DMLOnly), string(polardbxv1polardbx.Super), }), ) } } return errList } func (v *PolarDBXClusterV1Validator) validateConfig(ctx context.Context, config *polardbxv1polardbx.Config) field.ErrorList { var errList field.ErrorList fieldPath := field.NewPath("spec", "config") // Check DN's overwrite. if config.DN.MycnfOverwrite != "" { _, err := iniutil.ParseMyCnfOverlayFile(bytes.NewBufferString(config.DN.MycnfOverwrite)) if err != nil { errList = append(errList, field.Invalid( fieldPath.Child("config", "dn", "mycnfOverwrite"), config.DN.MycnfOverwrite, "invalid format, parse error: "+err.Error()), ) } } return errList } func (v *PolarDBXClusterV1Validator) validate(ctx context.Context, polardbx *polardbxv1.PolarDBXCluster) error { spec := &polardbx.Spec errList := field.ErrorList{} errList = append(errList, v.validateTopology(ctx, &polardbx.Spec.Topology)...) errList = append(errList, v.validateConfig(ctx, &polardbx.Spec.Config)...) errList = append(errList, v.validatePrivileges(ctx, polardbx.Spec.Privileges)...) errList = append(errList, v.validateSecurity(ctx, polardbx.Spec.Security)...) if spec.Readonly { if spec.PrimaryCluster == "" { errList = append(errList, field.Invalid( field.NewPath("spec", "primaryCluster"), spec.PrimaryCluster, fmt.Sprintf(`primaryCluster cannot be empty for readonly pxc`), )) } } switch spec.ProtocolVersion.String() { case "5", "5.7", "8", "8.0": // break default: errList = append(errList, field.NotSupported( field.NewPath("spec", "protocolVersion"), spec.ProtocolVersion, []string{ "5", "5.7", "8", "8.0", }), ) } staticConfig := spec.Config.CN.Static if staticConfig != nil { switch staticConfig.RPCProtocolVersion.String() { case "1", "2", "": default: errList = append(errList, field.NotSupported( field.NewPath("spec", "RPCProtocolVersion"), spec.Config.CN.Static.RPCProtocolVersion, []string{ "1", "2", }), ) } } switch spec.ServiceType { case corev1.ServiceTypeClusterIP, corev1.ServiceTypeNodePort, corev1.ServiceTypeLoadBalancer, corev1.ServiceTypeExternalName: // break default: errList = append(errList, field.NotSupported( field.NewPath("spec", "serviceType"), spec.ServiceType, []string{ string(corev1.ServiceTypeClusterIP), string(corev1.ServiceTypeNodePort), string(corev1.ServiceTypeLoadBalancer), string(corev1.ServiceTypeExternalName), }), ) } switch spec.UpgradeStrategy { case polardbxv1polardbx.RecreateUpgradeStrategy, polardbxv1polardbx.RollingUpgradeStrategy: // break default: errList = append(errList, field.NotSupported( field.NewPath("spec", "upgradeStrategy"), spec.UpgradeStrategy, []string{ string(polardbxv1polardbx.RecreateUpgradeStrategy), string(polardbxv1polardbx.RollingUpgradeStrategy), })) } if len(errList) > 0 { return apierrors.NewInvalid( polardbx.GroupVersionKind().GroupKind(), polardbx.Name, errList) } return nil } func (v *PolarDBXClusterV1Validator) ValidateCreate(ctx context.Context, obj runtime.Object) error { return v.validate(ctx, obj.(*polardbxv1.PolarDBXCluster)) } func (v *PolarDBXClusterV1Validator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error { old, new := oldObj.(*polardbxv1.PolarDBXCluster), newObj.(*polardbxv1.PolarDBXCluster) gvk := old.GroupVersionKind() // Validate the immutable fields, such as storage engine. oldSpec, newSpec := &old.Spec, &new.Spec if oldSpec.ShareGMS != newSpec.ShareGMS { return apierrors.NewForbidden( schema.GroupResource{ Group: gvk.Group, Resource: gvk.Kind, }, new.Name, field.Forbidden(field.NewPath("spec").Child("shareGMS"), "field is immutable"), ) } if oldSpec.Readonly != newSpec.Readonly { return apierrors.NewForbidden( schema.GroupResource{ Group: gvk.Group, Resource: gvk.Kind, }, new.Name, field.Forbidden(field.NewPath("spec").Child("readonly"), "field is immutable"), ) } if oldSpec.PrimaryCluster != newSpec.PrimaryCluster { return apierrors.NewForbidden( schema.GroupResource{ Group: gvk.Group, Resource: gvk.Kind, }, new.Name, field.Forbidden(field.NewPath("spec").Child("primaryCluster"), "field is immutable"), ) } oldStorageEngine := oldSpec.Topology.Nodes.DN.Template.Engine newStorageEngine := newSpec.Topology.Nodes.DN.Template.Engine if oldStorageEngine != newStorageEngine { return apierrors.NewForbidden( schema.GroupResource{ Group: gvk.Group, Resource: gvk.Kind, }, new.Name, field.Forbidden(field.NewPath("spec"). Child("topology"). Child("nodes"). Child("dn"). Child("template"). Child("engine"), "storage engine can not be changed"), ) } oldGmsStorageEngine := oldStorageEngine if oldSpec.Topology.Nodes.GMS.Template != nil { oldGmsStorageEngine = oldSpec.Topology.Nodes.GMS.Template.Engine } newGmsStorageEngine := newStorageEngine if newSpec.Topology.Nodes.GMS.Template != nil { newGmsStorageEngine = newSpec.Topology.Nodes.GMS.Template.Engine } if oldGmsStorageEngine != newGmsStorageEngine { return apierrors.NewForbidden( schema.GroupResource{ Group: gvk.Group, Resource: gvk.Kind, }, new.Name, field.Forbidden(field.NewPath("spec"). Child("topology"). Child("nodes"). Child("gms"). Child("template"). Child("engine"), "storage engine can not be changed"), ) } if !equality.Semantic.DeepEqual(oldSpec.Security, newSpec.Security) { return apierrors.NewForbidden( schema.GroupResource{ Group: gvk.Group, Resource: gvk.Kind, }, new.Name, field.Forbidden( field.NewPath("spec", "security"), "security is immutable", ), ) } if !equality.Semantic.DeepEqual(oldSpec.InitReadonly, newSpec.InitReadonly) { return apierrors.NewForbidden( schema.GroupResource{ Group: gvk.Group, Resource: gvk.Kind, }, new.Name, field.Forbidden( field.NewPath("spec", "initReadonly"), "initReadonly is immutable", ), ) } if !equality.Semantic.DeepEqual(oldSpec.Config.CN.ColdDataFileStorage, newSpec.Config.CN.ColdDataFileStorage) { return apierrors.NewForbidden( schema.GroupResource{ Group: gvk.Group, Resource: gvk.Kind, }, new.Name, field.Forbidden( field.NewPath("spec", "config", "cn", "coldDataFileStorage"), "coldDataFileStorage is immutable", ), ) } // Validate the new object at last. return v.validate(ctx, new) } func (v *PolarDBXClusterV1Validator) ValidateDelete(ctx context.Context, obj runtime.Object) error { return nil } func NewPolarDBXClusterV1Validator(configLoader func() *ValidatorConfig) extension.CustomValidator { return &PolarDBXClusterV1Validator{ configLoader: configLoader, } }