From a5ada5c2172db89d7863f38f8eb461099007f311 Mon Sep 17 00:00:00 2001 From: Christian Schlotter Date: Wed, 30 Aug 2023 10:49:16 +0200 Subject: [PATCH] ClusterClass: Introduce NamingStrategy and allow generating names using go templates --- api/v1alpha4/conversion.go | 2 + api/v1alpha4/zz_generated.conversion.go | 2 + api/v1beta1/clusterclass_types.go | 39 ++++++ api/v1beta1/zz_generated.deepcopy.go | 75 +++++++++++ api/v1beta1/zz_generated.openapi.go | 84 +++++++++++- .../cluster.x-k8s.io_clusterclasses.yaml | 27 ++++ .../topology/cluster/desired_state.go | 77 ++++++++--- .../topology/cluster/desired_state_test.go | 15 ++- .../topology/names/namegenerator.go | 127 ++++++++++++++++++ internal/test/builder/builders.go | 20 +++ .../test/builder/zz_generated.deepcopy.go | 10 ++ internal/webhooks/clusterclass.go | 37 +++++ internal/webhooks/clusterclass_test.go | 44 ++++++ 13 files changed, 531 insertions(+), 28 deletions(-) create mode 100644 internal/controllers/topology/names/namegenerator.go diff --git a/api/v1alpha4/conversion.go b/api/v1alpha4/conversion.go index 5b1c45c25a22..368dff1b1d94 100644 --- a/api/v1alpha4/conversion.go +++ b/api/v1alpha4/conversion.go @@ -124,6 +124,7 @@ func (src *ClusterClass) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Patches = restored.Spec.Patches dst.Spec.Variables = restored.Spec.Variables dst.Spec.ControlPlane.MachineHealthCheck = restored.Spec.ControlPlane.MachineHealthCheck + dst.Spec.ControlPlane.NamingStrategy = restored.Spec.ControlPlane.NamingStrategy dst.Spec.ControlPlane.NodeDrainTimeout = restored.Spec.ControlPlane.NodeDrainTimeout dst.Spec.ControlPlane.NodeVolumeDetachTimeout = restored.Spec.ControlPlane.NodeVolumeDetachTimeout dst.Spec.ControlPlane.NodeDeletionTimeout = restored.Spec.ControlPlane.NodeDeletionTimeout @@ -132,6 +133,7 @@ func (src *ClusterClass) ConvertTo(dstRaw conversion.Hub) error { for i := range restored.Spec.Workers.MachineDeployments { dst.Spec.Workers.MachineDeployments[i].MachineHealthCheck = restored.Spec.Workers.MachineDeployments[i].MachineHealthCheck dst.Spec.Workers.MachineDeployments[i].FailureDomain = restored.Spec.Workers.MachineDeployments[i].FailureDomain + dst.Spec.Workers.MachineDeployments[i].NamingStrategy = restored.Spec.Workers.MachineDeployments[i].NamingStrategy dst.Spec.Workers.MachineDeployments[i].NodeDrainTimeout = restored.Spec.Workers.MachineDeployments[i].NodeDrainTimeout dst.Spec.Workers.MachineDeployments[i].NodeVolumeDetachTimeout = restored.Spec.Workers.MachineDeployments[i].NodeVolumeDetachTimeout dst.Spec.Workers.MachineDeployments[i].NodeDeletionTimeout = restored.Spec.Workers.MachineDeployments[i].NodeDeletionTimeout diff --git a/api/v1alpha4/zz_generated.conversion.go b/api/v1alpha4/zz_generated.conversion.go index cbdf0746ea3a..e89720cc15fc 100644 --- a/api/v1alpha4/zz_generated.conversion.go +++ b/api/v1alpha4/zz_generated.conversion.go @@ -863,6 +863,7 @@ func autoConvert_v1beta1_ControlPlaneClass_To_v1alpha4_ControlPlaneClass(in *v1b } out.MachineInfrastructure = (*LocalObjectTemplate)(unsafe.Pointer(in.MachineInfrastructure)) // WARNING: in.MachineHealthCheck requires manual conversion: does not exist in peer-type + // WARNING: in.NamingStrategy requires manual conversion: does not exist in peer-type // WARNING: in.NodeDrainTimeout requires manual conversion: does not exist in peer-type // WARNING: in.NodeVolumeDetachTimeout requires manual conversion: does not exist in peer-type // WARNING: in.NodeDeletionTimeout requires manual conversion: does not exist in peer-type @@ -1042,6 +1043,7 @@ func autoConvert_v1beta1_MachineDeploymentClass_To_v1alpha4_MachineDeploymentCla } // WARNING: in.MachineHealthCheck requires manual conversion: does not exist in peer-type // WARNING: in.FailureDomain requires manual conversion: does not exist in peer-type + // WARNING: in.NamingStrategy requires manual conversion: does not exist in peer-type // WARNING: in.NodeDrainTimeout requires manual conversion: does not exist in peer-type // WARNING: in.NodeVolumeDetachTimeout requires manual conversion: does not exist in peer-type // WARNING: in.NodeDeletionTimeout requires manual conversion: does not exist in peer-type diff --git a/api/v1beta1/clusterclass_types.go b/api/v1beta1/clusterclass_types.go index f699df58143d..e2043d6b5748 100644 --- a/api/v1beta1/clusterclass_types.go +++ b/api/v1beta1/clusterclass_types.go @@ -106,6 +106,10 @@ type ControlPlaneClass struct { // +optional MachineHealthCheck *MachineHealthCheckClass `json:"machineHealthCheck,omitempty"` + // NamingStrategy allows to change the naming pattern used when creating the control plane provider object. + // If not defined, it will fallback to `{{ .cluster.name }}-{{ .random }}`. + NamingStrategy *ControlPlaneClassNamingStrategy `json:"namingStrategy,omitempty"` + // NodeDrainTimeout is the total amount of time that the controller will spend on draining a node. // The default value is 0, meaning that the node can be drained without any time limitations. // NOTE: NodeDrainTimeout is different from `kubectl drain --timeout` @@ -127,6 +131,15 @@ type ControlPlaneClass struct { NodeDeletionTimeout *metav1.Duration `json:"nodeDeletionTimeout,omitempty"` } +// ControlPlaneClassNamingStrategy defines a naming strategy for templated objects of a ControlPlaneClass. +type ControlPlaneClassNamingStrategy struct { + // Template defines the template to use for generating the name of the ControlPlane 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. + 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 @@ -162,6 +175,10 @@ type MachineDeploymentClass struct { // +optional FailureDomain *string `json:"failureDomain,omitempty"` + // NamingStrategy allows to change the naming pattern used when creating the MachineDeployment. + // If not defined, it will fallback to `{{ .cluster.name }}-{{ .machineDeployment.topologyName }}-{{ .random }}`. + NamingStrategy *MachineDeploymentClassNamingStrategy `json:"namingStrategy,omitempty"` + // NodeDrainTimeout is the total amount of time that the controller will spend on draining a node. // The default value is 0, meaning that the node can be drained without any time limitations. // NOTE: NodeDrainTimeout is different from `kubectl drain --timeout` @@ -212,6 +229,15 @@ type MachineDeploymentClassTemplate struct { Infrastructure LocalObjectTemplate `json:"infrastructure"` } +// MachineDeploymentClassNamingStrategy defines a naming strategy for templated objects of a MachineDeploymentClass. +type MachineDeploymentClassNamingStrategy struct { + // Template defines the template to use for generating the name of the MachineDeployment object. + // If not defined, it will fallback to `{{ .cluster.name }}-{{ .machineDeployment.topologyName }}-{{ .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. + Template *string `json:"template,omitempty"` +} + // MachineHealthCheckClass defines a MachineHealthCheck for a group of Machines. type MachineHealthCheckClass struct { // UnhealthyConditions contains a list of the conditions that determine @@ -267,6 +293,10 @@ type MachinePoolClass struct { // +optional FailureDomains []string `json:"failureDomains,omitempty"` + // NamingStrategy allows to change the naming pattern used when creating the MachinePool. + // If not defined, it will fallback to `{{ .cluster.name }}-{{ .machinePool.topologyName }}-{{ .random }}`. + NamingStrategy *MachinePoolClassNamingStrategy `json:"namingStrategy,omitempty"` + // NodeDrainTimeout is the total amount of time that the controller will spend on draining a node. // The default value is 0, meaning that the node can be drained without any time limitations. // NOTE: NodeDrainTimeout is different from `kubectl drain --timeout` @@ -312,6 +342,15 @@ type MachinePoolClassTemplate struct { Infrastructure LocalObjectTemplate `json:"infrastructure"` } +// MachinePoolClassNamingStrategy defines a naming strategy for templated objects of a MachinePoolClass. +type MachinePoolClassNamingStrategy struct { + // Template defines the template to use for generating the name of the MachinePool object. + // If not defined, it will fallback to `{{ .cluster.name }}-{{ .machinePool.topologyName }}-{{ .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. + Template *string `json:"template,omitempty"` +} + // IsZero returns true if none of the values of MachineHealthCheckClass are defined. func (m MachineHealthCheckClass) IsZero() bool { return reflect.ValueOf(m).IsZero() diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index b9698d606e33..7eb964845925 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -507,6 +507,11 @@ func (in *ControlPlaneClass) DeepCopyInto(out *ControlPlaneClass) { *out = new(MachineHealthCheckClass) (*in).DeepCopyInto(*out) } + if in.NamingStrategy != nil { + in, out := &in.NamingStrategy, &out.NamingStrategy + *out = new(ControlPlaneClassNamingStrategy) + (*in).DeepCopyInto(*out) + } if in.NodeDrainTimeout != nil { in, out := &in.NodeDrainTimeout, &out.NodeDrainTimeout *out = new(metav1.Duration) @@ -534,6 +539,26 @@ func (in *ControlPlaneClass) DeepCopy() *ControlPlaneClass { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlaneClassNamingStrategy) DeepCopyInto(out *ControlPlaneClassNamingStrategy) { + *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 ControlPlaneClassNamingStrategy. +func (in *ControlPlaneClassNamingStrategy) DeepCopy() *ControlPlaneClassNamingStrategy { + if in == nil { + return nil + } + out := new(ControlPlaneClassNamingStrategy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ControlPlaneTopology) DeepCopyInto(out *ControlPlaneTopology) { *out = *in @@ -911,6 +936,11 @@ func (in *MachineDeploymentClass) DeepCopyInto(out *MachineDeploymentClass) { *out = new(string) **out = **in } + if in.NamingStrategy != nil { + in, out := &in.NamingStrategy, &out.NamingStrategy + *out = new(MachineDeploymentClassNamingStrategy) + (*in).DeepCopyInto(*out) + } if in.NodeDrainTimeout != nil { in, out := &in.NodeDrainTimeout, &out.NodeDrainTimeout *out = new(metav1.Duration) @@ -948,6 +978,26 @@ func (in *MachineDeploymentClass) DeepCopy() *MachineDeploymentClass { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineDeploymentClassNamingStrategy) DeepCopyInto(out *MachineDeploymentClassNamingStrategy) { + *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 MachineDeploymentClassNamingStrategy. +func (in *MachineDeploymentClassNamingStrategy) DeepCopy() *MachineDeploymentClassNamingStrategy { + if in == nil { + return nil + } + out := new(MachineDeploymentClassNamingStrategy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MachineDeploymentClassTemplate) DeepCopyInto(out *MachineDeploymentClassTemplate) { *out = *in @@ -1398,6 +1448,11 @@ func (in *MachinePoolClass) DeepCopyInto(out *MachinePoolClass) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.NamingStrategy != nil { + in, out := &in.NamingStrategy, &out.NamingStrategy + *out = new(MachinePoolClassNamingStrategy) + (*in).DeepCopyInto(*out) + } if in.NodeDrainTimeout != nil { in, out := &in.NodeDrainTimeout, &out.NodeDrainTimeout *out = new(metav1.Duration) @@ -1430,6 +1485,26 @@ func (in *MachinePoolClass) DeepCopy() *MachinePoolClass { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachinePoolClassNamingStrategy) DeepCopyInto(out *MachinePoolClassNamingStrategy) { + *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 MachinePoolClassNamingStrategy. +func (in *MachinePoolClassNamingStrategy) DeepCopy() *MachinePoolClassNamingStrategy { + if in == nil { + return nil + } + out := new(MachinePoolClassNamingStrategy) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MachinePoolClassTemplate) DeepCopyInto(out *MachinePoolClassTemplate) { *out = *in diff --git a/api/v1beta1/zz_generated.openapi.go b/api/v1beta1/zz_generated.openapi.go index 8526a8cf22cf..83a0840cd103 100644 --- a/api/v1beta1/zz_generated.openapi.go +++ b/api/v1beta1/zz_generated.openapi.go @@ -48,6 +48,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "sigs.k8s.io/cluster-api/api/v1beta1.ClusterVariable": schema_sigsk8sio_cluster_api_api_v1beta1_ClusterVariable(ref), "sigs.k8s.io/cluster-api/api/v1beta1.Condition": schema_sigsk8sio_cluster_api_api_v1beta1_Condition(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClass": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneClass(ref), + "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClassNamingStrategy": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneClassNamingStrategy(ref), "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneTopology": schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneTopology(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), @@ -59,6 +60,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "sigs.k8s.io/cluster-api/api/v1beta1.MachineAddress": schema_sigsk8sio_cluster_api_api_v1beta1_MachineAddress(ref), "sigs.k8s.io/cluster-api/api/v1beta1.MachineDeployment": schema_sigsk8sio_cluster_api_api_v1beta1_MachineDeployment(ref), "sigs.k8s.io/cluster-api/api/v1beta1.MachineDeploymentClass": schema_sigsk8sio_cluster_api_api_v1beta1_MachineDeploymentClass(ref), + "sigs.k8s.io/cluster-api/api/v1beta1.MachineDeploymentClassNamingStrategy": schema_sigsk8sio_cluster_api_api_v1beta1_MachineDeploymentClassNamingStrategy(ref), "sigs.k8s.io/cluster-api/api/v1beta1.MachineDeploymentClassTemplate": schema_sigsk8sio_cluster_api_api_v1beta1_MachineDeploymentClassTemplate(ref), "sigs.k8s.io/cluster-api/api/v1beta1.MachineDeploymentList": schema_sigsk8sio_cluster_api_api_v1beta1_MachineDeploymentList(ref), "sigs.k8s.io/cluster-api/api/v1beta1.MachineDeploymentSpec": schema_sigsk8sio_cluster_api_api_v1beta1_MachineDeploymentSpec(ref), @@ -74,6 +76,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "sigs.k8s.io/cluster-api/api/v1beta1.MachineHealthCheckTopology": schema_sigsk8sio_cluster_api_api_v1beta1_MachineHealthCheckTopology(ref), "sigs.k8s.io/cluster-api/api/v1beta1.MachineList": schema_sigsk8sio_cluster_api_api_v1beta1_MachineList(ref), "sigs.k8s.io/cluster-api/api/v1beta1.MachinePoolClass": schema_sigsk8sio_cluster_api_api_v1beta1_MachinePoolClass(ref), + "sigs.k8s.io/cluster-api/api/v1beta1.MachinePoolClassNamingStrategy": schema_sigsk8sio_cluster_api_api_v1beta1_MachinePoolClassNamingStrategy(ref), "sigs.k8s.io/cluster-api/api/v1beta1.MachinePoolClassTemplate": schema_sigsk8sio_cluster_api_api_v1beta1_MachinePoolClassTemplate(ref), "sigs.k8s.io/cluster-api/api/v1beta1.MachinePoolTopology": schema_sigsk8sio_cluster_api_api_v1beta1_MachinePoolTopology(ref), "sigs.k8s.io/cluster-api/api/v1beta1.MachinePoolVariables": schema_sigsk8sio_cluster_api_api_v1beta1_MachinePoolVariables(ref), @@ -958,6 +961,12 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneClass(ref common.Refer Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.MachineHealthCheckClass"), }, }, + "namingStrategy": { + SchemaProps: spec.SchemaProps{ + Description: "NamingStrategy allows to change the naming pattern used when creating the control plane provider object. If not defined, it will fallback to `{{ .cluster.name }}-{{ .random }}`", + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClassNamingStrategy"), + }, + }, "nodeDrainTimeout": { SchemaProps: spec.SchemaProps{ Description: "NodeDrainTimeout is the total amount of time that the controller will spend on draining a node. The default value is 0, meaning that the node can be drained without any time limitations. NOTE: NodeDrainTimeout is different from `kubectl drain --timeout` NOTE: This value can be overridden while defining a Cluster.Topology.", @@ -981,7 +990,26 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneClass(ref common.Refer }, }, Dependencies: []string{ - "k8s.io/api/core/v1.ObjectReference", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "sigs.k8s.io/cluster-api/api/v1beta1.LocalObjectTemplate", "sigs.k8s.io/cluster-api/api/v1beta1.MachineHealthCheckClass", "sigs.k8s.io/cluster-api/api/v1beta1.ObjectMeta"}, + "k8s.io/api/core/v1.ObjectReference", "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "sigs.k8s.io/cluster-api/api/v1beta1.ControlPlaneClassNamingStrategy", "sigs.k8s.io/cluster-api/api/v1beta1.LocalObjectTemplate", "sigs.k8s.io/cluster-api/api/v1beta1.MachineHealthCheckClass", "sigs.k8s.io/cluster-api/api/v1beta1.ObjectMeta"}, + } +} + +func schema_sigsk8sio_cluster_api_api_v1beta1_ControlPlaneClassNamingStrategy(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ControlPlaneClassNamingStrategy defines a naming strategy for templated objects of a ControlPlaneClass.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "template": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, } } @@ -1559,6 +1587,12 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_MachineDeploymentClass(ref common. Format: "", }, }, + "namingStrategy": { + SchemaProps: spec.SchemaProps{ + Description: "NamingStrategy allows to change the naming pattern used when creating the MachineDeployment. If not defined, it will fallback to `{{ .cluster.name }}-{{ .machineDeployment.topologyName }}-{{ .random }}`", + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.MachineDeploymentClassNamingStrategy"), + }, + }, "nodeDrainTimeout": { SchemaProps: spec.SchemaProps{ Description: "NodeDrainTimeout is the total amount of time that the controller will spend on draining a node. The default value is 0, meaning that the node can be drained without any time limitations. NOTE: NodeDrainTimeout is different from `kubectl drain --timeout` NOTE: This value can be overridden while defining a Cluster.Topology using this MachineDeploymentClass.", @@ -1595,7 +1629,26 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_MachineDeploymentClass(ref common. }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "sigs.k8s.io/cluster-api/api/v1beta1.MachineDeploymentClassTemplate", "sigs.k8s.io/cluster-api/api/v1beta1.MachineDeploymentStrategy", "sigs.k8s.io/cluster-api/api/v1beta1.MachineHealthCheckClass"}, + "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "sigs.k8s.io/cluster-api/api/v1beta1.MachineDeploymentClassNamingStrategy", "sigs.k8s.io/cluster-api/api/v1beta1.MachineDeploymentClassTemplate", "sigs.k8s.io/cluster-api/api/v1beta1.MachineDeploymentStrategy", "sigs.k8s.io/cluster-api/api/v1beta1.MachineHealthCheckClass"}, + } +} + +func schema_sigsk8sio_cluster_api_api_v1beta1_MachineDeploymentClassNamingStrategy(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "MachineDeploymentClassNamingStrategy defines a naming strategy for templated objects of a MachineDeploymentClass.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "template": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, } } @@ -2457,6 +2510,12 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_MachinePoolClass(ref common.Refere }, }, }, + "namingStrategy": { + SchemaProps: spec.SchemaProps{ + Description: "NamingStrategy allows to change the naming pattern used when creating the MachinePool. If not defined, it will fallback to `{{ .cluster.name }}-{{ .machinePool.topologyName }}-{{ .random }}`", + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta1.MachinePoolClassNamingStrategy"), + }, + }, "nodeDrainTimeout": { SchemaProps: spec.SchemaProps{ Description: "NodeDrainTimeout is the total amount of time that the controller will spend on draining a node. The default value is 0, meaning that the node can be drained without any time limitations. NOTE: NodeDrainTimeout is different from `kubectl drain --timeout` NOTE: This value can be overridden while defining a Cluster.Topology using this MachinePoolClass.", @@ -2487,7 +2546,26 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_MachinePoolClass(ref common.Refere }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "sigs.k8s.io/cluster-api/api/v1beta1.MachinePoolClassTemplate"}, + "k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "sigs.k8s.io/cluster-api/api/v1beta1.MachinePoolClassNamingStrategy", "sigs.k8s.io/cluster-api/api/v1beta1.MachinePoolClassTemplate"}, + } +} + +func schema_sigsk8sio_cluster_api_api_v1beta1_MachinePoolClassNamingStrategy(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "MachinePoolClassNamingStrategy defines a naming strategy for templated objects of a MachinePoolClass.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "template": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, } } diff --git a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml index d97c3b08035d..f27728077169 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusterclasses.yaml @@ -587,6 +587,15 @@ spec: More info: http://kubernetes.io/docs/user-guide/labels' type: object type: object + namingStrategy: + description: NamingStrategy allows to change the naming pattern + used when creating the control plane provider object. If not + defined, it will fallback to `{{ .cluster.name }}-{{ .random + }}` + properties: + template: + type: string + type: object nodeDeletionTimeout: description: 'NodeDeletionTimeout defines how long the controller will attempt to delete the Node that the Machine hosts after @@ -1168,6 +1177,15 @@ spec: using this MachineDeploymentClass.' format: int32 type: integer + namingStrategy: + description: NamingStrategy allows to change the naming + pattern used when creating the MachineDeployment. If not + defined, it will fallback to `{{ .cluster.name }}-{{ .machineDeployment.topologyName + }}-{{ .random }}` + properties: + template: + type: string + type: object nodeDeletionTimeout: description: 'NodeDeletionTimeout defines how long the controller will attempt to delete the Node that the Machine hosts @@ -1437,6 +1455,15 @@ spec: using this MachinePoolClass.' format: int32 type: integer + namingStrategy: + description: NamingStrategy allows to change the naming + pattern used when creating the MachinePool. If not defined, + it will fallback to `{{ .cluster.name }}-{{ .machinePool.topologyName + }}-{{ .random }}` + properties: + template: + type: string + type: object nodeDeletionTimeout: description: 'NodeDeletionTimeout defines how long the controller will attempt to delete the Node that the Machine hosts diff --git a/internal/controllers/topology/cluster/desired_state.go b/internal/controllers/topology/cluster/desired_state.go index 0aed86b5c517..54631ec36589 100644 --- a/internal/controllers/topology/cluster/desired_state.go +++ b/internal/controllers/topology/cluster/desired_state.go @@ -25,7 +25,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/storage/names" "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" @@ -37,6 +36,7 @@ import ( "sigs.k8s.io/cluster-api/feature" "sigs.k8s.io/cluster-api/internal/contract" "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/scope" + "sigs.k8s.io/cluster-api/internal/controllers/topology/names" "sigs.k8s.io/cluster-api/internal/hooks" tlog "sigs.k8s.io/cluster-api/internal/log" "sigs.k8s.io/cluster-api/internal/webhooks" @@ -161,7 +161,7 @@ func computeInfrastructureCluster(_ context.Context, s *scope.Scope) (*unstructu template: template, templateClonedFromRef: templateClonedFromRef, cluster: cluster, - namePrefix: fmt.Sprintf("%s-", cluster.Name), + nameGenerator: names.SimpleNameGenerator(fmt.Sprintf("%s-", 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. @@ -199,18 +199,17 @@ func computeControlPlaneInfrastructureMachineTemplate(_ context.Context, s *scop } } - controlPlaneInfrastructureMachineTemplate := templateToTemplate(templateToInput{ + return templateToTemplate(templateToInput{ template: template, templateClonedFromRef: templateClonedFromRef, cluster: cluster, - namePrefix: controlPlaneInfrastructureMachineTemplateNamePrefix(cluster.Name), + nameGenerator: names.SimpleNameGenerator(controlPlaneInfrastructureMachineTemplateNamePrefix(cluster.Name)), currentObjectRef: currentRef, // Note: we are adding an ownerRef to Cluster so the template will be automatically garbage collected // in case of errors in between creating this template and updating the Cluster object // with the reference to the ControlPlane object using this template. ownerRef: ownerReferenceTo(s.Current.Cluster), }) - return controlPlaneInfrastructureMachineTemplate, nil } // computeControlPlane computes the desired state for the ControlPlane object starting from the @@ -236,11 +235,16 @@ func (r *Reconciler) computeControlPlane(ctx context.Context, s *scope.Scope, in controlPlaneAnnotations := util.MergeMap(topologyMetadata.Annotations, clusterClassMetadata.Annotations) + nameTemplate := "{{ .cluster.name }}-{{ .random }}" + if s.Blueprint.ClusterClass.Spec.ControlPlane.NamingStrategy != nil && s.Blueprint.ClusterClass.Spec.ControlPlane.NamingStrategy.Template != nil { + nameTemplate = *s.Blueprint.ClusterClass.Spec.ControlPlane.NamingStrategy.Template + } + controlPlane, err := templateToObject(templateToInput{ template: template, templateClonedFromRef: templateClonedFromRef, cluster: cluster, - namePrefix: fmt.Sprintf("%s-", cluster.Name), + nameGenerator: names.ControlPlaneNameGenerator(nameTemplate, s.Current.Cluster.Name), currentObjectRef: currentRef, labels: controlPlaneLabels, annotations: controlPlaneAnnotations, @@ -609,17 +613,21 @@ func computeMachineDeployment(ctx context.Context, s *scope.Scope, machineDeploy if currentMachineDeployment != nil && currentMachineDeployment.BootstrapTemplate != nil { currentBootstrapTemplateRef = currentMachineDeployment.Object.Spec.Template.Spec.Bootstrap.ConfigRef } - desiredMachineDeployment.BootstrapTemplate = templateToTemplate(templateToInput{ + var err error + desiredMachineDeployment.BootstrapTemplate, err = templateToTemplate(templateToInput{ template: machineDeploymentBlueprint.BootstrapTemplate, templateClonedFromRef: contract.ObjToRef(machineDeploymentBlueprint.BootstrapTemplate), cluster: s.Current.Cluster, - namePrefix: bootstrapTemplateNamePrefix(s.Current.Cluster.Name, machineDeploymentTopology.Name), + nameGenerator: names.SimpleNameGenerator(bootstrapTemplateNamePrefix(s.Current.Cluster.Name, machineDeploymentTopology.Name)), currentObjectRef: currentBootstrapTemplateRef, // Note: we are adding an ownerRef to Cluster so the template will be automatically garbage collected // in case of errors in between creating this template and creating/updating the MachineDeployment object // with the reference to the ControlPlane object using this template. ownerRef: ownerReferenceTo(s.Current.Cluster), }) + if err != nil { + return nil, err + } bootstrapTemplateLabels := desiredMachineDeployment.BootstrapTemplate.GetLabels() if bootstrapTemplateLabels == nil { @@ -634,17 +642,20 @@ func computeMachineDeployment(ctx context.Context, s *scope.Scope, machineDeploy if currentMachineDeployment != nil && currentMachineDeployment.InfrastructureMachineTemplate != nil { currentInfraMachineTemplateRef = ¤tMachineDeployment.Object.Spec.Template.Spec.InfrastructureRef } - desiredMachineDeployment.InfrastructureMachineTemplate = templateToTemplate(templateToInput{ + desiredMachineDeployment.InfrastructureMachineTemplate, err = templateToTemplate(templateToInput{ template: machineDeploymentBlueprint.InfrastructureMachineTemplate, templateClonedFromRef: contract.ObjToRef(machineDeploymentBlueprint.InfrastructureMachineTemplate), cluster: s.Current.Cluster, - namePrefix: infrastructureMachineTemplateNamePrefix(s.Current.Cluster.Name, machineDeploymentTopology.Name), + nameGenerator: names.SimpleNameGenerator(infrastructureMachineTemplateNamePrefix(s.Current.Cluster.Name, machineDeploymentTopology.Name)), currentObjectRef: currentInfraMachineTemplateRef, // Note: we are adding an ownerRef to Cluster so the template will be automatically garbage collected // in case of errors in between creating this template and creating/updating the MachineDeployment object // with the reference to the ControlPlane object using this template. ownerRef: ownerReferenceTo(s.Current.Cluster), }) + if err != nil { + return nil, err + } infraMachineTemplateLabels := desiredMachineDeployment.InfrastructureMachineTemplate.GetLabels() if infraMachineTemplateLabels == nil { @@ -696,13 +707,23 @@ func computeMachineDeployment(ctx context.Context, s *scope.Scope, machineDeploy return nil, errors.Wrap(err, "failed to calculate desired infrastructure machine template ref") } + nameTemplate := "{{ .cluster.name }}-{{ .machineDeployment.topologyName }}-{{ .random }}" + if machineDeploymentClass.NamingStrategy != nil && machineDeploymentClass.NamingStrategy.Template != nil { + nameTemplate = *machineDeploymentClass.NamingStrategy.Template + } + + name, err := names.MachineDeploymentNameGenerator(nameTemplate, s.Current.Cluster.Name, machineDeploymentTopology.Name).GenerateName() + if err != nil { + return nil, errors.Wrap(err, "failed to generate name for machine deployment") + } + desiredMachineDeploymentObj := &clusterv1.MachineDeployment{ TypeMeta: metav1.TypeMeta{ Kind: clusterv1.GroupVersion.WithKind("MachineDeployment").Kind, APIVersion: clusterv1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-%s-", s.Current.Cluster.Name, machineDeploymentTopology.Name)), + Name: name, Namespace: s.Current.Cluster.Namespace, }, Spec: clusterv1.MachineDeploymentSpec{ @@ -951,7 +972,7 @@ func computeMachinePool(_ context.Context, s *scope.Scope, machinePoolTopology c template: machinePoolBlueprint.BootstrapTemplate, templateClonedFromRef: contract.ObjToRef(machinePoolBlueprint.BootstrapTemplate), cluster: s.Current.Cluster, - namePrefix: bootstrapConfigNamePrefix(s.Current.Cluster.Name, machinePoolTopology.Name), + nameGenerator: names.SimpleNameGenerator(bootstrapConfigNamePrefix(s.Current.Cluster.Name, machinePoolTopology.Name)), currentObjectRef: currentBootstrapConfigRef, }) if err != nil { @@ -975,7 +996,7 @@ func computeMachinePool(_ context.Context, s *scope.Scope, machinePoolTopology c template: machinePoolBlueprint.InfrastructureMachinePoolTemplate, templateClonedFromRef: contract.ObjToRef(machinePoolBlueprint.InfrastructureMachinePoolTemplate), cluster: s.Current.Cluster, - namePrefix: infrastructureMachinePoolNamePrefix(s.Current.Cluster.Name, machinePoolTopology.Name), + nameGenerator: names.SimpleNameGenerator(infrastructureMachinePoolNamePrefix(s.Current.Cluster.Name, machinePoolTopology.Name)), currentObjectRef: currentInfraMachinePoolRef, }) if err != nil { @@ -1027,13 +1048,23 @@ func computeMachinePool(_ context.Context, s *scope.Scope, machinePoolTopology c return nil, errors.Wrap(err, "failed to calculate desired infrastructure machine pool ref") } + nameTemplate := "{{ .cluster.name }}-{{ .machinePool.topologyName }}-{{ .random }}" + if machinePoolClass.NamingStrategy != nil && machinePoolClass.NamingStrategy.Template != nil { + nameTemplate = *machinePoolClass.NamingStrategy.Template + } + + name, err := names.MachinePoolName(nameTemplate, s.Current.Cluster.Name, machinePoolTopology.Name).GenerateName() + if err != nil { + return nil, errors.Wrap(err, "failed to generate name for machine pool") + } + desiredMachinePoolObj := &expv1.MachinePool{ TypeMeta: metav1.TypeMeta{ Kind: expv1.GroupVersion.WithKind("MachinePool").Kind, APIVersion: expv1.GroupVersion.String(), }, ObjectMeta: metav1.ObjectMeta{ - Name: names.SimpleNameGenerator.GenerateName(fmt.Sprintf("%s-%s-", s.Current.Cluster.Name, machinePoolTopology.Name)), + Name: name, Namespace: s.Current.Cluster.Namespace, }, Spec: expv1.MachinePoolSpec{ @@ -1190,7 +1221,7 @@ type templateToInput struct { template *unstructured.Unstructured templateClonedFromRef *corev1.ObjectReference cluster *clusterv1.Cluster - namePrefix string + nameGenerator names.NameGenerator currentObjectRef *corev1.ObjectReference labels map[string]string annotations map[string]string @@ -1230,7 +1261,11 @@ func templateToObject(in templateToInput) (*unstructured.Unstructured, error) { // Ensure the generated objects have a meaningful name. // NOTE: In case there is already a ref to this object in the Cluster, re-use the same name // in order to simplify compare at later stages of the reconcile process. - object.SetName(names.SimpleNameGenerator.GenerateName(in.namePrefix)) + name, err := in.nameGenerator.GenerateName() + if err != nil { + return nil, err + } + object.SetName(name) if in.currentObjectRef != nil && len(in.currentObjectRef.Name) > 0 { object.SetName(in.currentObjectRef.Name) } @@ -1243,7 +1278,7 @@ func templateToObject(in templateToInput) (*unstructured.Unstructured, error) { // and assigning a meaningful name (or reusing current reference name). // NOTE: We are creating a copy of the ClusterClass template for each cluster so // it is possible to add cluster specific information without affecting the original object. -func templateToTemplate(in templateToInput) *unstructured.Unstructured { +func templateToTemplate(in templateToInput) (*unstructured.Unstructured, error) { template := &unstructured.Unstructured{} in.template.DeepCopyInto(template) @@ -1290,12 +1325,16 @@ func templateToTemplate(in templateToInput) *unstructured.Unstructured { // Ensure the generated template gets a meaningful name. // NOTE: In case there is already an object ref to this template, it is required to re-use the same name // in order to simplify compare at later stages of the reconcile process. - template.SetName(names.SimpleNameGenerator.GenerateName(in.namePrefix)) + name, err := in.nameGenerator.GenerateName() + if err != nil { + return nil, err + } + template.SetName(name) if in.currentObjectRef != nil && len(in.currentObjectRef.Name) > 0 { template.SetName(in.currentObjectRef.Name) } - return template + return template, nil } func ownerReferenceTo(obj client.Object) *metav1.OwnerReference { diff --git a/internal/controllers/topology/cluster/desired_state_test.go b/internal/controllers/topology/cluster/desired_state_test.go index b2edb1d7b645..0b6cca21fb68 100644 --- a/internal/controllers/topology/cluster/desired_state_test.go +++ b/internal/controllers/topology/cluster/desired_state_test.go @@ -38,6 +38,7 @@ import ( "sigs.k8s.io/cluster-api/feature" "sigs.k8s.io/cluster-api/internal/contract" "sigs.k8s.io/cluster-api/internal/controllers/topology/cluster/scope" + topologynames "sigs.k8s.io/cluster-api/internal/controllers/topology/names" "sigs.k8s.io/cluster-api/internal/hooks" fakeruntimeclient "sigs.k8s.io/cluster-api/internal/runtime/client/fake" "sigs.k8s.io/cluster-api/internal/test/builder" @@ -1995,7 +1996,7 @@ func TestTemplateToObject(t *testing.T) { template: template, templateClonedFromRef: fakeRef1, cluster: cluster, - namePrefix: cluster.Name, + nameGenerator: topologynames.SimpleNameGenerator(cluster.Name), currentObjectRef: nil, }) g.Expect(err).ToNot(HaveOccurred()) @@ -2015,7 +2016,7 @@ func TestTemplateToObject(t *testing.T) { template: template, templateClonedFromRef: fakeRef1, cluster: cluster, - namePrefix: cluster.Name, + nameGenerator: topologynames.SimpleNameGenerator(cluster.Name), currentObjectRef: fakeRef2, }) g.Expect(err).ToNot(HaveOccurred()) @@ -2052,13 +2053,14 @@ func TestTemplateToTemplate(t *testing.T) { t.Run("Generates a template from a template", func(t *testing.T) { g := NewWithT(t) - obj := templateToTemplate(templateToInput{ + obj, err := templateToTemplate(templateToInput{ template: template, templateClonedFromRef: fakeRef1, cluster: cluster, - namePrefix: cluster.Name, + nameGenerator: topologynames.SimpleNameGenerator(cluster.Name), currentObjectRef: nil, }) + g.Expect(err).ToNot(HaveOccurred()) g.Expect(obj).ToNot(BeNil()) assertTemplateToTemplate(g, assertTemplateInput{ cluster: cluster, @@ -2070,13 +2072,14 @@ func TestTemplateToTemplate(t *testing.T) { }) t.Run("Overrides the generated name if there is already a reference", func(t *testing.T) { g := NewWithT(t) - obj := templateToTemplate(templateToInput{ + obj, err := templateToTemplate(templateToInput{ template: template, templateClonedFromRef: fakeRef1, cluster: cluster, - namePrefix: cluster.Name, + nameGenerator: topologynames.SimpleNameGenerator(cluster.Name), currentObjectRef: fakeRef2, }) + g.Expect(err).ToNot(HaveOccurred()) g.Expect(obj).ToNot(BeNil()) assertTemplateToTemplate(g, assertTemplateInput{ cluster: cluster, diff --git a/internal/controllers/topology/names/namegenerator.go b/internal/controllers/topology/names/namegenerator.go new file mode 100644 index 000000000000..0df03517106d --- /dev/null +++ b/internal/controllers/topology/names/namegenerator.go @@ -0,0 +1,127 @@ +/* +Copyright 2023 The Kubernetes Authors. + +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 names provides functions to generate object names. +package names + +import ( + "bytes" + "fmt" + "text/template" + + "github.com/pkg/errors" + utilrand "k8s.io/apimachinery/pkg/util/rand" +) + +const ( + maxNameLength = 63 + randomLength = 5 + maxGeneratedNameLength = maxNameLength - randomLength +) + +type simpleNameGenerator struct { + base string +} + +func (s *simpleNameGenerator) GenerateName() (string, error) { + base := s.base + if len(base) > maxGeneratedNameLength { + base = base[:maxGeneratedNameLength] + } + return fmt.Sprintf("%s%s", base, utilrand.String(randomLength)), nil +} + +// NameGenerator generates names for objects. +type NameGenerator interface { + // GenerateName generates a valid name. The generator is responsible for + // knowing the maximum valid name length. + GenerateName() (string, error) +} + +// SimpleNameGenerator returns a NameGenerator which is based on +// k8s.io/apiserver/pkg/storage/names.SimpleNameGenerator. +func SimpleNameGenerator(base string) NameGenerator { + return &simpleNameGenerator{ + base: base, + } +} + +// ControlPlaneNameGenerator returns a generator for creating a control plane name. +func ControlPlaneNameGenerator(templateString, clusterName string) NameGenerator { + return newTemplateGenerator(templateString, clusterName, + map[string]interface{}{}) +} + +// MachineDeploymentNameGenerator returns a generator for creating a machinedeployment name. +func MachineDeploymentNameGenerator(templateString, clusterName, topologyName string) NameGenerator { + return newTemplateGenerator(templateString, clusterName, + map[string]interface{}{ + "machineDeployment": map[string]interface{}{ + "topologyName": topologyName, + }, + }) +} + +// MachinePoolName returns a generator for creating a machinepool name. +func MachinePoolName(templateString, clusterName, topologyName string) NameGenerator { + return newTemplateGenerator(templateString, clusterName, + map[string]interface{}{ + "machinePool": map[string]interface{}{ + "topologyName": topologyName, + }, + }) +} + +// templateGenerator parses the template string as text/template and executes it using +// the passed data to generate a name. +type templateGenerator struct { + template string + data map[string]interface{} +} + +func newTemplateGenerator(template, clusterName string, data map[string]interface{}) NameGenerator { + data["cluster"] = map[string]interface{}{ + "name": clusterName, + } + data["random"] = utilrand.String(randomLength) + + return &templateGenerator{ + template: template, + data: data, + } +} + +func (g *templateGenerator) GenerateName() (string, error) { + tpl, err := template.New("name").Parse(g.template) + if err != nil { + return "", errors.Wrapf(err, "parsing template %q", g.template) + } + + var buf bytes.Buffer + if err := tpl.Execute(&buf, g.data); err != nil { + return "", errors.Wrapf(err, "rendering template: %q", tpl.Name()) + } + + name := buf.String() + + // If the name exceeds the maxNameLength: trim to maxGeneratedNameLength and add + // a random suffix. + if len(name) > maxNameLength { + name = name[:maxGeneratedNameLength] + utilrand.String(randomLength) + } + + return name, nil +} diff --git a/internal/test/builder/builders.go b/internal/test/builder/builders.go index 92037392a096..363e425a1997 100644 --- a/internal/test/builder/builders.go +++ b/internal/test/builder/builders.go @@ -485,6 +485,7 @@ type MachineDeploymentClassBuilder struct { nodeDeletionTimeout *metav1.Duration minReadySeconds *int32 strategy *clusterv1.MachineDeploymentStrategy + namingStrategy *clusterv1.ControlPlaneClassNamingStrategy } // MachineDeploymentClass returns a MachineDeploymentClassBuilder with the given name and namespace. @@ -560,6 +561,12 @@ func (m *MachineDeploymentClassBuilder) WithStrategy(s *clusterv1.MachineDeploym return m } +// WithNamingStrategy sets the NamingStrategy for the MachineDeploymentClassBuilder. +func (m *MachineDeploymentClassBuilder) WithNamingStrategy(n *clusterv1.ControlPlaneClassNamingStrategy) *MachineDeploymentClassBuilder { + m.namingStrategy = n + return m +} + // Build creates a full MachineDeploymentClass object with the variables passed to the MachineDeploymentClassBuilder. func (m *MachineDeploymentClassBuilder) Build() *clusterv1.MachineDeploymentClass { obj := &clusterv1.MachineDeploymentClass{ @@ -598,6 +605,9 @@ func (m *MachineDeploymentClassBuilder) Build() *clusterv1.MachineDeploymentClas if m.strategy != nil { obj.Strategy = m.strategy } + if m.namingStrategy != nil { + obj.NamingStrategy = m.namingStrategy + } return obj } @@ -613,6 +623,7 @@ type MachinePoolClassBuilder struct { nodeVolumeDetachTimeout *metav1.Duration nodeDeletionTimeout *metav1.Duration minReadySeconds *int32 + namingStrategy *clusterv1.ControlPlaneClassNamingStrategy } // MachinePoolClass returns a MachinePoolClassBuilder with the given name and namespace. @@ -676,6 +687,12 @@ func (m *MachinePoolClassBuilder) WithMinReadySeconds(t *int32) *MachinePoolClas return m } +// WithNamingStrategy sets the NamingStrategy for the MachinePoolClassBuilder. +func (m *MachinePoolClassBuilder) WithNamingStrategy(n *clusterv1.ControlPlaneClassNamingStrategy) *MachinePoolClassBuilder { + m.namingStrategy = n + return m +} + // Build creates a full MachinePoolClass object with the variables passed to the MachinePoolClassBuilder. func (m *MachinePoolClassBuilder) Build() *clusterv1.MachinePoolClass { obj := &clusterv1.MachinePoolClass{ @@ -708,6 +725,9 @@ func (m *MachinePoolClassBuilder) Build() *clusterv1.MachinePoolClass { if m.minReadySeconds != nil { obj.MinReadySeconds = m.minReadySeconds } + if m.namingStrategy != nil { + obj.NamingStrategy = m.namingStrategy + } return obj } diff --git a/internal/test/builder/zz_generated.deepcopy.go b/internal/test/builder/zz_generated.deepcopy.go index c6a1e986ca36..86ce540a872b 100644 --- a/internal/test/builder/zz_generated.deepcopy.go +++ b/internal/test/builder/zz_generated.deepcopy.go @@ -510,6 +510,11 @@ func (in *MachineDeploymentClassBuilder) DeepCopyInto(out *MachineDeploymentClas *out = new(v1beta1.MachineDeploymentStrategy) (*in).DeepCopyInto(*out) } + if in.namingStrategy != nil { + in, out := &in.namingStrategy, &out.namingStrategy + *out = new(v1beta1.ControlPlaneClassNamingStrategy) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineDeploymentClassBuilder. @@ -682,6 +687,11 @@ func (in *MachinePoolClassBuilder) DeepCopyInto(out *MachinePoolClassBuilder) { *out = new(int32) **out = **in } + if in.namingStrategy != nil { + in, out := &in.namingStrategy, &out.namingStrategy + *out = new(v1beta1.ControlPlaneClassNamingStrategy) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachinePoolClassBuilder. diff --git a/internal/webhooks/clusterclass.go b/internal/webhooks/clusterclass.go index 78549f43966a..b95a20ec6b54 100644 --- a/internal/webhooks/clusterclass.go +++ b/internal/webhooks/clusterclass.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "strings" + "text/template" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -157,6 +158,9 @@ func (webhook *ClusterClass) validate(ctx context.Context, oldClusterClass, newC // Ensure MachineHealthChecks are valid. allErrs = append(allErrs, validateMachineHealthCheckClasses(newClusterClass)...) + // Ensure NamePrefixes are valid templates. + allErrs = append(allErrs, validateNamingStrategies(newClusterClass)...) + // Validate variables. allErrs = append(allErrs, variables.ValidateClusterClassVariables(ctx, newClusterClass.Spec.Variables, field.NewPath("spec", "variables"))..., @@ -410,6 +414,39 @@ func validateMachineHealthCheckClasses(clusterClass *clusterv1.ClusterClass) fie return allErrs } +func validateNamingStrategies(clusterClass *clusterv1.ClusterClass) field.ErrorList { + var allErrs field.ErrorList + + allErrs = append(allErrs, validateNamingStrategy(clusterClass.Spec.ControlPlane.NamingStrategy, field.NewPath("spec", "controlPlane", "namingStrategy"))...) + for i, md := range clusterClass.Spec.Workers.MachineDeployments { + allErrs = append(allErrs, validateNamingStrategy(md.NamingStrategy, field.NewPath("spec", "workers", "machineDeployments").Index(i).Child("namingStrategy"))...) + } + for i, mp := range clusterClass.Spec.Workers.MachinePools { + allErrs = append(allErrs, validateNamingStrategy(mp.NamingStrategy, field.NewPath("spec", "workers", "machinePools").Index(i).Child("namingStrategy"))...) + } + + return allErrs +} + +func validateNamingStrategy(namingStrategy *clusterv1.ControlPlaneClassNamingStrategy, fldPath *field.Path) field.ErrorList { + if namingStrategy == nil || namingStrategy.Template == nil { + return nil + } + + var allErrs field.ErrorList + + if _, err := template.New("namingStrategy.name").Parse(*namingStrategy.Template); err != nil { + allErrs = append(allErrs, + field.Invalid( + fldPath.Child("name"), + *namingStrategy.Template, + fmt.Sprintf("template can not be parsed: %v", err), + )) + } + + return allErrs +} + // validateMachineHealthCheckClass validates the MachineHealthCheckSpec fields defined in a MachineHealthCheckClass. func validateMachineHealthCheckClass(fldPath *field.Path, namepace string, m *clusterv1.MachineHealthCheckClass) field.ErrorList { mhc := clusterv1.MachineHealthCheck{ diff --git a/internal/webhooks/clusterclass_test.go b/internal/webhooks/clusterclass_test.go index 702d453f23ed..467883a30d4d 100644 --- a/internal/webhooks/clusterclass_test.go +++ b/internal/webhooks/clusterclass_test.go @@ -1165,6 +1165,50 @@ func TestClusterClassValidation(t *testing.T) { Build(), expectErr: true, }, + { + name: "should not return error for valid md namingStrategy.name", + in: builder.ClusterClass(metav1.NamespaceDefault, "class1"). + WithInfrastructureClusterTemplate( + builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "infra1").Build()). + WithControlPlaneTemplate( + builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1"). + Build()). + WithControlPlaneInfrastructureMachineTemplate( + builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "cpInfra1"). + Build()). + WithWorkerMachineDeploymentClasses( + *builder.MachineDeploymentClass("aa"). + WithInfrastructureTemplate( + builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "infra1").Build()). + WithBootstrapTemplate( + builder.BootstrapTemplate(metav1.NamespaceDefault, "bootstrap1").Build()). + WithNamingStrategy(&clusterv1.ControlPlaneClassNamingStrategy{Template: pointer.String("template-{{ .variable }}")}). + Build()). + Build(), + expectErr: false, + }, + { + name: "should return error for invalid md namingStrategy.name", + in: builder.ClusterClass(metav1.NamespaceDefault, "class1"). + WithInfrastructureClusterTemplate( + builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "infra1").Build()). + WithControlPlaneTemplate( + builder.ControlPlaneTemplate(metav1.NamespaceDefault, "cp1"). + Build()). + WithControlPlaneInfrastructureMachineTemplate( + builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "cpInfra1"). + Build()). + WithWorkerMachineDeploymentClasses( + *builder.MachineDeploymentClass("aa"). + WithInfrastructureTemplate( + builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "infra1").Build()). + WithBootstrapTemplate( + builder.BootstrapTemplate(metav1.NamespaceDefault, "bootstrap1").Build()). + WithNamingStrategy(&clusterv1.ControlPlaneClassNamingStrategy{Template: pointer.String("template-{{{{ .variable }}")}). + Build()). + Build(), + expectErr: true, + }, } for _, tt := range tests {