Skip to content

Commit d9a16c9

Browse files
authored
Merge pull request #4928 from fabriziopandini/clusterclass-api-types
⚠️ Add ClusterClass types
2 parents 9719e4f + fb67f57 commit d9a16c9

20 files changed

+2510
-116
lines changed

PROJECT

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ resources:
1313
- group: cluster
1414
kind: MachineDeployment
1515
version: v1alpha3
16+
- group: cluster
17+
kind: ClusterClass
18+
version: v1alpha4
1619
- group: cluster
1720
kind: Cluster
1821
version: v1alpha4

api/v1alpha3/conversion.go

+21-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error {
3838
conditions.MarkTrue(dst, v1alpha4.ControlPlaneInitializedCondition)
3939
}
4040

41+
// Manually restore data.
42+
restored := &v1alpha4.Cluster{}
43+
if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok {
44+
return err
45+
}
46+
47+
if restored.Spec.Topology != nil {
48+
dst.Spec.Topology = restored.Spec.Topology
49+
}
50+
4151
return nil
4252
}
4353

@@ -53,6 +63,11 @@ func (dst *Cluster) ConvertFrom(srcRaw conversion.Hub) error {
5363
dst.Status.ControlPlaneInitialized = true
5464
}
5565

66+
// Preserve Hub data on down-conversion except for metadata
67+
if err := utilconversion.MarshalData(src, dst); err != nil {
68+
return err
69+
}
70+
5671
return nil
5772
}
5873

@@ -222,8 +237,12 @@ func (dst *MachineHealthCheckList) ConvertFrom(srcRaw conversion.Hub) error {
222237
return Convert_v1alpha4_MachineHealthCheckList_To_v1alpha3_MachineHealthCheckList(src, dst, nil)
223238
}
224239

