Skip to content

Commit

Permalink
Implement MachinePool Machines in CAPI, CAPD, and clusterctl
Browse files Browse the repository at this point in the history
  • Loading branch information
Jont828 committed Jun 6, 2023
1 parent 60b1426 commit e4c9c9f
Show file tree
Hide file tree
Showing 34 changed files with 1,735 additions and 226 deletions.
5 changes: 5 additions & 0 deletions api/v1beta1/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ const (
// update that disallows a pre-existing Cluster to be populated with Topology information and Class.
ClusterTopologyUnsafeUpdateClassNameAnnotation = "unsafe.topology.cluster.x-k8s.io/disable-update-class-name-check"

// FallbackMachineLabel indicates that a Machine belongs to a MachinePool that does not support MachinePool Machines.
// As such, these Machines exist to create a consistent user experience and will not have an infrastructure reference. The user will
// also be prevented from deleting these Machines.
FallbackMachineLabel = "machinepool.cluster.x-k8s.io/fallback-machine"

// ProviderNameLabel is the label set on components in the provider manifest.
// This label allows to easily identify all the components belonging to a provider; the clusterctl
// tool uses this label for implementing provider's lifecycle operations.
Expand Down
4 changes: 4 additions & 0 deletions api/v1beta1/machine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ const (
// MachineDeploymentNameLabel is the label set on machines if they're controlled by MachineDeployment.
MachineDeploymentNameLabel = "cluster.x-k8s.io/deployment-name"

// MachinePoolNameLabel is the label indicating the name of the MachinePool a Machine is controlled by.
// Note: The value of this label may be a hash if the MachinePool name is longer than 63 characters.
MachinePoolNameLabel = "cluster.x-k8s.io/pool-name"

// MachineControlPlaneNameLabel is the label set on machines if they're controlled by a ControlPlane.
// Note: The value of this label may be a hash if the control plane name is longer than 63 characters.
MachineControlPlaneNameLabel = "cluster.x-k8s.io/control-plane-name"
Expand Down
135 changes: 108 additions & 27 deletions api/v1beta1/machine_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ limitations under the License.
package v1beta1

import (
"context"
"fmt"
"os"
"strings"
"time"

Expand All @@ -36,14 +38,15 @@ const defaultNodeDeletionTimeout = 10 * time.Second

func (m *Machine) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(m).
For(&Machine{}).
WithValidator(MachineValidator(mgr.GetScheme())).
Complete()
}

// +kubebuilder:webhook:verbs=create;update,path=/validate-cluster-x-k8s-io-v1beta1-machine,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=machines,versions=v1beta1,name=validation.machine.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
// +kubebuilder:webhook:verbs=create;update;delete,path=/validate-cluster-x-k8s-io-v1beta1-machine,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=machines,versions=v1beta1,name=validation.machine.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
// +kubebuilder:webhook:verbs=create;update,path=/mutate-cluster-x-k8s-io-v1beta1-machine,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=machines,versions=v1beta1,name=default.machine.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1

var _ webhook.Validator = &Machine{}
var _ webhook.CustomValidator = &machineValidator{}
var _ webhook.Defaulter = &Machine{}

// Default implements webhook.Defaulter so a webhook will be registered for the type.
Expand All @@ -58,7 +61,10 @@ func (m *Machine) Default() {
}

if m.Spec.InfrastructureRef.Namespace == "" {
m.Spec.InfrastructureRef.Namespace = m.Namespace
// Don't autofill namespace for MachinePool Machines since the infraRef will be populated by the MachinePool controller.
if !isMachinePoolMachine(m) {
m.Spec.InfrastructureRef.Namespace = m.Namespace
}
}

if m.Spec.Version != nil && !strings.HasPrefix(*m.Spec.Version, "v") {
Expand All @@ -71,36 +77,94 @@ func (m *Machine) Default() {
}
}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
func (m *Machine) ValidateCreate() (admission.Warnings, error) {
// MachineValidator creates a new CustomValidator for Machines.
func MachineValidator(_ *runtime.Scheme) webhook.CustomValidator {
return &machineValidator{}
}

// machineValidator implements a defaulting webhook for Machine.
type machineValidator struct{}

// // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
// func (m *Machine) ValidateCreate() (admission.Warnings, error) {
// return nil, m.validate(nil)
// }

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

// // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
//
// func (m *Machine) ValidateDelete() (admission.Warnings, error) {
// return nil, nil
func (*machineValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
m, ok := obj.(*Machine)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", obj))
}

return nil, m.validate(nil)
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
func (m *Machine) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
oldM, ok := old.(*Machine)
func (*machineValidator) ValidateUpdate(_ context.Context, oldObj runtime.Object, newObj runtime.Object) (admission.Warnings, error) {
newM, ok := newObj.(*Machine)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", newObj))
}

oldM, ok := oldObj.(*Machine)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", old))
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", oldObj))
}
return nil, m.validate(oldM)
return nil, newM.validate(oldM)
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
func (m *Machine) ValidateDelete() (admission.Warnings, error) {
func (*machineValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
m, ok := obj.(*Machine)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", obj))
}

req, err := admission.RequestFromContext(ctx)
if err != nil {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a admission.Request inside context: %v", err))
}

// Fallback machines are placeholders for InfraMachinePools that do not support MachinePool Machines. These have
// no bootstrap or infrastructure data and cannot be deleted by users. They instead exist to provide a consistent
// user experience for MachinePool Machines.
if _, isFallbackMachine := m.Labels[FallbackMachineLabel]; isFallbackMachine {
// Only allow the request if it is coming from the CAPI controller service account.
if req.UserInfo.Username != "system:serviceaccount:"+os.Getenv("POD_NAMESPACE")+":"+os.Getenv("POD_SERVICE_ACCOUNT") {
return nil, apierrors.NewBadRequest("this Machine is a placeholder for InfraMachinePools that do not support MachinePool Machines and cannot be deleted by users, scale down the MachinePool instead to delete")
}
}

return nil, nil
}

func (m *Machine) validate(old *Machine) error {
var allErrs field.ErrorList
specPath := field.NewPath("spec")
if m.Spec.Bootstrap.ConfigRef == nil && m.Spec.Bootstrap.DataSecretName == nil {
allErrs = append(
allErrs,
field.Required(
specPath.Child("bootstrap", "data"),
"expected either spec.bootstrap.dataSecretName or spec.bootstrap.configRef to be populated",
),
)
// MachinePool Machines don't have a bootstrap configRef, so don't require it. The bootstrap config is instead owned by the MachinePool.
if !isMachinePoolMachine(m) {
allErrs = append(
allErrs,
field.Required(
specPath.Child("bootstrap", "data"),
"expected either spec.bootstrap.dataSecretName or spec.bootstrap.configRef to be populated",
),
)
}
}

if m.Spec.Bootstrap.ConfigRef != nil && m.Spec.Bootstrap.ConfigRef.Namespace != m.Namespace {
Expand All @@ -114,15 +178,18 @@ func (m *Machine) validate(old *Machine) error {
)
}

if m.Spec.InfrastructureRef.Namespace != m.Namespace {
allErrs = append(
allErrs,
field.Invalid(
specPath.Child("infrastructureRef", "namespace"),
m.Spec.InfrastructureRef.Namespace,
"must match metadata.namespace",
),
)
// InfraRef can be empty for MachinePool Machines so skip the check in that case.
if !isMachinePoolMachine(m) {
if m.Spec.InfrastructureRef.Namespace != m.Namespace {
allErrs = append(
allErrs,
field.Invalid(
specPath.Child("infrastructureRef", "namespace"),
m.Spec.InfrastructureRef.Namespace,
"must match metadata.namespace",
),
)
}
}

if old != nil && old.Spec.ClusterName != m.Spec.ClusterName {
Expand All @@ -143,3 +210,17 @@ func (m *Machine) validate(old *Machine) error {
}
return apierrors.NewInvalid(GroupVersion.WithKind("Machine").GroupKind(), m.Name, allErrs)
}

func isMachinePoolMachine(m *Machine) bool {
if m.OwnerReferences == nil {
return false
}

for _, owner := range m.OwnerReferences {
if owner.Kind == "MachinePool" {
return true
}
}

return false
}
Loading

0 comments on commit e4c9c9f

Please sign in to comment.