diff --git a/api/v1beta1/clusterclass_types.go b/api/v1beta1/clusterclass_types.go index b637fcc26126..661d571083a9 100644 --- a/api/v1beta1/clusterclass_types.go +++ b/api/v1beta1/clusterclass_types.go @@ -100,6 +100,10 @@ type ClusterClassSpec struct { // +optional Infrastructure LocalObjectTemplate `json:"infrastructure,omitempty"` + // infrastructureNamingStrategy allows changing the naming pattern used when creating the infrastructure object. + // +optional + InfrastructureNamingStrategy *InfrastructureNamingStrategy `json:"infrastructureNamingStrategy,omitempty"` + // controlPlane is a reference to a local struct that holds the details // for provisioning the Control Plane for the Cluster. // +optional @@ -207,6 +211,19 @@ type ControlPlaneClassNamingStrategy struct { Template *string `json:"template,omitempty"` } +// InfrastructureNamingStrategy defines the naming strategy for infrastructure objects. +type InfrastructureNamingStrategy struct { + // template defines the template to use for generating the name of the Infrastructure object. + // If not defined, it will fallback to `{{ .cluster.name }}-{{ .random }}`. + // If the templated string exceeds 63 characters, it will be trimmed to 58 characters and will + // get concatenated with a random suffix of length 5. + // The templating mechanism provides the following arguments: + // * `.cluster.name`: The name of the cluster object. + // * `.random`: A random alphanumeric string, without vowels, of length 5. + // +optional + Template *string `json:"template,omitempty"` +} + // WorkersClass is a collection of deployment classes. type WorkersClass struct { // machineDeployments is a list of machine deployment classes that can be used to create diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 7b6656d83d1e..ec25c1dfb000 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -211,6 +211,11 @@ func (in *ClusterClassSpec) DeepCopyInto(out *ClusterClassSpec) { copy(*out, *in) } in.Infrastructure.DeepCopyInto(&out.Infrastructure) + if in.InfrastructureNamingStrategy != nil { + in, out := &in.InfrastructureNamingStrategy, &out.InfrastructureNamingStrategy + *out = new(InfrastructureNamingStrategy) + (*in).DeepCopyInto(*out) + } in.ControlPlane.DeepCopyInto(&out.ControlPlane) in.Workers.DeepCopyInto(&out.Workers) if in.Variables != nil { @@ -877,6 +882,26 @@ func (in FailureDomains) DeepCopy() FailureDomains { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureNamingStrategy) DeepCopyInto(out *InfrastructureNamingStrategy) { + *out = *in + if in.Template != nil { + in, out := &in.Template, &out.Template + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureNamingStrategy. +func (in *InfrastructureNamingStrategy) DeepCopy() *InfrastructureNamingStrategy { + if in == nil { + return nil + } + out := new(InfrastructureNamingStrategy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JSONPatch) DeepCopyInto(out *JSONPatch) { *out = *in diff --git a/api/v1beta1/zz_generated.openapi.go b/api/v1beta1/zz_generated.openapi.go index eceda3181e20..9c211d64866e 100644 --- a/api/v1beta1/zz_generated.openapi.go +++ b/api/v1beta1/zz_generated.openapi.go @@ -56,6 +56,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneVariables": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneVariables(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ExternalPatchDefinition": schema_sigsk8sio_cluster_api_api_v1beta1_ExternalPatchDefinition(ref), "sigs.k8s.io/cluster-api/api/v1beta1.FailureDomainSpec": schema_sigsk8sio_cluster_api_api_v1beta1_FailureDomainSpec(ref), + "sigs.k8s.io/cluster-api/api/v1beta1.InfrastructureNamingStrategy": schema_sigsk8sio_cluster_api_api_v1beta1_InfrastructureNamingStrategy(ref), "sigs.k8s.io/cluster-api/api/v1beta1.JSONPatch": schema_sigsk8sio_cluster_api_api_v1beta1_JSONPatch(ref), "sigs.k8s.io/cluster-api/api/v1beta1.JSONPatchValue": schema_sigsk8sio_cluster_api_api_v1beta1_JSONPatchValue(ref), "sigs.k8s.io/cluster-api/api/v1beta1.JSONSchemaProps": schema_sigsk8sio_cluster_api_api_v1beta1_JSONSchemaProps(ref), @@ -441,6 +442,12 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassSpec(ref common.Refere Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.LocalObjectTemplate"), }, }, + "infrastructureNamingStrategy": { + SchemaProps: spec.SchemaProps{ + Description: "infrastructureNamingStrategy allows changing the naming pattern used when creating the infrastructure object.", + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.InfrastructureNamingStrategy"), + }, + }, "controlPlane": { SchemaProps: spec.SchemaProps{ Description: "controlPlane is a reference to a local struct that holds the details for provisioning the Control Plane for the Cluster.", @@ -487,7 +494,7 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ClusterClassSpec(ref common.Refere }, }, Dependencies: []string{ - "sigs.k8s.io/cluster-api/api/v1beta1.ClusterAvailabilityGate", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassPatch", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassVariable", "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass", "sigs.k8s.io/cluster-api/api/v1beta1.LocalObjectTemplate", "sigs.k8s.io/cluster-api/api/v1beta1.WorkersClass"}, + "sigs.k8s.io/cluster-api/api/v1beta1.ClusterAvailabilityGate", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassPatch", "sigs.k8s.io/cluster-api/api/v1beta1.ClusterClassVariable", "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass", "sigs.k8s.io/cluster-api/api/v1beta1.InfrastructureNamingStrategy", "sigs.k8s.io/cluster-api/api/v1beta1.LocalObjectTemplate", "sigs.k8s.io/cluster-api/api/v1beta1.WorkersClass"}, } } @@ -1530,6 +1537,26 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_FailureDomainSpec(ref common.Refer } } +func schema_sigsk8sio_cluster_api_api_v1beta1_InfrastructureNamingStrategy(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "InfrastructureNamingStrategy defines the naming strategy for infrastructure objects.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "template": { + SchemaProps: spec.SchemaProps{ + Description: "template defines the template to use for generating the name of the Infrastructure object. If not defined, it will fallback to `{{ .cluster.name }}-{{ .random }}`. If the templated string exceeds 63 characters, it will be trimmed to 58 characters and will get concatenated with a random suffix of length 5. The templating mechanism provides the following arguments: * `.cluster.name`: The name of the cluster object. * `.random`: A random alphanumeric string, without vowels, of length 5.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + func schema_sigsk8sio_cluster_api_api_v1beta1_JSONPatch(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml index 2dba022d5072..8d6410a38a50 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml @@ -859,6 +859,21 @@ spec: required: - ref type: object + infrastructureNamingStrategy: + description: infrastructureNamingStrategy allows changing the naming + pattern used when creating the infrastructure object. + properties: + template: + description: |- + template defines the template to use for generating the name of the Infrastructure object. + If not defined, it will fallback to `{{ .cluster.name }}-{{ .random }}`. + If the templated string exceeds 63 characters, it will be trimmed to 58 characters and will + get concatenated with a random suffix of length 5. + The templating mechanism provides the following arguments: + * `.cluster.name`: The name of the cluster object. + * `.random`: A random alphanumeric string, without vowels, of length 5. + type: string + type: object patches: description: |- patches defines the patches which are applied to customize diff --git a/exp/topology/desiredstate/desired_state.go b/exp/topology/desiredstate/desired_state.go index a96dc5b13ec3..85fdd63a1ef1 100644 --- a/exp/topology/desiredstate/desired_state.go +++ b/exp/topology/desiredstate/desired_state.go @@ -193,11 +193,16 @@ func computeInfrastructureCluster(_ context.Context, s *scope.Scope) (*unstructu cluster := s.Current.Cluster currentRef := cluster.Spec.InfrastructureRef + nameTemplate := "{{ .cluster.name }}-{{ .random }}" + if s.Blueprint.ClusterClass.Spec.InfrastructureNamingStrategy != nil && s.Blueprint.ClusterClass.Spec.InfrastructureNamingStrategy.Template != nil { + nameTemplate = *s.Blueprint.ClusterClass.Spec.InfrastructureNamingStrategy.Template + } + infrastructureCluster, err := templateToObject(templateToInput{ template: template, templateClonedFromRef: templateClonedFromRef, cluster: cluster, - nameGenerator: topologynames.SimpleNameGenerator(fmt.Sprintf("%s-", cluster.Name)), + nameGenerator: topologynames.InfraClusterNameGenerator(nameTemplate, cluster.Name), currentObjectRef: currentRef, // Note: It is not possible to add an ownerRef to Cluster at this stage, otherwise the provisioning // of the infrastructure cluster starts no matter of the object being actually referenced by the Cluster itself. diff --git a/internal/apis/core/v1alpha4/conversion.go b/internal/apis/core/v1alpha4/conversion.go index f006f2895a65..6f0a1f49a998 100644 --- a/internal/apis/core/v1alpha4/conversion.go +++ b/internal/apis/core/v1alpha4/conversion.go @@ -133,6 +133,7 @@ func (src *ClusterClass) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.ControlPlane.MachineHealthCheck = restored.Spec.ControlPlane.MachineHealthCheck dst.Spec.ControlPlane.ReadinessGates = restored.Spec.ControlPlane.ReadinessGates dst.Spec.ControlPlane.NamingStrategy = restored.Spec.ControlPlane.NamingStrategy + dst.Spec.InfrastructureNamingStrategy = restored.Spec.InfrastructureNamingStrategy dst.Spec.ControlPlane.NodeDrainTimeout = restored.Spec.ControlPlane.NodeDrainTimeout dst.Spec.ControlPlane.NodeVolumeDetachTimeout = restored.Spec.ControlPlane.NodeVolumeDetachTimeout dst.Spec.ControlPlane.NodeDeletionTimeout = restored.Spec.ControlPlane.NodeDeletionTimeout diff --git a/internal/apis/core/v1alpha4/zz_generated.conversion.go b/internal/apis/core/v1alpha4/zz_generated.conversion.go index ca7076623f9e..c5bbfcfd5d90 100644 --- a/internal/apis/core/v1alpha4/zz_generated.conversion.go +++ b/internal/apis/core/v1alpha4/zz_generated.conversion.go @@ -646,6 +646,7 @@ func autoConvert_v1beta1_ClusterClassSpec_To_v1alpha4_ClusterClassSpec(in *v1bet if err := Convert_v1beta1_LocalObjectTemplate_To_v1alpha4_LocalObjectTemplate(&in.Infrastructure, &out.Infrastructure, s); err != nil { return err } + // WARNING: in.InfrastructureNamingStrategy requires manual conversion: does not exist in peer-type if err := Convert_v1beta1_ControlPlaneClass_To_v1alpha4_ControlPlaneClass(&in.ControlPlane, &out.ControlPlane, s); err != nil { return err } diff --git a/internal/topology/names/names.go b/internal/topology/names/names.go index f6d4259b7e8f..c43e376c3467 100644 --- a/internal/topology/names/names.go +++ b/internal/topology/names/names.go @@ -106,6 +106,12 @@ func MachineSetMachineNameGenerator(templateString, clusterName, machineSetName }) } +// InfraClusterNameGenerator returns a generator for creating a infrastructure cluster name. +func InfraClusterNameGenerator(templateString, clusterName string) NameGenerator { + return newTemplateGenerator(templateString, clusterName, + map[string]interface{}{}) +} + // templateGenerator parses the template string as text/template and executes it using // the passed data to generate a name. type templateGenerator struct { diff --git a/internal/webhooks/clusterclass.go b/internal/webhooks/clusterclass.go index 544f2dbd500a..cd98ec83ea7e 100644 --- a/internal/webhooks/clusterclass.go +++ b/internal/webhooks/clusterclass.go @@ -435,6 +435,23 @@ func validateMachineHealthCheckClasses(clusterClass *clusterv1.ClusterClass) fie func validateNamingStrategies(clusterClass *clusterv1.ClusterClass) field.ErrorList { var allErrs field.ErrorList + if clusterClass.Spec.InfrastructureNamingStrategy != nil && clusterClass.Spec.InfrastructureNamingStrategy.Template != nil { + name, err := topologynames.InfraClusterNameGenerator(*clusterClass.Spec.InfrastructureNamingStrategy.Template, "cluster").GenerateName() + templateFldPath := field.NewPath("spec", "infrastructureNamingStrategy", "template") + if err != nil { + allErrs = append(allErrs, + field.Invalid( + templateFldPath, + *clusterClass.Spec.InfrastructureNamingStrategy.Template, + fmt.Sprintf("invalid InfraCluster name template: %v", err), + )) + } else { + for _, err := range validation.IsDNS1123Subdomain(name) { + allErrs = append(allErrs, field.Invalid(templateFldPath, *clusterClass.Spec.InfrastructureNamingStrategy.Template, err)) + } + } + } + if clusterClass.Spec.ControlPlane.NamingStrategy != nil && clusterClass.Spec.ControlPlane.NamingStrategy.Template != nil { name, err := topologynames.ControlPlaneNameGenerator(*clusterClass.Spec.ControlPlane.NamingStrategy.Template, "cluster").GenerateName() templateFldPath := field.NewPath("spec", "controlPlane", "namingStrategy", "template") diff --git a/internal/webhooks/clusterclass_test.go b/internal/webhooks/clusterclass_test.go index aeb4a3249f6b..6447ed8e002f 100644 --- a/internal/webhooks/clusterclass_test.go +++ b/internal/webhooks/clusterclass_test.go @@ -1644,6 +1644,7 @@ func TestClusterClassValidation(t *testing.T) { in: builder.ClusterClass(metav1.NamespaceDefault, "class1"). WithInfrastructureClusterTemplate( builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "infra1").Build()). + WithInfraClusterStrategy(&clusterv1.InfrastructureNamingStrategy{Template: ptr.To("{{ .cluster.name }}-infra-{{ .random }}")}). WithControlPlaneTemplate( builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1"). Build()). @@ -1670,6 +1671,36 @@ func TestClusterClassValidation(t *testing.T) { Build(), expectErr: false, }, + { + name: "should return error for invalid InfraCluster InfrastructureNamingStrategy.template", + in: builder.ClusterClass(metav1.NamespaceDefault, "class1"). + WithInfrastructureClusterTemplate( + builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "infra1").Build()). + WithInfraClusterStrategy(&clusterv1.InfrastructureNamingStrategy{Template: ptr.To("template-infra-{{ .invalidkey }}")}). + WithControlPlaneTemplate( + builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1"). + Build()). + WithControlPlaneInfrastructureMachineTemplate( + builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "cpInfra1"). + Build()). + Build(), + expectErr: true, + }, + { + name: "should return error for invalid InfraCluster InfrastructureNamingStrategy.template when the generated name does not conform to RFC 1123", + in: builder.ClusterClass(metav1.NamespaceDefault, "class1"). + WithInfrastructureClusterTemplate( + builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "infra1").Build()). + WithInfraClusterStrategy(&clusterv1.InfrastructureNamingStrategy{Template: ptr.To("template-infra-{{ .cluster.name }}-")}). + WithControlPlaneTemplate( + builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1"). + Build()). + WithControlPlaneInfrastructureMachineTemplate( + builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "cpInfra1"). + Build()). + Build(), + expectErr: true, + }, { name: "should return error for invalid ControlPlane namingStrategy.template", in: builder.ClusterClass(metav1.NamespaceDefault, "class1"). diff --git a/test/e2e/data/infrastructure-docker/main/clusterclass-quick-start-runtimesdk.yaml b/test/e2e/data/infrastructure-docker/main/clusterclass-quick-start-runtimesdk.yaml index 538a26ddca8a..f8181538ecaa 100644 --- a/test/e2e/data/infrastructure-docker/main/clusterclass-quick-start-runtimesdk.yaml +++ b/test/e2e/data/infrastructure-docker/main/clusterclass-quick-start-runtimesdk.yaml @@ -20,6 +20,8 @@ spec: apiVersion: infrastructure.cluster.x-k8s.io/v1beta1 kind: DockerClusterTemplate name: quick-start-cluster + infrastructureNamingStrategy: + template: "{{ .cluster.name }}-infra-{{ .random }}" workers: machineDeployments: - class: default-worker diff --git a/util/test/builder/builders.go b/util/test/builder/builders.go index 91f843040787..5835e56b41d4 100644 --- a/util/test/builder/builders.go +++ b/util/test/builder/builders.go @@ -347,6 +347,7 @@ type ClusterClassBuilder struct { controlPlaneNodeVolumeDetachTimeout *metav1.Duration controlPlaneNodeDeletionTimeout *metav1.Duration controlPlaneNamingStrategy *clusterv1.ControlPlaneClassNamingStrategy + infraClusterNamingStrategy *clusterv1.InfrastructureNamingStrategy machineDeploymentClasses []clusterv1.MachineDeploymentClass machinePoolClasses []clusterv1.MachinePoolClass variables []clusterv1.ClusterClassVariable @@ -426,6 +427,12 @@ func (c *ClusterClassBuilder) WithControlPlaneNamingStrategy(n *clusterv1.Contro return c } +// WithInfraClusterStrategy sets the NamingStrategy for the infra cluster to the ClusterClassBuilder. +func (c *ClusterClassBuilder) WithInfraClusterStrategy(n *clusterv1.InfrastructureNamingStrategy) *ClusterClassBuilder { + c.infraClusterNamingStrategy = n + return c +} + // WithVariables adds the Variables to the ClusterClassBuilder. func (c *ClusterClassBuilder) WithVariables(vars ...clusterv1.ClusterClassVariable) *ClusterClassBuilder { c.variables = vars @@ -524,6 +531,9 @@ func (c *ClusterClassBuilder) Build() *clusterv1.ClusterClass { if c.controlPlaneNamingStrategy != nil { obj.Spec.ControlPlane.NamingStrategy = c.controlPlaneNamingStrategy } + if c.infraClusterNamingStrategy != nil { + obj.Spec.InfrastructureNamingStrategy = c.infraClusterNamingStrategy + } obj.Spec.Workers.MachineDeployments = c.machineDeploymentClasses obj.Spec.Workers.MachinePools = c.machinePoolClasses diff --git a/util/test/builder/zz_generated.deepcopy.go b/util/test/builder/zz_generated.deepcopy.go index 1a7ed63726f7..af7d0bdb9b76 100644 --- a/util/test/builder/zz_generated.deepcopy.go +++ b/util/test/builder/zz_generated.deepcopy.go @@ -163,6 +163,11 @@ func (in *ClusterClassBuilder) DeepCopyInto(out *ClusterClassBuilder) { *out = new(v1beta1.ControlPlaneClassNamingStrategy) (*in).DeepCopyInto(*out) } + if in.infraClusterNamingStrategy != nil { + in, out := &in.infraClusterNamingStrategy, &out.infraClusterNamingStrategy + *out = new(v1beta1.InfrastructureNamingStrategy) + (*in).DeepCopyInto(*out) + } if in.machineDeploymentClasses != nil { in, out := &in.machineDeploymentClasses, &out.machineDeploymentClasses *out = make([]v1beta1.MachineDeploymentClass, len(*in))