225-
// Convert_v1alpha3_Bootstrap_To_v1alpha4_Bootstrap is an autogenerated conversion function.
226-
func Convert_v1alpha3_Bootstrap_To_v1alpha4_Bootstrap(in *Bootstrap, out *v1alpha4.Bootstrap, s apiconversion.Scope) error { //nolint
240+
func Convert_v1alpha4_ClusterSpec_To_v1alpha3_ClusterSpec(in *v1alpha4.ClusterSpec, out *ClusterSpec, s apiconversion.Scope) error {
241+
// NOTE: custom conversion func is required because spec.Topology does not exists in v1alpha3
242+
return autoConvert_v1alpha4_ClusterSpec_To_v1alpha3_ClusterSpec(in, out, s)
243+
}
244+
245+
func Convert_v1alpha3_Bootstrap_To_v1alpha4_Bootstrap(in *Bootstrap, out *v1alpha4.Bootstrap, s apiconversion.Scope) error {
227246
return autoConvert_v1alpha3_Bootstrap_To_v1alpha4_Bootstrap(in, out, s)
228247
}
229248

api/v1alpha3/zz_generated.conversion.go

+6-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1alpha4/cluster_types.go

+67
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,73 @@ type ClusterSpec struct {
6060
// for provisioning infrastructure for a cluster in said provider.
6161
// +optional
6262
InfrastructureRef *corev1.ObjectReference `json:"infrastructureRef,omitempty"`
63+
64+
// This encapsulates the topology for the cluster.
65+
// NOTE: It is required to enable the ClusterTopology
66+
// feature gate flag to activate managed topologies support;
67+
// this feature is highly experimental, and parts of it might still be not implemented.
68+
// +optional
69+
Topology *Topology `json:"topology,omitempty"`
70+
}
71+
72+
// Topology encapsulates the information of the managed resources.
73+
type Topology struct {
74+
// The name of the ClusterClass object to create the topology.
75+
Class string `json:"class"`
76+
77+
// The Kubernetes version of the cluster.
78+
Version string `json:"version"`
79+
80+
// RolloutAfter performs a rollout of the entire cluster one component at a time,
81+
// control plane first and then machine deployments.
82+
// +optional
83+
RolloutAfter *metav1.Time `json:"rolloutAfter,omitempty"`
84+
85+
// ControlPlane describes the cluster control plane.
86+
ControlPlane ControlPlaneTopology `json:"controlPlane"`
87+
88+
// Workers encapsulates the different constructs that form the worker nodes
89+
// for the cluster.
90+
// +optional
91+
Workers *WorkersTopology `json:"workers,omitempty"`
92+
}
93+
94+
// ControlPlaneTopology specifies the parameters for the control plane nodes in the cluster.
95+
type ControlPlaneTopology struct {
96+
Metadata ObjectMeta `json:"metadata,omitempty"`
97+
98+
// Replicas is the number of control plane nodes.
99+
Replicas int `json:"replicas"`
100+
}
101+
102+
// WorkersTopology represents the different sets of worker nodes in the cluster.
103+
type WorkersTopology struct {
104+
// MachineDeployments is a list of machine deployments in the cluster.
105+
MachineDeployments []MachineDeploymentTopology `json:"machineDeployments,omitempty"`
106+
}
107+
108+
// MachineDeploymentTopology specifies the different parameters for a set of worker nodes in the topology.
109+
// This set of nodes is managed by a MachineDeployment object whose lifecycle is managed by the Cluster controller.
110+
type MachineDeploymentTopology struct {
111+
Metadata ObjectMeta `json:"metadata,omitempty"`
112+
113+
// Class is the name of the MachineDeploymentClass used to create the set of worker nodes.
114+
// This should match one of the deployment classes defined in the ClusterClass object
115+
// mentioned in the `Cluster.Spec.Class` field.
116+
Class string `json:"class"`
117+
118+
// Name is the unique identifier for this MachineDeploymentTopology.
119+
// The value is used with other unique identifiers to create a MachineDeployment's Name
120+
// (e.g. cluster's name, etc). In case the name is greater than the allowed maximum length,
121+
// the values are hashed together.
122+
Name string `json:"name"`
123+
124+
// Replicas is the number of worker nodes belonging to this set.
125+
// If the value is nil, the MachineDeployment is created without the number of Replicas (defaulting to zero)
126+
// and it's assumed that an external entity (like cluster autoscaler) is responsible for the management
127+
// of this value.
128+
// +optional
129+
Replicas *int `json:"replicas,omitempty"`
63130
}
64131

65132
// ANCHOR_END: ClusterSpec

api/v1alpha4/cluster_webhook.go

+159-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,16 @@ limitations under the License.
1717
package v1alpha4
1818

1919
import (
20+
"fmt"
21+
"strings"
22+
23+
"github.com/blang/semver"
2024
apierrors "k8s.io/apimachinery/pkg/api/errors"
2125
"k8s.io/apimachinery/pkg/runtime"
26+
"k8s.io/apimachinery/pkg/util/sets"
2227
"k8s.io/apimachinery/pkg/util/validation/field"
28+
"sigs.k8s.io/cluster-api/feature"
29+
"sigs.k8s.io/cluster-api/util/version"
2330
ctrl "sigs.k8s.io/controller-runtime"
2431
"sigs.k8s.io/controller-runtime/pkg/webhook"
2532
)
@@ -45,24 +52,36 @@ func (c *Cluster) Default() {
4552
if c.Spec.ControlPlaneRef != nil && len(c.Spec.ControlPlaneRef.Namespace) == 0 {
4653
c.Spec.ControlPlaneRef.Namespace = c.Namespace
4754
}
55+
56+
// If the Cluster uses a managed topology
57+
if c.Spec.Topology != nil {
58+
// tolerate version strings without a "v" prefix: prepend it if it's not there
59+
if !strings.HasPrefix(c.Spec.Topology.Version, "v") {
60+
c.Spec.Topology.Version = "v" + c.Spec.Topology.Version
61+
}
62+
}
4863
}
4964

5065
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
5166
func (c *Cluster) ValidateCreate() error {
52-
return c.validate()
67+
return c.validate(nil)
5368
}
5469

5570
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
5671
func (c *Cluster) ValidateUpdate(old runtime.Object) error {
57-
return c.validate()
72+
oldCluster, ok := old.(*Cluster)
73+
if !ok {
74+
return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", old))
75+
}
76+
return c.validate(oldCluster)
5877
}
5978

6079
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
6180
func (c *Cluster) ValidateDelete() error {
6281
return nil
6382
}
6483

65-
func (c *Cluster) validate() error {
84+
func (c *Cluster) validate(old *Cluster) error {
6685
var allErrs field.ErrorList
6786
if c.Spec.InfrastructureRef != nil && c.Spec.InfrastructureRef.Namespace != c.Namespace {
6887
allErrs = append(
@@ -86,8 +105,145 @@ func (c *Cluster) validate() error {
86105
)
87106
}
88107

108+
// Validate the managed topology, if defined.
109+
if c.Spec.Topology != nil {
110+
if topologyErrs := c.validateTopology(old); len(topologyErrs) > 0 {
111+
allErrs = append(allErrs, topologyErrs...)
112+
}
113+
}
114+
89115
if len(allErrs) == 0 {
90116
return nil
91117
}
92118
return apierrors.NewInvalid(GroupVersion.WithKind("Cluster").GroupKind(), c.Name, allErrs)
93119
}
120+
121+
func (c *Cluster) validateTopology(old *Cluster) field.ErrorList {
122+
// NOTE: ClusterClass and managed topologies are behind ClusterTopology feature gate flag; the web hook
123+
// must prevent the usage of Cluster.Topology in case the feature flag is disabled.
124+
if !feature.Gates.Enabled(feature.ClusterTopology) {
125+
return field.ErrorList{
126+
field.Forbidden(
127+
field.NewPath("spec", "topology"),
128+
"can be set only if the ClusterTopology feature flag is enabled",
129+
),
130+
}
131+
}
132+
133+
var allErrs field.ErrorList
134+
135+
// class should be defined.
136+
if len(c.Spec.Topology.Class) == 0 {
137+
allErrs = append(
138+
allErrs,
139+
field.Invalid(
140+
field.NewPath("spec", "topology", "class"),
141+
c.Spec.Topology.Class,
142+
"cannot be empty",
143+
),
144+
)
145+
}
146+
147+
// version should be valid.
148+
if !version.KubeSemver.MatchString(c.Spec.Topology.Version) {
149+
allErrs = append(
150+
allErrs,
151+
field.Invalid(
152+
field.NewPath("spec", "topology", "version"),
153+
c.Spec.Topology.Version,
154+
"must be a valid semantic version",
155+
),
156+
)
157+
}
158+
159+
// MachineDeployment names must be unique.
160+
if c.Spec.Topology.Workers != nil {
161+
names := sets.String{}
162+
for _, md := range c.Spec.Topology.Workers.MachineDeployments {
163+
if names.Has(md.Name) {
164+
allErrs = append(allErrs,
165+
field.Invalid(
166+
field.NewPath("spec", "topology", "workers", "machineDeployments"),
167+
md,
168+
fmt.Sprintf("MachineDeployment names should be unique. MachineDeployment with name %q is defined more than once.", md.Name),
169+
),
170+
)
171+
}
172+
names.Insert(md.Name)
173+
}
174+
}
175+
176+
switch old {
177+
case nil: // On create
178+
// c.Spec.InfrastructureRef and c.Spec.ControlPlaneRef could not be set
179+
if c.Spec.InfrastructureRef != nil {
180+
allErrs = append(
181+
allErrs,
182+
field.Invalid(
183+
field.NewPath("spec", "infrastructureRef"),
184+
c.Spec.InfrastructureRef,
185+
"cannot be set when a Topology is defined",
186+
),
187+
)
188+
}
189+
if c.Spec.ControlPlaneRef != nil {
190+
allErrs = append(
191+
allErrs,
192+
field.Invalid(
193+
field.NewPath("spec", "controlPlaneRef"),
194+
c.Spec.ControlPlaneRef,
195+
"cannot be set when a Topology is defined",
196+
),
197+
)
198+
}
199+
default: // On update
200+
// Class could not be mutated.
201+
if c.Spec.Topology.Class != old.Spec.Topology.Class {
202+
allErrs = append(
203+
allErrs,
204+
field.Invalid(
205+
field.NewPath("spec", "topology", "class"),
206+
c.Spec.Topology.Class,
207+
"class cannot be changed",
208+
),
209+
)
210+
}
211+
212+
// Version could only be increased.
213+
inVersion, err := semver.ParseTolerant(c.Spec.Topology.Version)
214+
if err != nil {
215+
allErrs = append(
216+
allErrs,
217+
field.Invalid(
218+
field.NewPath("spec", "topology", "version"),
219+
c.Spec.Topology.Version,
220+
"is not a valid version",
221+
),
222+
)
223+
}
224+
oldVersion, err := semver.ParseTolerant(old.Spec.Topology.Version)
225+
if err != nil {
226+
// NOTE: this should never happen. Nevertheless, handling this for extra caution.
227+
allErrs = append(
228+
allErrs,
229+
field.Invalid(
230+
field.NewPath("spec", "topology", "version"),
231+
c.Spec.Topology.Class,
232+
"cannot be compared with the old version",
233+
),
234+
)
235+
}
236+
if inVersion.NE(semver.Version{}) && oldVersion.NE(semver.Version{}) && !inVersion.GTE(oldVersion) {
237+
allErrs = append(
238+
allErrs,
239+
field.Invalid(
240+
field.NewPath("spec", "topology", "version"),
241+
c.Spec.Topology.Version,
242+
"cannot be decreased",
243+
),
244+
)
245+
}
246+
}
247+
248+
return allErrs
249+
}

0 commit comments

Comments
 (0)