diff --git a/api/v1beta1/cluster_types.go b/api/v1beta1/cluster_types.go index b96807b6febd..51aa4e2c5dea 100644 --- a/api/v1beta1/cluster_types.go +++ b/api/v1beta1/cluster_types.go @@ -546,13 +546,15 @@ type Topology struct { // +kubebuilder:validation:MaxLength=253 Class string `json:"class"` - // classNamespace is the namespace of the ClusterClass object to create the topology. - // If the namespace is empty or not set, it is defaulted to the namespace of the cluster object. - // Value must follow the DNS1123Subdomain syntax. + // classNamespace is the namespace of the ClusterClass that should be used for the topology. + // If classNamespace is empty or not set, it is defaulted to the namespace of the Cluster object. + // classNamespace must be a valid namespace name and because of that be at most 63 characters in length + // and it must consist only of lower case alphanumeric characters or hyphens (-), and must start + // and end with an alphanumeric character. // +optional // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=253 - // +kubebuilder:validation:Pattern=`^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\.[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)*$` + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` ClassNamespace string `json:"classNamespace,omitempty"` // version is the Kubernetes version of the cluster. diff --git a/api/v1beta1/conversion.go b/api/v1beta1/conversion.go index 45691df57f74..1ce2c5a2585e 100644 --- a/api/v1beta1/conversion.go +++ b/api/v1beta1/conversion.go @@ -247,6 +247,16 @@ func Convert_v1beta2_ClusterStatus_To_v1beta1_ClusterStatus(in *clusterv1.Cluste return nil } +func Convert_v1beta1_Topology_To_v1beta2_Topology(in *Topology, out *clusterv1.Topology, s apimachineryconversion.Scope) error { + if err := autoConvert_v1beta1_Topology_To_v1beta2_Topology(in, out, s); err != nil { + return err + } + + out.ClassRef.Name = in.Class + out.ClassRef.Namespace = in.ClassNamespace + return nil +} + func Convert_v1beta1_ClusterStatus_To_v1beta2_ClusterStatus(in *ClusterStatus, out *clusterv1.ClusterStatus, s apimachineryconversion.Scope) error { if err := autoConvert_v1beta1_ClusterStatus_To_v1beta2_ClusterStatus(in, out, s); err != nil { return err @@ -307,6 +317,16 @@ func Convert_v1beta1_ClusterStatus_To_v1beta2_ClusterStatus(in *ClusterStatus, o return nil } +func Convert_v1beta2_Topology_To_v1beta1_Topology(in *clusterv1.Topology, out *Topology, s apimachineryconversion.Scope) error { + if err := autoConvert_v1beta2_Topology_To_v1beta1_Topology(in, out, s); err != nil { + return err + } + + out.Class = in.ClassRef.Name + out.ClassNamespace = in.ClassRef.Namespace + return nil +} + func Convert_v1beta2_MachineDeploymentStatus_To_v1beta1_MachineDeploymentStatus(in *clusterv1.MachineDeploymentStatus, out *MachineDeploymentStatus, s apimachineryconversion.Scope) error { if err := autoConvert_v1beta2_MachineDeploymentStatus_To_v1beta1_MachineDeploymentStatus(in, out, s); err != nil { return err diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index 8b1aeed0d63f..dce320f1ad9f 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -755,16 +755,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*Topology)(nil), (*v1beta2.Topology)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_Topology_To_v1beta2_Topology(a.(*Topology), b.(*v1beta2.Topology), scope) - }); err != nil { - return err - } - if err := s.AddGeneratedConversionFunc((*v1beta2.Topology)(nil), (*Topology)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta2_Topology_To_v1beta1_Topology(a.(*v1beta2.Topology), b.(*Topology), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*UnhealthyCondition)(nil), (*v1beta2.UnhealthyCondition)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_UnhealthyCondition_To_v1beta2_UnhealthyCondition(a.(*UnhealthyCondition), b.(*v1beta2.UnhealthyCondition), scope) }); err != nil { @@ -890,6 +880,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*Topology)(nil), (*v1beta2.Topology)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_Topology_To_v1beta2_Topology(a.(*Topology), b.(*v1beta2.Topology), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1beta2.ClusterClassSpec)(nil), (*ClusterClassSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta2_ClusterClassSpec_To_v1beta1_ClusterClassSpec(a.(*v1beta2.ClusterClassSpec), b.(*ClusterClassSpec), scope) }); err != nil { @@ -930,6 +925,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*v1beta2.Topology)(nil), (*Topology)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_Topology_To_v1beta1_Topology(a.(*v1beta2.Topology), b.(*Topology), scope) + }); err != nil { + return err + } return nil } @@ -3502,8 +3502,8 @@ func Convert_v1beta2_RemediationStrategy_To_v1beta1_RemediationStrategy(in *v1be } func autoConvert_v1beta1_Topology_To_v1beta2_Topology(in *Topology, out *v1beta2.Topology, s conversion.Scope) error { - out.Class = in.Class - out.ClassNamespace = in.ClassNamespace + // WARNING: in.Class requires manual conversion: does not exist in peer-type + // WARNING: in.ClassNamespace requires manual conversion: does not exist in peer-type out.Version = in.Version out.RolloutAfter = (*v1.Time)(unsafe.Pointer(in.RolloutAfter)) if err := Convert_v1beta1_ControlPlaneTopology_To_v1beta2_ControlPlaneTopology(&in.ControlPlane, &out.ControlPlane, s); err != nil { @@ -3532,14 +3532,8 @@ func autoConvert_v1beta1_Topology_To_v1beta2_Topology(in *Topology, out *v1beta2 return nil } -// Convert_v1beta1_Topology_To_v1beta2_Topology is an autogenerated conversion function. -func Convert_v1beta1_Topology_To_v1beta2_Topology(in *Topology, out *v1beta2.Topology, s conversion.Scope) error { - return autoConvert_v1beta1_Topology_To_v1beta2_Topology(in, out, s) -} - func autoConvert_v1beta2_Topology_To_v1beta1_Topology(in *v1beta2.Topology, out *Topology, s conversion.Scope) error { - out.Class = in.Class - out.ClassNamespace = in.ClassNamespace + // WARNING: in.ClassRef requires manual conversion: does not exist in peer-type out.Version = in.Version out.RolloutAfter = (*v1.Time)(unsafe.Pointer(in.RolloutAfter)) if err := Convert_v1beta2_ControlPlaneTopology_To_v1beta1_ControlPlaneTopology(&in.ControlPlane, &out.ControlPlane, s); err != nil { @@ -3568,11 +3562,6 @@ func autoConvert_v1beta2_Topology_To_v1beta1_Topology(in *v1beta2.Topology, out return nil } -// Convert_v1beta2_Topology_To_v1beta1_Topology is an autogenerated conversion function. -func Convert_v1beta2_Topology_To_v1beta1_Topology(in *v1beta2.Topology, out *Topology, s conversion.Scope) error { - return autoConvert_v1beta2_Topology_To_v1beta1_Topology(in, out, s) -} - func autoConvert_v1beta1_UnhealthyCondition_To_v1beta2_UnhealthyCondition(in *UnhealthyCondition, out *v1beta2.UnhealthyCondition, s conversion.Scope) error { out.Type = corev1.NodeConditionType(in.Type) out.Status = corev1.ConditionStatus(in.Status) diff --git a/api/v1beta1/zz_generated.openapi.go b/api/v1beta1/zz_generated.openapi.go index cd5dbf15c3e5..aa0f813621b1 100644 --- a/api/v1beta1/zz_generated.openapi.go +++ b/api/v1beta1/zz_generated.openapi.go @@ -4678,7 +4678,7 @@ func schema_sigsk8sio_cluster_api_api_v1beta1_Topology(ref common.ReferenceCallb }, "classNamespace": { SchemaProps: spec.SchemaProps{ - Description: "classNamespace is the namespace of the ClusterClass object to create the topology. If the namespace is empty or not set, it is defaulted to the namespace of the cluster object. Value must follow the DNS1123Subdomain syntax.", + Description: "classNamespace is the namespace of the ClusterClass that should be used for the topology. If classNamespace is empty or not set, it is defaulted to the namespace of the Cluster object. classNamespace must be a valid namespace name and because of that be at most 63 characters in length and it must consist only of lower case alphanumeric characters or hyphens (-), and must start and end with an alphanumeric character.", Type: []string{"string"}, Format: "", }, diff --git a/api/v1beta2/cluster_types.go b/api/v1beta2/cluster_types.go index 1936e1cec848..53dbb6a66a0d 100644 --- a/api/v1beta2/cluster_types.go +++ b/api/v1beta2/cluster_types.go @@ -538,20 +538,9 @@ type ClusterAvailabilityGate struct { // Topology encapsulates the information of the managed resources. type Topology struct { - // class is the name of the ClusterClass object to create the topology. + // classRef is the ref to the ClusterClass that should be used for the topology. // +required - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=253 - Class string `json:"class"` - - // classNamespace is the namespace of the ClusterClass object to create the topology. - // If the namespace is empty or not set, it is defaulted to the namespace of the cluster object. - // Value must follow the DNS1123Subdomain syntax. - // +optional - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=253 - // +kubebuilder:validation:Pattern=`^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\.[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)*$` - ClassNamespace string `json:"classNamespace,omitempty"` + ClassRef ClusterClassRef `json:"classRef"` // version is the Kubernetes version of the cluster. // +required @@ -586,6 +575,30 @@ type Topology struct { Variables []ClusterVariable `json:"variables,omitempty"` } +// ClusterClassRef is the ref to the ClusterClass that should be used for the topology. +type ClusterClassRef struct { + // name is the name of the ClusterClass that should be used for the topology. + // name must be a valid ClusterClass name and because of that be at most 253 characters in length + // and it must consist only of lower case alphanumeric characters, hyphens (-) and periods (.), and must start + // and end with an alphanumeric character. + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` + Name string `json:"name"` + + // namespace is the namespace of the ClusterClass that should be used for the topology. + // If namespace is empty or not set, it is defaulted to the namespace of the Cluster object. + // namespace must be a valid namespace name and because of that be at most 63 characters in length + // and it must consist only of lower case alphanumeric characters or hyphens (-), and must start + // and end with an alphanumeric character. + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` + Namespace string `json:"namespace,omitempty"` +} + // ControlPlaneTopology specifies the parameters for the control plane nodes in the cluster. type ControlPlaneTopology struct { // metadata is the metadata applied to the ControlPlane and the Machines of the ControlPlane @@ -1180,8 +1193,8 @@ func (c *Cluster) GetClassKey() types.NamespacedName { return types.NamespacedName{} } - namespace := cmp.Or(c.Spec.Topology.ClassNamespace, c.Namespace) - return types.NamespacedName{Namespace: namespace, Name: c.Spec.Topology.Class} + namespace := cmp.Or(c.Spec.Topology.ClassRef.Namespace, c.Namespace) + return types.NamespacedName{Namespace: namespace, Name: c.Spec.Topology.ClassRef.Name} } // GetV1Beta1Conditions returns the set of conditions for this object. diff --git a/api/v1beta2/index/cluster_test.go b/api/v1beta2/index/cluster_test.go index 81e78c521b23..1236ae186c3a 100644 --- a/api/v1beta2/index/cluster_test.go +++ b/api/v1beta2/index/cluster_test.go @@ -46,7 +46,9 @@ func TestClusterByClusterClassRef(t *testing.T) { }, Spec: clusterv1.ClusterSpec{ Topology: &clusterv1.Topology{ - Class: "class1", + ClassRef: clusterv1.ClusterClassRef{ + Name: "class1", + }, }, }, }, @@ -61,8 +63,10 @@ func TestClusterByClusterClassRef(t *testing.T) { }, Spec: clusterv1.ClusterSpec{ Topology: &clusterv1.Topology{ - Class: "class1", - ClassNamespace: "other", + ClassRef: clusterv1.ClusterClassRef{ + Name: "class1", + Namespace: "other", + }, }, }, }, diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 741b50fa22ca..aa546ce4a7ee 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -222,6 +222,21 @@ func (in *ClusterClassPatch) DeepCopy() *ClusterClassPatch { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterClassRef) DeepCopyInto(out *ClusterClassRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClassRef. +func (in *ClusterClassRef) DeepCopy() *ClusterClassRef { + if in == nil { + return nil + } + out := new(ClusterClassRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterClassSpec) DeepCopyInto(out *ClusterClassSpec) { *out = *in @@ -2862,6 +2877,7 @@ func (in *RemediationStrategy) DeepCopy() *RemediationStrategy { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Topology) DeepCopyInto(out *Topology) { *out = *in + out.ClassRef = in.ClassRef if in.RolloutAfter != nil { in, out := &in.RolloutAfter, &out.RolloutAfter *out = (*in).DeepCopy() diff --git a/api/v1beta2/zz_generated.openapi.go b/api/v1beta2/zz_generated.openapi.go index eee6a4dbf1d3..1a36d21cf045 100644 --- a/api/v1beta2/zz_generated.openapi.go +++ b/api/v1beta2/zz_generated.openapi.go @@ -36,6 +36,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "sigs.k8s.io/cluster-api/api/v1beta2.ClusterClassDeprecatedStatus": schema_sigsk8sio_cluster_api_api_v1beta2_ClusterClassDeprecatedStatus(ref), "sigs.k8s.io/cluster-api/api/v1beta2.ClusterClassList": schema_sigsk8sio_cluster_api_api_v1beta2_ClusterClassList(ref), "sigs.k8s.io/cluster-api/api/v1beta2.ClusterClassPatch": schema_sigsk8sio_cluster_api_api_v1beta2_ClusterClassPatch(ref), + "sigs.k8s.io/cluster-api/api/v1beta2.ClusterClassRef": schema_sigsk8sio_cluster_api_api_v1beta2_ClusterClassRef(ref), "sigs.k8s.io/cluster-api/api/v1beta2.ClusterClassSpec": schema_sigsk8sio_cluster_api_api_v1beta2_ClusterClassSpec(ref), "sigs.k8s.io/cluster-api/api/v1beta2.ClusterClassStatus": schema_sigsk8sio_cluster_api_api_v1beta2_ClusterClassStatus(ref), "sigs.k8s.io/cluster-api/api/v1beta2.ClusterClassStatusVariable": schema_sigsk8sio_cluster_api_api_v1beta2_ClusterClassStatusVariable(ref), @@ -451,6 +452,35 @@ func schema_sigsk8sio_cluster_api_api_v1beta2_ClusterClassPatch(ref common.Refer } } +func schema_sigsk8sio_cluster_api_api_v1beta2_ClusterClassRef(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ClusterClassRef is the ref to the ClusterClass that should be used for the topology.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "name is the name of the ClusterClass that should be used for the topology. name must be a valid ClusterClass name and because of that be at most 253 characters in length and it must consist only of lower case alphanumeric characters, hyphens (-) and periods (.), and must start and end with an alphanumeric character.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "namespace is the namespace of the ClusterClass that should be used for the topology. If namespace is empty or not set, it is defaulted to the namespace of the Cluster object. namespace must be a valid namespace name and because of that be at most 63 characters in length and it must consist only of lower case alphanumeric characters or hyphens (-), and must start and end with an alphanumeric character.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"name"}, + }, + }, + } +} + func schema_sigsk8sio_cluster_api_api_v1beta2_ClusterClassSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -4856,19 +4886,11 @@ func schema_sigsk8sio_cluster_api_api_v1beta2_Topology(ref common.ReferenceCallb Description: "Topology encapsulates the information of the managed resources.", Type: []string{"object"}, Properties: map[string]spec.Schema{ - "class": { + "classRef": { SchemaProps: spec.SchemaProps{ - Description: "class is the name of the ClusterClass object to create the topology.", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "classNamespace": { - SchemaProps: spec.SchemaProps{ - Description: "classNamespace is the namespace of the ClusterClass object to create the topology. If the namespace is empty or not set, it is defaulted to the namespace of the cluster object. Value must follow the DNS1123Subdomain syntax.", - Type: []string{"string"}, - Format: "", + Description: "classRef is the ref to the ClusterClass that should be used for the topology.", + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/api/v1beta2.ClusterClassRef"), }, }, "version": { @@ -4921,11 +4943,11 @@ func schema_sigsk8sio_cluster_api_api_v1beta2_Topology(ref common.ReferenceCallb }, }, }, - Required: []string{"class", "version"}, + Required: []string{"classRef", "version"}, }, }, Dependencies: []string{ - "k8s.io/apimachinery/pkg/apis/meta/v1.Time", "sigs.k8s.io/cluster-api/api/v1beta2.ClusterVariable", "sigs.k8s.io/cluster-api/api/v1beta2.ControlPlaneTopology", "sigs.k8s.io/cluster-api/api/v1beta2.WorkersTopology"}, + "k8s.io/apimachinery/pkg/apis/meta/v1.Time", "sigs.k8s.io/cluster-api/api/v1beta2.ClusterClassRef", "sigs.k8s.io/cluster-api/api/v1beta2.ClusterVariable", "sigs.k8s.io/cluster-api/api/v1beta2.ControlPlaneTopology", "sigs.k8s.io/cluster-api/api/v1beta2.WorkersTopology"}, } } diff --git a/cmd/clusterctl/client/cluster/assets/topology-test/existing-my-cluster.yaml b/cmd/clusterctl/client/cluster/assets/topology-test/existing-my-cluster.yaml index 9db0eac02042..2dead59002d4 100644 --- a/cmd/clusterctl/client/cluster/assets/topology-test/existing-my-cluster.yaml +++ b/cmd/clusterctl/client/cluster/assets/topology-test/existing-my-cluster.yaml @@ -26,7 +26,8 @@ spec: name: my-cluster-zrq96 namespace: default topology: - class: my-cluster-class + classRef: + name: my-cluster-class version: v1.21.2 controlPlane: metadata: {} diff --git a/cmd/clusterctl/client/cluster/assets/topology-test/existing-my-second-cluster.yaml b/cmd/clusterctl/client/cluster/assets/topology-test/existing-my-second-cluster.yaml index 3304a0e39c58..d1ab1a7dff68 100644 --- a/cmd/clusterctl/client/cluster/assets/topology-test/existing-my-second-cluster.yaml +++ b/cmd/clusterctl/client/cluster/assets/topology-test/existing-my-second-cluster.yaml @@ -27,7 +27,8 @@ spec: name: my-second-cluster-zrq96 namespace: default topology: - class: my-cluster-class + classRef: + name: my-cluster-class version: v1.21.2 controlPlane: metadata: {} diff --git a/cmd/clusterctl/client/cluster/assets/topology-test/modified-my-cluster.yaml b/cmd/clusterctl/client/cluster/assets/topology-test/modified-my-cluster.yaml index 6fbd9de62399..e364f6de624a 100644 --- a/cmd/clusterctl/client/cluster/assets/topology-test/modified-my-cluster.yaml +++ b/cmd/clusterctl/client/cluster/assets/topology-test/modified-my-cluster.yaml @@ -13,7 +13,8 @@ spec: cidrBlocks: ["192.168.0.0/16"] serviceDomain: "cluster.local" topology: - class: my-cluster-class + classRef: + name: my-cluster-class version: v1.21.2 controlPlane: metadata: {} diff --git a/cmd/clusterctl/client/cluster/assets/topology-test/new-clusterclass-and-cluster.yaml b/cmd/clusterctl/client/cluster/assets/topology-test/new-clusterclass-and-cluster.yaml index 8e8f5f3748d3..96eef04742ad 100644 --- a/cmd/clusterctl/client/cluster/assets/topology-test/new-clusterclass-and-cluster.yaml +++ b/cmd/clusterctl/client/cluster/assets/topology-test/new-clusterclass-and-cluster.yaml @@ -186,7 +186,8 @@ spec: cidrBlocks: ["192.168.0.0/16"] serviceDomain: "cluster.local" topology: - class: my-cluster-class + classRef: + name: my-cluster-class version: v1.21.2 controlPlane: metadata: {} diff --git a/cmd/clusterctl/client/cluster/assets/topology-test/objects-in-different-namespaces.yaml b/cmd/clusterctl/client/cluster/assets/topology-test/objects-in-different-namespaces.yaml index adbc8bb2d7d0..8bfd63001d42 100644 --- a/cmd/clusterctl/client/cluster/assets/topology-test/objects-in-different-namespaces.yaml +++ b/cmd/clusterctl/client/cluster/assets/topology-test/objects-in-different-namespaces.yaml @@ -13,7 +13,8 @@ spec: cidrBlocks: ["192.168.0.0/16"] serviceDomain: "cluster.local" topology: - class: my-cluster-class + classRef: + name: my-cluster-class version: v1.21.2 controlPlane: metadata: {} @@ -34,7 +35,8 @@ spec: cidrBlocks: ["192.168.0.0/16"] serviceDomain: "cluster.local" topology: - class: my-second-cluster-class + classRef: + name: my-second-cluster-class version: v1.21.2 controlPlane: metadata: {} diff --git a/cmd/clusterctl/client/cluster/objectgraph.go b/cmd/clusterctl/client/cluster/objectgraph.go index 7086ed836c0e..57c9e6fbd607 100644 --- a/cmd/clusterctl/client/cluster/objectgraph.go +++ b/cmd/clusterctl/client/cluster/objectgraph.go @@ -827,13 +827,13 @@ func (o *objectGraph) setShouldNotDelete(ctx context.Context, namespace string) } // ignore cluster not referencing a CC in the namespace being moved. - if cluster.Spec.Topology.ClassNamespace != namespace { + if cluster.Spec.Topology.ClassRef.Namespace != namespace { continue } // Otherwise mark the referenced CC as should not be deleted. for _, class := range o.getClusterClasses() { - if class.identity.Namespace == cluster.Spec.Topology.ClassNamespace && class.identity.Name == cluster.Spec.Topology.Class { + if class.identity.Namespace == cluster.Spec.Topology.ClassRef.Namespace && class.identity.Name == cluster.Spec.Topology.ClassRef.Name { class.shouldNotDelete = true // Ensure that also the templates referenced by the CC won't be deleted. o.setShouldNotDeleteHierarchy(class) diff --git a/cmd/clusterctl/client/clusterclass_test.go b/cmd/clusterctl/client/clusterclass_test.go index 17486c3dada3..3572d898df1f 100644 --- a/cmd/clusterctl/client/clusterclass_test.go +++ b/cmd/clusterctl/client/clusterclass_test.go @@ -219,10 +219,11 @@ func TestAddClusterClassIfMissing(t *testing.T) { fmt.Sprintf(" namespace: %s\n", tt.targetNamespace) + "spec:\n" + " topology:\n" + - " class: dev" + " classRef:\n" + + " name: dev" if tt.clusterClassNamespace != "" { - clusterWithTopology = fmt.Sprintf("%s\n classNamespace: %s", clusterWithTopology, tt.clusterClassNamespace) + clusterWithTopology = fmt.Sprintf("%s\n namespace: %s", clusterWithTopology, tt.clusterClassNamespace) } baseTemplate, err := repository.NewTemplate(repository.TemplateInput{ diff --git a/cmd/clusterctl/client/init_test.go b/cmd/clusterctl/client/init_test.go index b9249643bc9e..b893cf493c71 100644 --- a/cmd/clusterctl/client/init_test.go +++ b/cmd/clusterctl/client/init_test.go @@ -967,7 +967,8 @@ func mangedTopologyTemplateYAML(ns, clusterName, clusterClassName string) []byte fmt.Sprintf(" namespace: %s\n", ns) + "spec:\n" + " topology:\n" + - fmt.Sprintf(" class: %s", clusterClassName)) + " classRef:\n" + + fmt.Sprintf(" name: %s", clusterClassName)) } func clusterClassYAML(ns, clusterClassName string) []byte { diff --git a/cmd/clusterctl/internal/test/fake_objects.go b/cmd/clusterctl/internal/test/fake_objects.go index 14e9d8994e41..4d1d615b98f3 100644 --- a/cmd/clusterctl/internal/test/fake_objects.go +++ b/cmd/clusterctl/internal/test/fake_objects.go @@ -152,9 +152,13 @@ func (f *FakeCluster) Objs() []client.Object { } if f.topologyClass != nil { - cluster.Spec.Topology = &clusterv1.Topology{Class: *f.topologyClass} + cluster.Spec.Topology = &clusterv1.Topology{ + ClassRef: clusterv1.ClusterClassRef{ + Name: *f.topologyClass, + }, + } if f.topologyClassNamespace != nil { - cluster.Spec.Topology.ClassNamespace = *f.topologyClassNamespace + cluster.Spec.Topology.ClassRef.Namespace = *f.topologyClassNamespace } } diff --git a/config/crd/bases/cluster.x-k8s.io_clusters.yaml b/config/crd/bases/cluster.x-k8s.io_clusters.yaml index 6c77fbb08062..b2abe5384663 100644 --- a/config/crd/bases/cluster.x-k8s.io_clusters.yaml +++ b/config/crd/bases/cluster.x-k8s.io_clusters.yaml @@ -968,12 +968,14 @@ spec: type: string classNamespace: description: |- - classNamespace is the namespace of the ClusterClass object to create the topology. - If the namespace is empty or not set, it is defaulted to the namespace of the cluster object. - Value must follow the DNS1123Subdomain syntax. - maxLength: 253 + classNamespace is the namespace of the ClusterClass that should be used for the topology. + If classNamespace is empty or not set, it is defaulted to the namespace of the Cluster object. + classNamespace must be a valid namespace name and because of that be at most 63 characters in length + and it must consist only of lower case alphanumeric characters or hyphens (-), and must start + and end with an alphanumeric character. + maxLength: 63 minLength: 1 - pattern: ^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\.[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)*$ + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ type: string controlPlane: description: controlPlane describes the cluster control plane. @@ -2387,21 +2389,34 @@ spec: feature gate flag to activate managed topologies support; this feature is highly experimental, and parts of it might still be not implemented. properties: - class: - description: class is the name of the ClusterClass object to create - the topology. - maxLength: 253 - minLength: 1 - type: string - classNamespace: - description: |- - classNamespace is the namespace of the ClusterClass object to create the topology. - If the namespace is empty or not set, it is defaulted to the namespace of the cluster object. - Value must follow the DNS1123Subdomain syntax. - maxLength: 253 - minLength: 1 - pattern: ^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?(?:\.[a-z0-9](?:[-a-z0-9]*[a-z0-9])?)*$ - type: string + classRef: + description: classRef is the ref to the ClusterClass that should + be used for the topology. + properties: + name: + description: |- + name is the name of the ClusterClass that should be used for the topology. + name must be a valid ClusterClass name and because of that be at most 253 characters in length + and it must consist only of lower case alphanumeric characters, hyphens (-) and periods (.), and must start + and end with an alphanumeric character. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + namespace: + description: |- + namespace is the namespace of the ClusterClass that should be used for the topology. + If namespace is empty or not set, it is defaulted to the namespace of the Cluster object. + namespace must be a valid namespace name and because of that be at most 63 characters in length + and it must consist only of lower case alphanumeric characters or hyphens (-), and must start + and end with an alphanumeric character. + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + required: + - name + type: object controlPlane: description: controlPlane describes the cluster control plane. properties: @@ -3275,7 +3290,7 @@ spec: x-kubernetes-list-type: map type: object required: - - class + - classRef - version type: object type: object diff --git a/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md b/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md index 861bc2de080b..73dae926630a 100644 --- a/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md +++ b/docs/book/src/tasks/experimental-features/cluster-class/write-clusterclass.md @@ -30,14 +30,14 @@ infrastructure and workers of a Cluster. When a Cluster is using this ClusterCla are used to generate the objects of the managed topology of the Cluster. ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 spec: controlPlane: ref: - apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate name: docker-clusterclass-v0.1.0 namespace: default @@ -59,7 +59,7 @@ spec: template: bootstrap: ref: - apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 kind: KubeadmConfigTemplate name: docker-clusterclass-v0.1.0-default-worker namespace: default @@ -80,13 +80,14 @@ ClusterClass is already very flexible. Via the topology on the Cluster the follo * `.spec.topology.workers`: MachineDeployments and their replicas, metadata and failure domain ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: Cluster metadata: name: my-docker-cluster spec: topology: - class: docker-clusterclass-v0.1.0 + classRef: + name: docker-clusterclass-v0.1.0 version: v1.22.4 controlPlane: replicas: 3 @@ -143,7 +144,7 @@ ClusterClass also supports MachinePool workers. They work very similar to Machin can be specified in the ClusterClass template under the workers section like so: ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 @@ -154,7 +155,7 @@ spec: template: bootstrap: ref: - apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 kind: KubeadmConfigTemplate name: quick-start-default-worker-bootstraptemplate infrastructure: @@ -167,7 +168,7 @@ spec: They can then be similarly defined as workers in the cluster template like so: ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: Cluster metadata: name: my-docker-cluster @@ -193,7 +194,7 @@ MachineDeployment class. The following configuration makes sure a `MachineHealth created for the control plane and for every `MachineDeployment` using the `default-worker` class. ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 @@ -245,7 +246,7 @@ required. The schema defines how a variable is defaulted and validated. It suppo a subset of the schema of CRDs. For more information please see the [godoc](https://doc.crds.dev/github.com/kubernetes-sigs/cluster-api/cluster.x-k8s.io/ClusterClass/v1beta1#spec-variables-schema-openAPIV3Schema). ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 @@ -280,7 +281,7 @@ patches should be applied to that template. In this case we set the `imageReposi please see the [godoc](https://doc.crds.dev/github.com/kubernetes-sigs/cluster-api/cluster.x-k8s.io/ClusterClass/v1beta1#spec-patches-definitions). ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 @@ -290,7 +291,7 @@ spec: - name: imageRepository definitions: - selector: - apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + apiVersion: controlplane.cluster.x-k8s.io/v1beta2 kind: KubeadmControlPlaneTemplate matchResources: controlPlane: true @@ -320,7 +321,7 @@ After creating a ClusterClass with a variable definition, the user can now provi the variable in the Cluster as in the example below. ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: Cluster metadata: name: my-docker-cluster @@ -366,7 +367,7 @@ The following variables can be referenced in templates: Example which would match the default behavior: ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 @@ -393,7 +394,7 @@ The following variables can be referenced in templates: Example which would match the default behavior: ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 @@ -423,7 +424,7 @@ The following variables can be referenced in templates: Example which would match the default behavior: ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 @@ -447,15 +448,16 @@ As a user, I may need to create a `Cluster` from a `ClusterClass` object that ex Example of the `Cluster` object with the `name/namespace` reference: ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: Cluster metadata: name: my-docker-cluster namespace: default spec: topology: - class: docker-clusterclass-v0.1.0 - classNamespace: default + classRef: + name: docker-clusterclass-v0.1.0 + namespace: default version: v1.22.4 controlPlane: replicas: 3 @@ -491,7 +493,7 @@ spec: operations: ["CREATE", "UPDATE"] resources: ["clusters"] validations: - - expression: "!has(object.spec.topology.classNamespace) || object.spec.topology.classNamespace in params.data" + - expression: "!has(object.spec.topology.classRef.namespace) || object.spec.topology.classRef.namespace in params.data" --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingAdmissionPolicyBinding @@ -528,7 +530,7 @@ In the following example we make the `instanceType` of a `AWSMachineTemplate` cu First we define the `workerMachineType` variable and the corresponding patch: ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: aws-clusterclass-v0.1.0 @@ -574,14 +576,15 @@ In the Cluster resource the `workerMachineType` variable can then be set cluster it can also be overridden for an individual MachineDeployment or MachinePool. ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: Cluster metadata: name: my-aws-cluster spec: ... topology: - class: aws-clusterclass-v0.1.0 + classRef: + name: aws-clusterclass-v0.1.0 version: v1.22.0 controlPlane: replicas: 3 @@ -609,7 +612,8 @@ spec: In addition to variables specified in the ClusterClass, the following builtin variables can be referenced in patches: - `builtin.cluster.{name,namespace,uid,metadata.labels,metadata.annotations}` -- `builtin.cluster.topology.{version,class,classNamespace}` +- `builtin.cluster.topology.{version,classRef.name,classRef.namespace,class,classNamespace}` + - Note: `class` and `classNamespace` are deprecated and will be removed with the next apiVersion. - `builtin.cluster.network.{serviceDomain,services,pods}` - `builtin.controlPlane.{replicas,version,name,metadata.labels,metadata.annotations}` - Please note, these variables are only available when patching control plane or control plane @@ -632,7 +636,7 @@ referenced in patches: Builtin variables can be referenced just like regular variables, e.g.: ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 @@ -657,7 +661,7 @@ will always be the same as the one we set in the corresponding MachineDeployment (works the same way with `.builtin.controlPlane.version`). ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 @@ -689,7 +693,7 @@ by the schemas of the fields of the object. A map is specified with the type `ob of the map values. An array is specified via the type `array` and the schema of the array items. ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 @@ -729,7 +733,7 @@ spec: Objects, maps and arrays can be used in patches either directly by referencing the variable name, or by accessing individual fields. For example: ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 @@ -765,7 +769,7 @@ Of course it's also possible to only make the name of the reference configurable to a pre-defined enum. ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: azure-clusterclass-v0.1.0 @@ -806,7 +810,7 @@ We already saw above that it's possible to use variable values in JSON patches. possible to calculate values via Go templating or to use hard-coded values. ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 @@ -871,7 +875,7 @@ The patch is then only applied if the Go template evaluates to `true`. In the fo patch is only applied if the `httpProxy` variable is set (and not empty). ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: docker-clusterclass-v0.1.0 @@ -981,7 +985,7 @@ A workaround in this particular case is to create the array in the patch instead When creating the slice, existing values would be overwritten so this should only be used when it does not exist. The following example shows both cases to consider while writing a patch for adding a value to a slice. -This patch targets to add a file to the `files` slice of a `KubeadmConfigTemplate` which has [omitempty](https://github.com/kubernetes-sigs/cluster-api/blob/main/bootstrap/kubeadm/api/v1beta1/kubeadmconfig_types.go#L54) set. +This patch targets to add a file to the `files` slice of a `KubeadmConfigTemplate` which has [omitempty](https://github.com/kubernetes-sigs/cluster-api/blob/main/bootstrap/kubeadm/api/v1beta2/kubeadmconfig_types.go#L54) set. {{#tabs name:"tab-configuration-patches" tabs:"Add to existing slice,Create slice"}} {{#tab Add to existing slice}} @@ -989,7 +993,7 @@ This patch targets to add a file to the `files` slice of a `KubeadmConfigTemplat This patch **requires** the key `.spec.template.spec.files` to exist to succeed. ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: my-clusterclass @@ -999,7 +1003,7 @@ spec: - name: add file definitions: - selector: - apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 kind: KubeadmConfigTemplate jsonPatches: - op: add @@ -1008,7 +1012,7 @@ spec: content: Some content. path: /some/file --- -apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 kind: KubeadmConfigTemplate metadata: name: "quick-start-default-worker-bootstraptemplate" @@ -1027,7 +1031,7 @@ spec: This patch would **overwrite** an existing slice at `.spec.template.spec.files`. ```yaml -apiVersion: cluster.x-k8s.io/v1beta1 +apiVersion: cluster.x-k8s.io/v1beta2 kind: ClusterClass metadata: name: my-clusterclass @@ -1037,7 +1041,7 @@ spec: - name: add file definitions: - selector: - apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 kind: KubeadmConfigTemplate jsonPatches: - op: add @@ -1046,7 +1050,7 @@ spec: - content: Some content. path: /some/file --- -apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +apiVersion: bootstrap.cluster.x-k8s.io/v1beta2 kind: KubeadmConfigTemplate metadata: name: "quick-start-default-worker-bootstraptemplate" diff --git a/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go b/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go index 4b953c6ab003..eafeddc732d0 100644 --- a/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go +++ b/exp/runtime/hooks/api/v1alpha1/topologymutation_variable_types.go @@ -76,15 +76,39 @@ type ClusterTopologyBuiltins struct { // +optional Version string `json:"version,omitempty"` + // classRef is the ref to the ClusterClass that is used for the topology. + // + // +required + ClassRef ClusterTopologyClusterClassRefBuiltins `json:"classRef"` + // class is the name of the ClusterClass of the Cluster. + // + // Deprecated: Class is deprecated in favor of ClassRef.Name and is going to be removed in the next apiVersion. + // // +optional Class string `json:"class,omitempty"` // classNamespace is the namespace of the ClusterClass of the Cluster. + // + // Deprecated: ClassNamespace is deprecated in favor of ClassRef.Namespace and is going to be removed in the next apiVersion. + // // +optional ClassNamespace string `json:"classNamespace,omitempty"` } +// ClusterTopologyClusterClassRefBuiltins is the ref to the ClusterClass that is used for the topology. +type ClusterTopologyClusterClassRefBuiltins struct { + // name is the name of the ClusterClass that is used for the topology. + // + // +required + Name string `json:"name"` + + // namespace is the namespace of the ClusterClass that is used for the topology. + // + // +required + Namespace string `json:"namespace"` +} + // ClusterNetworkBuiltins represents builtin cluster network variables. type ClusterNetworkBuiltins struct { // serviceDomain is the domain name for services. diff --git a/exp/runtime/hooks/api/v1alpha1/zz_generated.deepcopy.go b/exp/runtime/hooks/api/v1alpha1/zz_generated.deepcopy.go index 181317d24b9a..e6b70c4b0c0e 100644 --- a/exp/runtime/hooks/api/v1alpha1/zz_generated.deepcopy.go +++ b/exp/runtime/hooks/api/v1alpha1/zz_generated.deepcopy.go @@ -429,6 +429,7 @@ func (in *ClusterNetworkBuiltins) DeepCopy() *ClusterNetworkBuiltins { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterTopologyBuiltins) DeepCopyInto(out *ClusterTopologyBuiltins) { *out = *in + out.ClassRef = in.ClassRef } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterTopologyBuiltins. @@ -441,6 +442,21 @@ func (in *ClusterTopologyBuiltins) DeepCopy() *ClusterTopologyBuiltins { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterTopologyClusterClassRefBuiltins) DeepCopyInto(out *ClusterTopologyClusterClassRefBuiltins) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterTopologyClusterClassRefBuiltins. +func (in *ClusterTopologyClusterClassRefBuiltins) DeepCopy() *ClusterTopologyClusterClassRefBuiltins { + if in == nil { + return nil + } + out := new(ClusterTopologyClusterClassRefBuiltins) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CommonRequest) DeepCopyInto(out *CommonRequest) { *out = *in diff --git a/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go b/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go index ba27fd1b5ba0..6d186fbb11bb 100644 --- a/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go +++ b/exp/runtime/hooks/api/v1alpha1/zz_generated.openapi.go @@ -44,6 +44,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1.ClusterBuiltins": schema_runtime_hooks_api_v1alpha1_ClusterBuiltins(ref), "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1.ClusterNetworkBuiltins": schema_runtime_hooks_api_v1alpha1_ClusterNetworkBuiltins(ref), "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1.ClusterTopologyBuiltins": schema_runtime_hooks_api_v1alpha1_ClusterTopologyBuiltins(ref), + "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1.ClusterTopologyClusterClassRefBuiltins": schema_runtime_hooks_api_v1alpha1_ClusterTopologyClusterClassRefBuiltins(ref), "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1.CommonRequest": schema_runtime_hooks_api_v1alpha1_CommonRequest(ref), "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1.CommonResponse": schema_runtime_hooks_api_v1alpha1_CommonResponse(ref), "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1.CommonRetryResponse": schema_runtime_hooks_api_v1alpha1_CommonRetryResponse(ref), @@ -876,21 +877,61 @@ func schema_runtime_hooks_api_v1alpha1_ClusterTopologyBuiltins(ref common.Refere Format: "", }, }, + "classRef": { + SchemaProps: spec.SchemaProps{ + Description: "classRef is the ref to the ClusterClass that is used for the topology.", + Default: map[string]interface{}{}, + Ref: ref("sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1.ClusterTopologyClusterClassRefBuiltins"), + }, + }, "class": { SchemaProps: spec.SchemaProps{ - Description: "class is the name of the ClusterClass of the Cluster.", + Description: "class is the name of the ClusterClass of the Cluster.\n\nDeprecated: Class is deprecated in favor of ClassRef.Name and is going to be removed in the next apiVersion.", Type: []string{"string"}, Format: "", }, }, "classNamespace": { SchemaProps: spec.SchemaProps{ - Description: "classNamespace is the namespace of the ClusterClass of the Cluster.", + Description: "classNamespace is the namespace of the ClusterClass of the Cluster.\n\nDeprecated: ClassNamespace is deprecated in favor of ClassRef.Namespace and is going to be removed in the next apiVersion.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"classRef"}, + }, + }, + Dependencies: []string{ + "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1.ClusterTopologyClusterClassRefBuiltins"}, + } +} + +func schema_runtime_hooks_api_v1alpha1_ClusterTopologyClusterClassRefBuiltins(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ClusterTopologyClusterClassRefBuiltins is the ref to the ClusterClass that is used for the topology.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "name is the name of the ClusterClass that is used for the topology.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "namespace is the namespace of the ClusterClass that is used for the topology.", + Default: "", Type: []string{"string"}, Format: "", }, }, }, + Required: []string{"name", "namespace"}, }, }, } diff --git a/exp/runtime/topologymutation/variables_test.go b/exp/runtime/topologymutation/variables_test.go index df6492f04b0f..a21f790a9168 100644 --- a/exp/runtime/topologymutation/variables_test.go +++ b/exp/runtime/topologymutation/variables_test.go @@ -307,7 +307,7 @@ func TestMergeVariables(t *testing.T) { m, err := MergeVariableMaps( map[string]apiextensionsv1.JSON{ - runtimehooksv1.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"class":"clusterClass1","version":"v1.21.1"}}}`)}, + runtimehooksv1.BuiltinsName: {Raw: []byte(`{"cluster":{"name":"cluster-name","namespace":"default","topology":{"classRef":{"name":"clusterClass1","namespace":"default"},"class":"clusterClass1","version":"v1.21.1"}}}`)}, "a": {Raw: []byte("a-different")}, "c": {Raw: []byte("c")}, }, @@ -321,7 +321,7 @@ func TestMergeVariables(t *testing.T) { ) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(m).To(HaveKeyWithValue(runtimehooksv1.BuiltinsName, apiextensionsv1.JSON{Raw: []byte(`{"cluster":{"name":"cluster-name-overwrite","namespace":"default","topology":{"version":"v1.21.1","class":"clusterClass1"}},"controlPlane":{"replicas":3}}`)})) + g.Expect(m).To(HaveKeyWithValue(runtimehooksv1.BuiltinsName, apiextensionsv1.JSON{Raw: []byte(`{"cluster":{"name":"cluster-name-overwrite","namespace":"default","topology":{"version":"v1.21.1","classRef":{"name":"clusterClass1","namespace":"default"},"class":"clusterClass1"}},"controlPlane":{"replicas":3}}`)})) g.Expect(m).To(HaveKeyWithValue("a", apiextensionsv1.JSON{Raw: []byte("a")})) g.Expect(m).To(HaveKeyWithValue("b", apiextensionsv1.JSON{Raw: []byte("b")})) g.Expect(m).To(HaveKeyWithValue("c", apiextensionsv1.JSON{Raw: []byte("c")})) diff --git a/internal/apis/core/v1alpha4/conversion.go b/internal/apis/core/v1alpha4/conversion.go index 6e2225d7ca58..6250597e85ef 100644 --- a/internal/apis/core/v1alpha4/conversion.go +++ b/internal/apis/core/v1alpha4/conversion.go @@ -69,7 +69,7 @@ func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error { if dst.Spec.Topology == nil { dst.Spec.Topology = &clusterv1.Topology{} } - dst.Spec.Topology.ClassNamespace = restored.Spec.Topology.ClassNamespace + dst.Spec.Topology.ClassRef.Namespace = restored.Spec.Topology.ClassRef.Namespace dst.Spec.Topology.Variables = restored.Spec.Topology.Variables dst.Spec.Topology.ControlPlane.Variables = restored.Spec.Topology.ControlPlane.Variables @@ -557,7 +557,12 @@ func Convert_v1beta2_ClusterStatus_To_v1alpha4_ClusterStatus(in *clusterv1.Clust func Convert_v1beta2_Topology_To_v1alpha4_Topology(in *clusterv1.Topology, out *Topology, s apimachineryconversion.Scope) error { // spec.topology.variables has been added with v1beta1. - return autoConvert_v1beta2_Topology_To_v1alpha4_Topology(in, out, s) + if err := autoConvert_v1beta2_Topology_To_v1alpha4_Topology(in, out, s); err != nil { + return err + } + + out.Class = in.ClassRef.Name + return nil } // Convert_v1beta2_MachineDeploymentTopology_To_v1alpha4_MachineDeploymentTopology is an autogenerated conversion function. @@ -576,6 +581,15 @@ func Convert_v1beta2_ControlPlaneClass_To_v1alpha4_ControlPlaneClass(in *cluster return autoConvert_v1beta2_ControlPlaneClass_To_v1alpha4_ControlPlaneClass(in, out, s) } +func Convert_v1alpha4_Topology_To_v1beta2_Topology(in *Topology, out *clusterv1.Topology, s apimachineryconversion.Scope) error { + if err := autoConvert_v1alpha4_Topology_To_v1beta2_Topology(in, out, s); err != nil { + return err + } + + out.ClassRef.Name = in.Class + return nil +} + func Convert_v1beta2_ControlPlaneTopology_To_v1alpha4_ControlPlaneTopology(in *clusterv1.ControlPlaneTopology, out *ControlPlaneTopology, s apimachineryconversion.Scope) error { // controlPlaneTopology.nodeDrainTimeout has been added with v1beta1. return autoConvert_v1beta2_ControlPlaneTopology_To_v1alpha4_ControlPlaneTopology(in, out, s) diff --git a/internal/apis/core/v1alpha4/zz_generated.conversion.go b/internal/apis/core/v1alpha4/zz_generated.conversion.go index c5889b888f76..0c3beee43da0 100644 --- a/internal/apis/core/v1alpha4/zz_generated.conversion.go +++ b/internal/apis/core/v1alpha4/zz_generated.conversion.go @@ -339,11 +339,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*Topology)(nil), (*v1beta2.Topology)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha4_Topology_To_v1beta2_Topology(a.(*Topology), b.(*v1beta2.Topology), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*UnhealthyCondition)(nil), (*v1beta2.UnhealthyCondition)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha4_UnhealthyCondition_To_v1beta2_UnhealthyCondition(a.(*UnhealthyCondition), b.(*v1beta2.UnhealthyCondition), scope) }); err != nil { @@ -399,6 +394,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*Topology)(nil), (*v1beta2.Topology)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha4_Topology_To_v1beta2_Topology(a.(*Topology), b.(*v1beta2.Topology), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*v1beta2.ClusterClassSpec)(nil), (*ClusterClassSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta2_ClusterClassSpec_To_v1alpha4_ClusterClassSpec(a.(*v1beta2.ClusterClassSpec), b.(*ClusterClassSpec), scope) }); err != nil { @@ -1852,7 +1852,7 @@ func Convert_v1beta2_ObjectMeta_To_v1alpha4_ObjectMeta(in *v1beta2.ObjectMeta, o } func autoConvert_v1alpha4_Topology_To_v1beta2_Topology(in *Topology, out *v1beta2.Topology, s conversion.Scope) error { - out.Class = in.Class + // WARNING: in.Class requires manual conversion: does not exist in peer-type out.Version = in.Version out.RolloutAfter = (*v1.Time)(unsafe.Pointer(in.RolloutAfter)) if err := Convert_v1alpha4_ControlPlaneTopology_To_v1beta2_ControlPlaneTopology(&in.ControlPlane, &out.ControlPlane, s); err != nil { @@ -1870,14 +1870,8 @@ func autoConvert_v1alpha4_Topology_To_v1beta2_Topology(in *Topology, out *v1beta return nil } -// Convert_v1alpha4_Topology_To_v1beta2_Topology is an autogenerated conversion function. -func Convert_v1alpha4_Topology_To_v1beta2_Topology(in *Topology, out *v1beta2.Topology, s conversion.Scope) error { - return autoConvert_v1alpha4_Topology_To_v1beta2_Topology(in, out, s) -} - func autoConvert_v1beta2_Topology_To_v1alpha4_Topology(in *v1beta2.Topology, out *Topology, s conversion.Scope) error { - out.Class = in.Class - // WARNING: in.ClassNamespace requires manual conversion: does not exist in peer-type + // WARNING: in.ClassRef requires manual conversion: does not exist in peer-type out.Version = in.Version out.RolloutAfter = (*v1.Time)(unsafe.Pointer(in.RolloutAfter)) if err := Convert_v1beta2_ControlPlaneTopology_To_v1alpha4_ControlPlaneTopology(&in.ControlPlane, &out.ControlPlane, s); err != nil { diff --git a/internal/controllers/topology/cluster/blueprint_test.go b/internal/controllers/topology/cluster/blueprint_test.go index 387cab529add..4d37d5f73481 100644 --- a/internal/controllers/topology/cluster/blueprint_test.go +++ b/internal/controllers/topology/cluster/blueprint_test.go @@ -376,7 +376,7 @@ func TestGetBlueprint(t *testing.T) { // If no clusterClass is defined in the test case fill in a dummy value "foo". if tt.clusterClass == nil { - cluster.Spec.Topology.Class = "foo" + cluster.Spec.Topology.ClassRef.Name = "foo" } // Sets up the fakeClient for the test case. diff --git a/internal/controllers/topology/cluster/cluster_controller_test.go b/internal/controllers/topology/cluster/cluster_controller_test.go index 8c4ecc4ee491..83c792ebce6a 100644 --- a/internal/controllers/topology/cluster/cluster_controller_test.go +++ b/internal/controllers/topology/cluster/cluster_controller_test.go @@ -402,7 +402,7 @@ func TestClusterReconciler_reconcileClusterClassRebase(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) // Change the ClusterClass pointed to in the Cluster's Topology. This is a ClusterClass rebase operation. clusterWithRebase := actualCluster.DeepCopy() - clusterWithRebase.Spec.Topology.Class = clusterClassName2 + clusterWithRebase.Spec.Topology.ClassRef.Name = clusterClassName2 g.Expect(patchHelper.Patch(ctx, clusterWithRebase)).Should(Succeed()) // Check to ensure all objects are correctly reconciled with the new ClusterClass. diff --git a/internal/controllers/topology/cluster/patches/engine_test.go b/internal/controllers/topology/cluster/patches/engine_test.go index 5eeccf603c18..2394dbde9ba4 100644 --- a/internal/controllers/topology/cluster/patches/engine_test.go +++ b/internal/controllers/topology/cluster/patches/engine_test.go @@ -1100,7 +1100,9 @@ func setupTestObjects() (*scope.ClusterBlueprint, *scope.ClusterState) { InfrastructureRef: nil, Topology: &clusterv1.Topology{ Version: "v1.21.2", - Class: clusterClass.Name, + ClassRef: clusterv1.ClusterClassRef{ + Name: clusterClass.Name, + }, ControlPlane: clusterv1.ControlPlaneTopology{ Replicas: ptr.To[int32](3), Variables: &clusterv1.ControlPlaneVariables{ diff --git a/internal/controllers/topology/cluster/patches/variables/variables.go b/internal/controllers/topology/cluster/patches/variables/variables.go index cdf054c57c97..2c3f2b384143 100644 --- a/internal/controllers/topology/cluster/patches/variables/variables.go +++ b/internal/controllers/topology/cluster/patches/variables/variables.go @@ -59,6 +59,10 @@ func Global(clusterTopology *clusterv1.Topology, cluster *clusterv1.Cluster, pat Version: cluster.Spec.Topology.Version, Class: cluster.GetClassKey().Name, ClassNamespace: cluster.GetClassKey().Namespace, + ClassRef: runtimehooksv1.ClusterTopologyClusterClassRefBuiltins{ + Name: cluster.GetClassKey().Name, + Namespace: cluster.GetClassKey().Namespace, + }, }, }, } diff --git a/internal/controllers/topology/cluster/patches/variables/variables_test.go b/internal/controllers/topology/cluster/patches/variables/variables_test.go index ce2c48db1242..0b407643b9d3 100644 --- a/internal/controllers/topology/cluster/patches/variables/variables_test.go +++ b/internal/controllers/topology/cluster/patches/variables/variables_test.go @@ -74,7 +74,9 @@ func TestGlobal(t *testing.T) { }, Spec: clusterv1.ClusterSpec{ Topology: &clusterv1.Topology{ - Class: "clusterClass1", + ClassRef: clusterv1.ClusterClassRef{ + Name: "clusterClass1", + }, Version: "v1.21.1", }, ClusterNetwork: &clusterv1.ClusterNetwork{ @@ -107,6 +109,10 @@ func TestGlobal(t *testing.T) { "metadata": {"labels":{"foo":"bar"}, "annotations":{"fizz":"buzz"}}, "topology":{ "version": "v1.21.1", + "classRef": { + "name": "clusterClass1", + "namespace": "default" + }, "class": "clusterClass1", "classNamespace": "default" }, @@ -147,7 +153,9 @@ func TestGlobal(t *testing.T) { }, Spec: clusterv1.ClusterSpec{ Topology: &clusterv1.Topology{ - Class: "clusterClass1", + ClassRef: clusterv1.ClusterClassRef{ + Name: "clusterClass1", + }, Version: "v1.21.1", }, ClusterNetwork: &clusterv1.ClusterNetwork{ @@ -179,6 +187,10 @@ func TestGlobal(t *testing.T) { "uid": "8a35f406-6b9b-4b78-8c93-a7f878d90623", "topology":{ "version": "v1.21.1", + "classRef": { + "name": "clusterClass1", + "namespace": "default" + }, "class": "clusterClass1", "classNamespace": "default" }, @@ -220,7 +232,9 @@ func TestGlobal(t *testing.T) { }, Spec: clusterv1.ClusterSpec{ Topology: &clusterv1.Topology{ - Class: "clusterClass1", + ClassRef: clusterv1.ClusterClassRef{ + Name: "clusterClass1", + }, Version: "v1.21.1", }, ClusterNetwork: &clusterv1.ClusterNetwork{ @@ -251,6 +265,10 @@ func TestGlobal(t *testing.T) { "uid": "8a35f406-6b9b-4b78-8c93-a7f878d90623", "topology":{ "version": "v1.21.1", + "classRef": { + "name": "clusterClass1", + "namespace": "default" + }, "class": "clusterClass1", "classNamespace": "default" }, @@ -291,7 +309,9 @@ func TestGlobal(t *testing.T) { }, Spec: clusterv1.ClusterSpec{ Topology: &clusterv1.Topology{ - Class: "clusterClass1", + ClassRef: clusterv1.ClusterClassRef{ + Name: "clusterClass1", + }, Version: "v1.21.1", }, ClusterNetwork: &clusterv1.ClusterNetwork{ @@ -319,6 +339,10 @@ func TestGlobal(t *testing.T) { "uid": "8a35f406-6b9b-4b78-8c93-a7f878d90623", "topology":{ "version": "v1.21.1", + "classRef": { + "name": "clusterClass1", + "namespace": "default" + }, "class": "clusterClass1", "classNamespace": "default" }, @@ -358,7 +382,9 @@ func TestGlobal(t *testing.T) { }, Spec: clusterv1.ClusterSpec{ Topology: &clusterv1.Topology{ - Class: "clusterClass1", + ClassRef: clusterv1.ClusterClassRef{ + Name: "clusterClass1", + }, Version: "v1.21.1", }, ClusterNetwork: nil, @@ -382,6 +408,10 @@ func TestGlobal(t *testing.T) { "uid": "8a35f406-6b9b-4b78-8c93-a7f878d90623", "topology":{ "version": "v1.21.1", + "classRef": { + "name": "clusterClass1", + "namespace": "default" + }, "class": "clusterClass1", "classNamespace": "default" } diff --git a/internal/webhooks/cluster.go b/internal/webhooks/cluster.go index b038b28eb061..aa538aed7ef2 100644 --- a/internal/webhooks/cluster.go +++ b/internal/webhooks/cluster.go @@ -112,7 +112,7 @@ func (webhook *Cluster) Default(ctx context.Context, obj runtime.Object) error { allErrs, field.Required( field.NewPath("spec", "topology", "class"), - "class cannot be empty", + "classRef.name cannot be empty", ), ) return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("Cluster").GroupKind(), cluster.Name, allErrs) @@ -311,7 +311,7 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu allErrs, field.Required( fldPath.Child("class"), - "class cannot be empty", + "classRef.name cannot be empty", ), ) // Return early if there is no defined class to validate. @@ -395,7 +395,7 @@ func (webhook *Cluster) validateTopology(ctx context.Context, oldCluster, newClu allErrs, field.Forbidden( fldPath.Child("class"), - "class cannot be set on an existing Cluster", + "classRef cannot be set on an existing Cluster", ), ) // return early here if there is no class to compare. diff --git a/internal/webhooks/cluster_test.go b/internal/webhooks/cluster_test.go index 77c8e9f31e0e..d538b6a80e8d 100644 --- a/internal/webhooks/cluster_test.go +++ b/internal/webhooks/cluster_test.go @@ -175,7 +175,9 @@ func TestClusterDefaultAndValidateVariables(t *testing.T) { topology: &clusterv1.Topology{}, oldTopology: &clusterv1.Topology{}, expect: &clusterv1.Topology{ - Class: "class1", + ClassRef: clusterv1.ClusterClassRef{ + Name: "class1", + }, Version: "v1.22.2", Variables: []clusterv1.ClusterVariable{}, }, @@ -1296,10 +1298,10 @@ func TestClusterDefaultAndValidateVariables(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setting Class and Version here to avoid obfuscating the test cases above. - tt.topology.Class = "class1" + tt.topology.ClassRef.Name = "class1" tt.topology.Version = "v1.22.2" if tt.expect != nil { - tt.expect.Class = "class1" + tt.expect.ClassRef.Name = "class1" tt.expect.Version = "v1.22.2" } @@ -3014,7 +3016,7 @@ func TestClusterTopologyValidationForTopologyClassChange(t *testing.T) { // Create and updated cluster which uses the name of the second class from the test definition in its '.spec.topology.' secondCluster := cluster.DeepCopy() - secondCluster.Spec.Topology.Class = tt.secondClass.Name + secondCluster.Spec.Topology.ClassRef.Name = tt.secondClass.Name // Checks the return error. warnings, err := c.ValidateUpdate(ctx, cluster, secondCluster) @@ -3281,7 +3283,7 @@ func TestClusterClassPollingErrors(t *testing.T) { injectedErr: interceptor.Funcs{ Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error { // Throw an error if the second ClusterClass `class2` used as the new ClusterClass is being retrieved. - if key.Name == secondTopology.Class { + if key.Name == secondTopology.ClassRef.Name { return errors.New("connection error") } return client.Get(ctx, key, obj) @@ -3298,7 +3300,7 @@ func TestClusterClassPollingErrors(t *testing.T) { injectedErr: interceptor.Funcs{ Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error { // Throw an error if the ClusterClass `class1` used as the old ClusterClass is being retrieved. - if key.Name == topology.Class { + if key.Name == topology.ClassRef.Name { return errors.New("connection error") } return client.Get(ctx, key, obj) diff --git a/internal/webhooks/patch_validation.go b/internal/webhooks/patch_validation.go index dfc80de69448..32c4e85323fd 100644 --- a/internal/webhooks/patch_validation.go +++ b/internal/webhooks/patch_validation.go @@ -477,6 +477,8 @@ var builtinVariables = sets.Set[string]{}.Insert( // ClusterTopology builtins. "builtin.cluster.topology", + "builtin.cluster.topology.classRef.name", + "builtin.cluster.topology.classRef.namespace", "builtin.cluster.topology.class", "builtin.cluster.topology.classNamespace", "builtin.cluster.topology.version", diff --git a/test/e2e/clusterclass_changes.go b/test/e2e/clusterclass_changes.go index deffe6db889e..470c6a9d9935 100644 --- a/test/e2e/clusterclass_changes.go +++ b/test/e2e/clusterclass_changes.go @@ -802,8 +802,8 @@ func rebaseClusterClassAndWait(ctx context.Context, input rebaseClusterClassAndW // Rebase the Cluster to the new ClusterClass. patchHelper, err := patch.NewHelper(input.Cluster, mgmtClient) Expect(err).ToNot(HaveOccurred()) - input.Cluster.Spec.Topology.Class = newClusterClassName - input.Cluster.Spec.Topology.ClassNamespace = input.ClusterClassNamespace + input.Cluster.Spec.Topology.ClassRef.Name = newClusterClassName + input.Cluster.Spec.Topology.ClassRef.Namespace = input.ClusterClassNamespace // We have to retry the patch. The ClusterClass was just created so the client cache in the // controller/webhook might not be aware of it yet. If the webhook is not aware of the ClusterClass // we get a "Cluster ... can't be validated. ClusterClass ... can not be retrieved" error. diff --git a/test/extension/handlers/topologymutation/handler_integration_test.go b/test/extension/handlers/topologymutation/handler_integration_test.go index 1627fc74c40e..86f638e9a00d 100644 --- a/test/extension/handlers/topologymutation/handler_integration_test.go +++ b/test/extension/handlers/topologymutation/handler_integration_test.go @@ -146,7 +146,9 @@ func getCluster() *clusterv1.Cluster { Topology: &clusterv1.Topology{ Version: "v1.29.0", // NOTE: Class name must match the ClusterClass name. - Class: "quick-start-runtimesdk", + ClassRef: clusterv1.ClusterClassRef{ + Name: "quick-start-runtimesdk", + }, ControlPlane: clusterv1.ControlPlaneTopology{ Replicas: ptr.To[int32](1), }, diff --git a/test/framework/clusterctl/clusterctl_helpers.go b/test/framework/clusterctl/clusterctl_helpers.go index 09e34f4c8aea..85a7158427a9 100644 --- a/test/framework/clusterctl/clusterctl_helpers.go +++ b/test/framework/clusterctl/clusterctl_helpers.go @@ -449,7 +449,7 @@ func ApplyCustomClusterTemplateAndWait(ctx context.Context, input ApplyCustomClu result.ClusterClass = framework.GetClusterClassByName(ctx, framework.GetClusterClassByNameInput{ Getter: input.ClusterProxy.GetClient(), Namespace: result.Cluster.GetClassKey().Namespace, - Name: result.Cluster.Spec.Topology.Class, + Name: result.Cluster.GetClassKey().Name, }) } diff --git a/util/test/builder/builders.go b/util/test/builder/builders.go index 16a4316853c1..43bc735b1183 100644 --- a/util/test/builder/builders.go +++ b/util/test/builder/builders.go @@ -187,10 +187,12 @@ func (c *ClusterTopologyBuilder) WithVariables(vars ...clusterv1.ClusterVariable // Build returns a testable cluster Topology object with any values passed to the builder. func (c *ClusterTopologyBuilder) Build() *clusterv1.Topology { t := &clusterv1.Topology{ - Class: c.class, - ClassNamespace: c.classNamespace, - Workers: c.workers, - Version: c.version, + ClassRef: clusterv1.ClusterClassRef{ + Name: c.class, + Namespace: c.classNamespace, + }, + Workers: c.workers, + Version: c.version, ControlPlane: clusterv1.ControlPlaneTopology{ Replicas: &c.controlPlaneReplicas, MachineHealthCheck: c.controlPlaneMHC,