sigs.k8s.io/cluster-api@v1.7.1/internal/webhooks/machine.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package webhooks
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  	"time"
    24  
    25  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/util/validation/field"
    29  	ctrl "sigs.k8s.io/controller-runtime"
    30  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    31  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    32  
    33  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    34  	"sigs.k8s.io/cluster-api/util/labels"
    35  	"sigs.k8s.io/cluster-api/util/version"
    36  )
    37  
    38  const defaultNodeDeletionTimeout = 10 * time.Second
    39  
    40  func (webhook *Machine) SetupWebhookWithManager(mgr ctrl.Manager) error {
    41  	return ctrl.NewWebhookManagedBy(mgr).
    42  		For(&clusterv1.Machine{}).
    43  		WithDefaulter(webhook).
    44  		WithValidator(webhook).
    45  		Complete()
    46  }
    47  
    48  // +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
    49  // +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
    50  
    51  // Machine implements a validation and defaulting webhook for Machine.
    52  type Machine struct{}
    53  
    54  var _ webhook.CustomValidator = &Machine{}
    55  var _ webhook.CustomDefaulter = &Machine{}
    56  
    57  // Default implements webhook.Defaulter so a webhook will be registered for the type.
    58  func (webhook *Machine) Default(_ context.Context, obj runtime.Object) error {
    59  	m, ok := obj.(*clusterv1.Machine)
    60  	if !ok {
    61  		return apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", obj))
    62  	}
    63  
    64  	if m.Labels == nil {
    65  		m.Labels = make(map[string]string)
    66  	}
    67  	m.Labels[clusterv1.ClusterNameLabel] = m.Spec.ClusterName
    68  
    69  	if m.Spec.Bootstrap.ConfigRef != nil && m.Spec.Bootstrap.ConfigRef.Namespace == "" {
    70  		m.Spec.Bootstrap.ConfigRef.Namespace = m.Namespace
    71  	}
    72  
    73  	if m.Spec.InfrastructureRef.Namespace == "" {
    74  		m.Spec.InfrastructureRef.Namespace = m.Namespace
    75  	}
    76  
    77  	if m.Spec.Version != nil && !strings.HasPrefix(*m.Spec.Version, "v") {
    78  		normalizedVersion := "v" + *m.Spec.Version
    79  		m.Spec.Version = &normalizedVersion
    80  	}
    81  
    82  	if m.Spec.NodeDeletionTimeout == nil {
    83  		m.Spec.NodeDeletionTimeout = &metav1.Duration{Duration: defaultNodeDeletionTimeout}
    84  	}
    85  
    86  	return nil
    87  }
    88  
    89  // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type.
    90  func (webhook *Machine) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
    91  	m, ok := obj.(*clusterv1.Machine)
    92  	if !ok {
    93  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", obj))
    94  	}
    95  
    96  	return nil, webhook.validate(nil, m)
    97  }
    98  
    99  // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
   100  func (webhook *Machine) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
   101  	oldM, ok := oldObj.(*clusterv1.Machine)
   102  	if !ok {
   103  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", oldObj))
   104  	}
   105  
   106  	newM, ok := newObj.(*clusterv1.Machine)
   107  	if !ok {
   108  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a Machine but got a %T", newObj))
   109  	}
   110  
   111  	return nil, webhook.validate(oldM, newM)
   112  }
   113  
   114  // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type.
   115  func (webhook *Machine) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
   116  	return nil, nil
   117  }
   118  
   119  func (webhook *Machine) validate(oldM, newM *clusterv1.Machine) error {
   120  	var allErrs field.ErrorList
   121  	specPath := field.NewPath("spec")
   122  	if newM.Spec.Bootstrap.ConfigRef == nil && newM.Spec.Bootstrap.DataSecretName == nil {
   123  		// MachinePool Machines don't have a bootstrap configRef, so don't require it. The bootstrap config is instead owned by the MachinePool.
   124  		if !labels.IsMachinePoolOwned(newM) {
   125  			allErrs = append(
   126  				allErrs,
   127  				field.Required(
   128  					specPath.Child("bootstrap", "data"),
   129  					"expected either spec.bootstrap.dataSecretName or spec.bootstrap.configRef to be populated",
   130  				),
   131  			)
   132  		}
   133  	}
   134  
   135  	if newM.Spec.Bootstrap.ConfigRef != nil && newM.Spec.Bootstrap.ConfigRef.Namespace != newM.Namespace {
   136  		allErrs = append(
   137  			allErrs,
   138  			field.Invalid(
   139  				specPath.Child("bootstrap", "configRef", "namespace"),
   140  				newM.Spec.Bootstrap.ConfigRef.Namespace,
   141  				"must match metadata.namespace",
   142  			),
   143  		)
   144  	}
   145  
   146  	if newM.Spec.InfrastructureRef.Namespace != newM.Namespace {
   147  		allErrs = append(
   148  			allErrs,
   149  			field.Invalid(
   150  				specPath.Child("infrastructureRef", "namespace"),
   151  				newM.Spec.InfrastructureRef.Namespace,
   152  				"must match metadata.namespace",
   153  			),
   154  		)
   155  	}
   156  
   157  	if oldM != nil && oldM.Spec.ClusterName != newM.Spec.ClusterName {
   158  		allErrs = append(
   159  			allErrs,
   160  			field.Forbidden(specPath.Child("clusterName"), "field is immutable"),
   161  		)
   162  	}
   163  
   164  	if newM.Spec.Version != nil {
   165  		if !version.KubeSemver.MatchString(*newM.Spec.Version) {
   166  			allErrs = append(allErrs, field.Invalid(specPath.Child("version"), *newM.Spec.Version, "must be a valid semantic version"))
   167  		}
   168  	}
   169  
   170  	if len(allErrs) == 0 {
   171  		return nil
   172  	}
   173  	return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("Machine").GroupKind(), newM.Name, allErrs)
   174  }