Skip to content

Commit e4c9c9f

Browse files
committed
Implement MachinePool Machines in CAPI, CAPD, and clusterctl
1 parent 60b1426 commit e4c9c9f

34 files changed

+1735
-226
lines changed

api/v1beta1/common_types.go

+5
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ const (
6161
// update that disallows a pre-existing Cluster to be populated with Topology information and Class.
6262
ClusterTopologyUnsafeUpdateClassNameAnnotation = "unsafe.topology.cluster.x-k8s.io/disable-update-class-name-check"
6363

64+
// FallbackMachineLabel indicates that a Machine belongs to a MachinePool that does not support MachinePool Machines.
65+
// As such, these Machines exist to create a consistent user experience and will not have an infrastructure reference. The user will
66+
// also be prevented from deleting these Machines.
67+
FallbackMachineLabel = "machinepool.cluster.x-k8s.io/fallback-machine"
68+
6469
// ProviderNameLabel is the label set on components in the provider manifest.
6570
// This label allows to easily identify all the components belonging to a provider; the clusterctl
6671
// tool uses this label for implementing provider's lifecycle operations.

api/v1beta1/machine_types.go

+4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ const (
4343
// MachineDeploymentNameLabel is the label set on machines if they're controlled by MachineDeployment.
4444
MachineDeploymentNameLabel = "cluster.x-k8s.io/deployment-name"
4545

46+
// MachinePoolNameLabel is the label indicating the name of the MachinePool a Machine is controlled by.
47+
// Note: The value of this label may be a hash if the MachinePool name is longer than 63 characters.
48+
MachinePoolNameLabel = "cluster.x-k8s.io/pool-name"
49+
4650
// MachineControlPlaneNameLabel is the label set on machines if they're controlled by a ControlPlane.
4751
// Note: The value of this label may be a hash if the control plane name is longer than 63 characters.
4852
MachineControlPlaneNameLabel = "cluster.x-k8s.io/control-plane-name"

api/v1beta1/machine_webhook.go

+108-27
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ limitations under the License.
1717
package v1beta1
1818

1919
import (
20+
"context"
2021
"fmt"
22+
"os"
2123
"strings"
2224
"time"
2325

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

3739
func (m *Machine) SetupWebhookWithManager(mgr ctrl.Manager) error {
3840
return ctrl.NewWebhookManagedBy(mgr).
39-
For(m).
41+
For(&Machine{}).
42+
WithValidator(MachineValidator(mgr.GetScheme())).
4043
Complete()
4144
}
4245

43-
// +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
46+
// +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
4447
// +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
4548

46-
var _ webhook.Validator = &Machine{}
49+
var _ webhook.CustomValidator = &machineValidator{}
4750
var _ webhook.Defaulter = &Machine{}
4851

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

6063
if m.Spec.InfrastructureRef.Namespace == "" {
61-
m.Spec.InfrastructureRef.Namespace = m.Namespace
64+
// Don't autofill namespace for MachinePool Machines since the infraRef will be populated by the MachinePool controller.
65+
if !isMachinePoolMachine(m) {
66+
m.Spec.InfrastructureRef.Namespace = m.Namespace
67+
}
6268
}
6369

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

74-
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
75-
func (m *Machine) ValidateCreate() (admission.Warnings, error) {
80+
// MachineValidator creates a new CustomValidator for Machines.
81+
func MachineValidator(_ *runtime.Scheme) webhook.CustomValidator {
82+
return &machineValidator{}
83+
}
84+
85+
// machineValidator implements a defaulting webhook for Machine.
86+
type machineValidator struct{}
87+
88+
// // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
89+
// func (m *Machine) ValidateCreate() (admission.Warnings, error) {
90+
// return nil, m.validate(nil)
91+
// }
92+
93+
// // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
94+
// func (m *Machine) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
95+
// oldM, ok := old.(*Machine)
96+
// if !ok {
97+
// return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", old))
98+
// }
99+
// return nil, m.validate(oldM)
100+
// }
101+
102+
// // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
103+
//
104+
// func (m *Machine) ValidateDelete() (admission.Warnings, error) {
105+
// return nil, nil
106+
func (*machineValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
107+
m, ok := obj.(*Machine)
108+
if !ok {
109+
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", obj))
110+
}
111+
76112
return nil, m.validate(nil)
77113
}
78114

79115
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
80-
func (m *Machine) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
81-
oldM, ok := old.(*Machine)
116+
func (*machineValidator) ValidateUpdate(_ context.Context, oldObj runtime.Object, newObj runtime.Object) (admission.Warnings, error) {
117+
newM, ok := newObj.(*Machine)
118+
if !ok {
119+
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", newObj))
120+
}
121+
122+
oldM, ok := oldObj.(*Machine)
82123
if !ok {
83-
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", old))
124+
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", oldObj))
84125
}
85-
return nil, m.validate(oldM)
126+
return nil, newM.validate(oldM)
86127
}
87128

88129
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
89-
func (m *Machine) ValidateDelete() (admission.Warnings, error) {
130+
func (*machineValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
131+
m, ok := obj.(*Machine)
132+
if !ok {
133+
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", obj))
134+
}
135+
136+
req, err := admission.RequestFromContext(ctx)
137+
if err != nil {
138+
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a admission.Request inside context: %v", err))
139+
}
140+
141+
// Fallback machines are placeholders for InfraMachinePools that do not support MachinePool Machines. These have
142+
// no bootstrap or infrastructure data and cannot be deleted by users. They instead exist to provide a consistent
143+
// user experience for MachinePool Machines.
144+
if _, isFallbackMachine := m.Labels[FallbackMachineLabel]; isFallbackMachine {
145+
// Only allow the request if it is coming from the CAPI controller service account.
146+
if req.UserInfo.Username != "system:serviceaccount:"+os.Getenv("POD_NAMESPACE")+":"+os.Getenv("POD_SERVICE_ACCOUNT") {
147+
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")
148+
}
149+
}
150+
90151
return nil, nil
91152
}
92153

93154
func (m *Machine) validate(old *Machine) error {
94155
var allErrs field.ErrorList
95156
specPath := field.NewPath("spec")
96157
if m.Spec.Bootstrap.ConfigRef == nil && m.Spec.Bootstrap.DataSecretName == nil {
97-
allErrs = append(
98-
allErrs,
99-
field.Required(
100-
specPath.Child("bootstrap", "data"),
101-
"expected either spec.bootstrap.dataSecretName or spec.bootstrap.configRef to be populated",
102-
),
103-
)
158+
// MachinePool Machines don't have a bootstrap configRef, so don't require it. The bootstrap config is instead owned by the MachinePool.
159+
if !isMachinePoolMachine(m) {
160+
allErrs = append(
161+
allErrs,
162+
field.Required(
163+
specPath.Child("bootstrap", "data"),
164+
"expected either spec.bootstrap.dataSecretName or spec.bootstrap.configRef to be populated",
165+
),
166+
)
167+
}
104168
}
105169

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

117-
if m.Spec.InfrastructureRef.Namespace != m.Namespace {
118-
allErrs = append(
119-
allErrs,
120-
field.Invalid(
121-
specPath.Child("infrastructureRef", "namespace"),
122-
m.Spec.InfrastructureRef.Namespace,
123-
"must match metadata.namespace",
124-
),
125-
)
181+
// InfraRef can be empty for MachinePool Machines so skip the check in that case.
182+
if !isMachinePoolMachine(m) {
183+
if m.Spec.InfrastructureRef.Namespace != m.Namespace {
184+
allErrs = append(
185+
allErrs,
186+
field.Invalid(
187+
specPath.Child("infrastructureRef", "namespace"),
188+
m.Spec.InfrastructureRef.Namespace,
189+
"must match metadata.namespace",
190+
),
191+
)
192+
}
126193
}
127194

128195
if old != nil && old.Spec.ClusterName != m.Spec.ClusterName {
@@ -143,3 +210,17 @@ func (m *Machine) validate(old *Machine) error {
143210
}
144211
return apierrors.NewInvalid(GroupVersion.WithKind("Machine").GroupKind(), m.Name, allErrs)
145212
}
213+
214+
func isMachinePoolMachine(m *Machine) bool {
215+
if m.OwnerReferences == nil {
216+
return false
217+
}
218+
219+
for _, owner := range m.OwnerReferences {
220+
if owner.Kind == "MachinePool" {
221+
return true
222+
}
223+
}
224+
225+
return false
226+
}

0 commit comments

Comments
 (0)