Skip to content

Commit 4cea0d6

Browse files
committed
Implement MachinePool Machines in CAPI, CAPD, and clusterctl
1 parent fc799af commit 4cea0d6

34 files changed

+1787
-284
lines changed

api/v1beta1/common_types.go

Lines changed: 14 additions & 0 deletions
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.
@@ -151,6 +156,15 @@ const (
151156
// VariableDefinitionFromInline indicates a patch or variable was defined in the `.spec` of a ClusterClass
152157
// rather than from an external patch extension.
153158
VariableDefinitionFromInline = "inline"
159+
160+
// ServiceAccountGroupName is the group name for service accounts.
161+
ServiceAccountGroupName = "system:serviceaccounts"
162+
163+
// CAPIServiceAccountGroupName is the group name for service accounts in the capi-system namespace.
164+
CAPIServiceAccountGroupName = "system:serviceaccounts:capi-system"
165+
166+
// CAPIServiceAccountUsername is the name for the service account used by the CAPI core controller manager.
167+
CAPIServiceAccountUsername = "system:serviceaccount:capi-system:capi-manager"
154168
)
155169

156170
// NodeUninitializedTaint can be added to Nodes at creation by the bootstrap provider, e.g. the

api/v1beta1/machine_types.go

Lines changed: 4 additions & 0 deletions
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

Lines changed: 110 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package v1beta1
1818

1919
import (
20+
"context"
2021
"fmt"
2122
"strings"
2223
"time"
@@ -27,6 +28,7 @@ import (
2728
"k8s.io/apimachinery/pkg/util/validation/field"
2829
ctrl "sigs.k8s.io/controller-runtime"
2930
"sigs.k8s.io/controller-runtime/pkg/webhook"
31+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
3032

3133
"sigs.k8s.io/cluster-api/util/version"
3234
)
@@ -35,14 +37,15 @@ const defaultNodeDeletionTimeout = 10 * time.Second
3537

3638
func (m *Machine) SetupWebhookWithManager(mgr ctrl.Manager) error {
3739
return ctrl.NewWebhookManagedBy(mgr).
38-
For(m).
40+
For(&Machine{}).
41+
WithValidator(MachineValidator(mgr.GetScheme())).
3942
Complete()
4043
}
4144

42-
// +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
43-
// +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
45+
// +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
46+
// +kubebuilder:webhook:verbs=create;update;delete,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
4447

45-
var _ webhook.Validator = &Machine{}
48+
var _ webhook.CustomValidator = &machineValidator{}
4649
var _ webhook.Defaulter = &Machine{}
4750

4851
// Default implements webhook.Defaulter so a webhook will be registered for the type.
@@ -57,7 +60,10 @@ func (m *Machine) Default() {
5760
}
5861

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

6369
if m.Spec.Version != nil && !strings.HasPrefix(*m.Spec.Version, "v") {
@@ -70,36 +76,96 @@ func (m *Machine) Default() {
7076
}
7177
}
7278

79+
// MachineValidator creates a new CustomValidator for Machines.
80+
func MachineValidator(scheme *runtime.Scheme) webhook.CustomValidator {
81+
// Note: The error return parameter is always nil and will be dropped with the next CR release.
82+
decoder, _ := admission.NewDecoder(scheme)
83+
return &machineValidator{
84+
decoder: decoder,
85+
}
86+
}
87+
88+
// machineValidator implements a defaulting webhook for Machine.
89+
type machineValidator struct {
90+
decoder *admission.Decoder
91+
}
92+
7393
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
74-
func (m *Machine) ValidateCreate() error {
94+
func (*machineValidator) ValidateCreate(_ context.Context, obj runtime.Object) error {
95+
m, ok := obj.(*Machine)
96+
if !ok {
97+
return apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", obj))
98+
}
99+
75100
return m.validate(nil)
76101
}
77102

78103
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
79-
func (m *Machine) ValidateUpdate(old runtime.Object) error {
80-
oldM, ok := old.(*Machine)
104+
func (*machineValidator) ValidateUpdate(_ context.Context, oldObj runtime.Object, newObj runtime.Object) error {
105+
newM, ok := newObj.(*Machine)
106+
if !ok {
107+
return apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", newObj))
108+
}
109+
110+
oldM, ok := oldObj.(*Machine)
81111
if !ok {
82-
return apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", old))
112+
return apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", oldObj))
83113
}
84-
return m.validate(oldM)
114+
return newM.validate(oldM)
85115
}
86116

87117
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
88-
func (m *Machine) ValidateDelete() error {
118+
func (*machineValidator) ValidateDelete(ctx context.Context, obj runtime.Object) error {
119+
m, ok := obj.(*Machine)
120+
if !ok {
121+
return apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", obj))
122+
}
123+
124+
req, err := admission.RequestFromContext(ctx)
125+
if err != nil {
126+
return apierrors.NewBadRequest(fmt.Sprintf("expected a admission.Request inside context: %v", err))
127+
}
128+
129+
// Fallback machines are placeholders for InfraMachinePools that do not support MachinePool Machines. These have
130+
// no bootstrap or infrastructure data and cannot be deleted by users. They instead exist to provide a consistent
131+
// user experience for MachinePool Machines.
132+
if _, isFallbackMachine := m.Labels[FallbackMachineLabel]; isFallbackMachine {
133+
if !isControllerRequest(req) {
134+
return 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")
135+
}
136+
}
137+
89138
return nil
90139
}
91140

141+
func isControllerRequest(req admission.Request) bool {
142+
if req.UserInfo.Username == CAPIServiceAccountUsername {
143+
return true
144+
}
145+
146+
for _, group := range req.UserInfo.Groups {
147+
if group == ServiceAccountGroupName || group == CAPIServiceAccountGroupName {
148+
return true
149+
}
150+
}
151+
152+
return false
153+
}
154+
92155
func (m *Machine) validate(old *Machine) error {
93156
var allErrs field.ErrorList
94157
specPath := field.NewPath("spec")
95158
if m.Spec.Bootstrap.ConfigRef == nil && m.Spec.Bootstrap.DataSecretName == nil {
96-
allErrs = append(
97-
allErrs,
98-
field.Required(
99-
specPath.Child("bootstrap", "data"),
100-
"expected either spec.bootstrap.dataSecretName or spec.bootstrap.configRef to be populated",
101-
),
102-
)
159+
// MachinePool Machines don't have a bootstrap configRef, so don't require it. The bootstrap config is instead owned by the MachinePool.
160+
if !isMachinePoolMachine(m) {
161+
allErrs = append(
162+
allErrs,
163+
field.Required(
164+
specPath.Child("bootstrap", "data"),
165+
"expected either spec.bootstrap.dataSecretName or spec.bootstrap.configRef to be populated",
166+
),
167+
)
168+
}
103169
}
104170

105171
if m.Spec.Bootstrap.ConfigRef != nil && m.Spec.Bootstrap.ConfigRef.Namespace != m.Namespace {
@@ -113,15 +179,18 @@ func (m *Machine) validate(old *Machine) error {
113179
)
114180
}
115181

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

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

0 commit comments

Comments
 (0)