Skip to content

Commit

Permalink
Add ClusterClass types
Browse files Browse the repository at this point in the history
  • Loading branch information
fabriziopandini committed Jul 16, 2021
1 parent 831ed7d commit f021a62
Show file tree
Hide file tree
Showing 20 changed files with 2,472 additions and 86 deletions.
3 changes: 3 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ resources:
- group: cluster
kind: MachineDeployment
version: v1alpha3
- group: cluster
kind: ClusterClass
version: v1alpha4
- group: cluster
kind: Cluster
version: v1alpha4
Expand Down
23 changes: 21 additions & 2 deletions api/v1alpha3/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ func (src *Cluster) ConvertTo(dstRaw conversion.Hub) error {
conditions.MarkTrue(dst, v1alpha4.ControlPlaneInitializedCondition)
}

// Manually restore data.
restored := &v1alpha4.Cluster{}
if ok, err := utilconversion.UnmarshalData(src, restored); err != nil || !ok {
return err
}

if restored.Spec.Topology != nil {
dst.Spec.Topology = restored.Spec.Topology
}

return nil
}

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

// Preserve Hub data on down-conversion except for metadata
if err := utilconversion.MarshalData(src, dst); err != nil {
return err
}

return nil
}

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

// Convert_v1alpha3_Bootstrap_To_v1alpha4_Bootstrap is an autogenerated conversion function.
func Convert_v1alpha3_Bootstrap_To_v1alpha4_Bootstrap(in *Bootstrap, out *v1alpha4.Bootstrap, s apiconversion.Scope) error { //nolint
func Convert_v1alpha4_ClusterSpec_To_v1alpha3_ClusterSpec(in *v1alpha4.ClusterSpec, out *ClusterSpec, s apiconversion.Scope) error {
// NOTE: custom conversion func is required because spec.Topology does not exists in v1alpha3
return autoConvert_v1alpha4_ClusterSpec_To_v1alpha3_ClusterSpec(in, out, s)
}

func Convert_v1alpha3_Bootstrap_To_v1alpha4_Bootstrap(in *Bootstrap, out *v1alpha4.Bootstrap, s apiconversion.Scope) error {
return autoConvert_v1alpha3_Bootstrap_To_v1alpha4_Bootstrap(in, out, s)
}

Expand Down
16 changes: 6 additions & 10 deletions api/v1alpha3/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions api/v1alpha4/cluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,72 @@ type ClusterSpec struct {
// for provisioning infrastructure for a cluster in said provider.
// +optional
InfrastructureRef *corev1.ObjectReference `json:"infrastructureRef,omitempty"`

// This encapsulates the topology for the cluster.
// NOTE: This feature is alpha; it is required to enable the ClusterTopology
// feature gate flag to activate managed topologies support.
// +optional
Topology *Topology `json:"topology,omitempty"`
}

// Topology encapsulates the information of the managed resources.
type Topology struct {
// The name of the ClusterClass object to create the topology.
Class string `json:"class"`

// The kubernetes version of the cluster.
Version string `json:"version"`

// RolloutAfter performs a rollout of the entire cluster one component at a time,
// control plane first and then machine deployments.
// +optional
RolloutAfter *metav1.Time `json:"rolloutAfter,omitempty"`

// The information for the Control plane of the cluster.
ControlPlane ControlPlaneTopology `json:"controlPlane"`

// Workers encapsulates the different constructs that form the worker nodes
// for the cluster.
// +optional
Workers *WorkersTopology `json:"workers,omitempty"`
}

// ControlPlaneTopology specifies the parameters for the control plane nodes in the cluster.
type ControlPlaneTopology struct {
Metadata ObjectMeta `json:"metadata,omitempty"`

// The number of control plane nodes.
Replicas int `json:"replicas"`
}

// WorkersTopology represents the different sets of worker nodes in the cluster.
type WorkersTopology struct {
// MachineDeployments is a list of machine deployment in the cluster.
MachineDeployments []MachineDeploymentTopology `json:"machineDeployments,omitempty"`
}

// MachineDeploymentTopology specifies the different parameters for a set of worker nodes in the topology.
// This set of nodes is managed by a MachineDeployment object whose lifecycle is managed by the Cluster controller.
type MachineDeploymentTopology struct {
Metadata ObjectMeta `json:"metadata,omitempty"`

// Class is the name of the MachineDeploymentClass used to create the set of worker nodes.
// This should match one of the deployment classes defined in the ClusterClass object
// mentioned in the `Cluster.Spec.Class` field.
Class string `json:"class"`

// Name is the unique identifier for this MachineDeploymentTopology.
// The value is used with other unique identifiers to create a MachineDeployment's Name
// (e.g. cluster's name, etc). In case the name is greater than the allowed maximum length,
// the values are hashed together.
Name string `json:"name"`

// The number of worker nodes belonging to this set.
// If the value is nil, the MachineDeployment is created without the number of Replicas (defaulting to zero)
// and it's assumed that an external entity (like cluster autoscaler) is responsible for the management
// of this value.
// +optional
Replicas *int `json:"replicas,omitempty"`
}

// ANCHOR_END: ClusterSpec
Expand Down
162 changes: 159 additions & 3 deletions api/v1alpha4/cluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@ limitations under the License.
package v1alpha4

import (
"fmt"
"strings"

"github.com/blang/semver"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/cluster-api/feature"
"sigs.k8s.io/cluster-api/util/version"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)
Expand All @@ -45,24 +52,36 @@ func (c *Cluster) Default() {
if c.Spec.ControlPlaneRef != nil && len(c.Spec.ControlPlaneRef.Namespace) == 0 {
c.Spec.ControlPlaneRef.Namespace = c.Namespace
}

// If the Cluster uses a managed topology
if c.Spec.Topology != nil {
// tolerate version strings without a "v" prefix: prepend it if it's not there
if !strings.HasPrefix(c.Spec.Topology.Version, "v") {
c.Spec.Topology.Version = "v" + c.Spec.Topology.Version
}
}
}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
func (c *Cluster) ValidateCreate() error {
return c.validate()
return c.validate(nil)
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
func (c *Cluster) ValidateUpdate(old runtime.Object) error {
return c.validate()
oldCluster, ok := old.(*Cluster)
if !ok {
return apierrors.NewBadRequest(fmt.Sprintf("expected a Cluster but got a %T", old))
}
return c.validate(oldCluster)
}

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

func (c *Cluster) validate() error {
func (c *Cluster) validate(old *Cluster) error {
var allErrs field.ErrorList
if c.Spec.InfrastructureRef != nil && c.Spec.InfrastructureRef.Namespace != c.Namespace {
allErrs = append(
Expand All @@ -86,8 +105,145 @@ func (c *Cluster) validate() error {
)
}

// Validate the managed topology, if defined.
if c.Spec.Topology != nil {
if topologyErrs := c.validateTopology(old); len(topologyErrs) > 0 {
allErrs = append(allErrs, topologyErrs...)
}
}

if len(allErrs) == 0 {
return nil
}
return apierrors.NewInvalid(GroupVersion.WithKind("Cluster").GroupKind(), c.Name, allErrs)
}

func (c *Cluster) validateTopology(old *Cluster) field.ErrorList {
// NOTE: ClusterClass and managed topologies are behind ClusterTopology feature gate flag; the web hook
// must prevent the usage of Cluster.Topology in case the feature flag is disabled.
if !feature.Gates.Enabled(feature.ClusterTopology) {
return field.ErrorList{
field.Forbidden(
field.NewPath("spec", "topology"),
"can be set only if the ClusterTopology feature flag is enabled",
),
}
}

var allErrs field.ErrorList

// class should be defined.
if len(c.Spec.Topology.Class) == 0 {
allErrs = append(
allErrs,
field.Invalid(
field.NewPath("spec", "topology", "class"),
c.Spec.Topology.Class,
"cannot be empty",
),
)
}

// version should be valid.
if !version.KubeSemver.MatchString(c.Spec.Topology.Version) {
allErrs = append(
allErrs,
field.Invalid(
field.NewPath("spec", "topology", "version"),
c.Spec.Topology.Version,
"must be a valid semantic version",
),
)
}

// MachineDeployment names must be unique.
if c.Spec.Topology.Workers != nil {
names := sets.String{}
for _, md := range c.Spec.Topology.Workers.MachineDeployments {
if names.Has(md.Name) {
allErrs = append(allErrs,
field.Invalid(
field.NewPath("spec", "topology", "workers", "machineDeployments"),
md,
fmt.Sprintf("MachineDeployment names should be unique. MachineDeployment with name %q is defined more than once.", md.Name),
),
)
}
names.Insert(md.Name)
}
}

switch old {
case nil: // On create
// c.Spec.InfrastructureRef and c.Spec.ControlPlaneRef could not be set
if c.Spec.InfrastructureRef != nil {
allErrs = append(
allErrs,
field.Invalid(
field.NewPath("spec", "infrastructureRef"),
c.Spec.InfrastructureRef,
"cannot be set when a Topology is defined",
),
)
}
if c.Spec.ControlPlaneRef != nil {
allErrs = append(
allErrs,
field.Invalid(
field.NewPath("spec", "controlPlaneRef"),
c.Spec.ControlPlaneRef,
"cannot be set when a Topology is defined",
),
)
}
default: // On update
// Class could not be mutated.
if c.Spec.Topology.Class != old.Spec.Topology.Class {
allErrs = append(
allErrs,
field.Invalid(
field.NewPath("spec", "topology", "class"),
c.Spec.Topology.Class,
"class cannot be changed",
),
)
}

// Version could only be increased.
inVersion, err := semver.ParseTolerant(c.Spec.Topology.Version)
if err != nil {
allErrs = append(
allErrs,
field.Invalid(
field.NewPath("spec", "topology", "version"),
c.Spec.Topology.Version,
"is not a valid version",
),
)
}
oldVersion, err := semver.ParseTolerant(old.Spec.Topology.Version)
if err != nil {
// NOTE: this should never happen. Nevertheless, handling this for extra caution.
allErrs = append(
allErrs,
field.Invalid(
field.NewPath("spec", "topology", "version"),
c.Spec.Topology.Class,
"cannot be compared with the old version",
),
)
}
if inVersion.NE(semver.Version{}) && oldVersion.NE(semver.Version{}) && !inVersion.GTE(oldVersion) {
allErrs = append(
allErrs,
field.Invalid(
field.NewPath("spec", "topology", "version"),
c.Spec.Topology.Version,
"cannot be decreased",
),
)
}
}

return allErrs
}
Loading

0 comments on commit f021a62

Please sign in to comment.