sigs.k8s.io/cluster-api@v1.7.1/exp/internal/webhooks/machinepool.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  
    24  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    25  	"k8s.io/apimachinery/pkg/runtime"
    26  	"k8s.io/apimachinery/pkg/util/validation/field"
    27  	"k8s.io/utils/ptr"
    28  	ctrl "sigs.k8s.io/controller-runtime"
    29  	"sigs.k8s.io/controller-runtime/pkg/webhook"
    30  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    31  
    32  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    33  	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
    34  	"sigs.k8s.io/cluster-api/feature"
    35  	"sigs.k8s.io/cluster-api/util/version"
    36  )
    37  
    38  func (webhook *MachinePool) SetupWebhookWithManager(mgr ctrl.Manager) error {
    39  	return ctrl.NewWebhookManagedBy(mgr).
    40  		For(&expv1.MachinePool{}).
    41  		WithDefaulter(webhook).
    42  		WithValidator(webhook).
    43  		Complete()
    44  }
    45  
    46  // +kubebuilder:webhook:verbs=create;update,path=/validate-cluster-x-k8s-io-v1beta1-machinepool,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=machinepools,versions=v1beta1,name=validation.machinepool.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    47  // +kubebuilder:webhook:verbs=create;update,path=/mutate-cluster-x-k8s-io-v1beta1-machinepool,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=cluster.x-k8s.io,resources=machinepools,versions=v1beta1,name=default.machinepool.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    48  
    49  // MachinePool implements a validation and defaulting webhook for MachinePool.
    50  type MachinePool struct{}
    51  
    52  var _ webhook.CustomValidator = &MachinePool{}
    53  var _ webhook.CustomDefaulter = &MachinePool{}
    54  
    55  // Default implements webhook.Defaulter so a webhook will be registered for the type.
    56  func (webhook *MachinePool) Default(_ context.Context, obj runtime.Object) error {
    57  	m, ok := obj.(*expv1.MachinePool)
    58  	if !ok {
    59  		return apierrors.NewBadRequest(fmt.Sprintf("expected a MachinePool but got a %T", obj))
    60  	}
    61  
    62  	if m.Labels == nil {
    63  		m.Labels = make(map[string]string)
    64  	}
    65  	m.Labels[clusterv1.ClusterNameLabel] = m.Spec.ClusterName
    66  
    67  	if m.Spec.Replicas == nil {
    68  		m.Spec.Replicas = ptr.To[int32](1)
    69  	}
    70  
    71  	if m.Spec.MinReadySeconds == nil {
    72  		m.Spec.MinReadySeconds = ptr.To[int32](0)
    73  	}
    74  
    75  	if m.Spec.Template.Spec.Bootstrap.ConfigRef != nil && m.Spec.Template.Spec.Bootstrap.ConfigRef.Namespace == "" {
    76  		m.Spec.Template.Spec.Bootstrap.ConfigRef.Namespace = m.Namespace
    77  	}
    78  
    79  	if m.Spec.Template.Spec.InfrastructureRef.Namespace == "" {
    80  		m.Spec.Template.Spec.InfrastructureRef.Namespace = m.Namespace
    81  	}
    82  
    83  	// tolerate version strings without a "v" prefix: prepend it if it's not there.
    84  	if m.Spec.Template.Spec.Version != nil && !strings.HasPrefix(*m.Spec.Template.Spec.Version, "v") {
    85  		normalizedVersion := "v" + *m.Spec.Template.Spec.Version
    86  		m.Spec.Template.Spec.Version = &normalizedVersion
    87  	}
    88  	return nil
    89  }
    90  
    91  // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
    92  func (webhook *MachinePool) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
    93  	mp, ok := obj.(*expv1.MachinePool)
    94  	if !ok {
    95  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachinePool but got a %T", obj))
    96  	}
    97  
    98  	return nil, webhook.validate(nil, mp)
    99  }
   100  
   101  // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
   102  func (webhook *MachinePool) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
   103  	oldMP, ok := oldObj.(*expv1.MachinePool)
   104  	if !ok {
   105  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachinePool but got a %T", oldObj))
   106  	}
   107  	newMP, ok := newObj.(*expv1.MachinePool)
   108  	if !ok {
   109  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachinePool but got a %T", newObj))
   110  	}
   111  	return nil, webhook.validate(oldMP, newMP)
   112  }
   113  
   114  // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
   115  func (webhook *MachinePool) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
   116  	mp, ok := obj.(*expv1.MachinePool)
   117  	if !ok {
   118  		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a MachinePool but got a %T", obj))
   119  	}
   120  
   121  	return nil, webhook.validate(nil, mp)
   122  }
   123  
   124  func (webhook *MachinePool) validate(oldObj, newObj *expv1.MachinePool) error {
   125  	// NOTE: MachinePool is behind MachinePool feature gate flag; the web hook
   126  	// must prevent creating newObj objects when the feature flag is disabled.
   127  	specPath := field.NewPath("spec")
   128  	if !feature.Gates.Enabled(feature.MachinePool) {
   129  		return field.Forbidden(
   130  			specPath,
   131  			"can be set only if the MachinePool feature flag is enabled",
   132  		)
   133  	}
   134  	var allErrs field.ErrorList
   135  	if newObj.Spec.Template.Spec.Bootstrap.ConfigRef == nil && newObj.Spec.Template.Spec.Bootstrap.DataSecretName == nil {
   136  		allErrs = append(
   137  			allErrs,
   138  			field.Required(
   139  				specPath.Child("template", "spec", "bootstrap", "data"),
   140  				"expected either spec.bootstrap.dataSecretName or spec.bootstrap.configRef to be populated",
   141  			),
   142  		)
   143  	}
   144  
   145  	if newObj.Spec.Template.Spec.Bootstrap.ConfigRef != nil && newObj.Spec.Template.Spec.Bootstrap.ConfigRef.Namespace != newObj.Namespace {
   146  		allErrs = append(
   147  			allErrs,
   148  			field.Invalid(
   149  				specPath.Child("template", "spec", "bootstrap", "configRef", "namespace"),
   150  				newObj.Spec.Template.Spec.Bootstrap.ConfigRef.Namespace,
   151  				"must match metadata.namespace",
   152  			),
   153  		)
   154  	}
   155  
   156  	if newObj.Spec.Template.Spec.InfrastructureRef.Namespace != newObj.Namespace {
   157  		allErrs = append(
   158  			allErrs,
   159  			field.Invalid(
   160  				specPath.Child("infrastructureRef", "namespace"),
   161  				newObj.Spec.Template.Spec.InfrastructureRef.Namespace,
   162  				"must match metadata.namespace",
   163  			),
   164  		)
   165  	}
   166  
   167  	if oldObj != nil && oldObj.Spec.ClusterName != newObj.Spec.ClusterName {
   168  		allErrs = append(
   169  			allErrs,
   170  			field.Forbidden(
   171  				specPath.Child("clusterName"),
   172  				"field is immutable"),
   173  		)
   174  	}
   175  
   176  	if newObj.Spec.Template.Spec.Version != nil {
   177  		if !version.KubeSemver.MatchString(*newObj.Spec.Template.Spec.Version) {
   178  			allErrs = append(allErrs, field.Invalid(specPath.Child("template", "spec", "version"), *newObj.Spec.Template.Spec.Version, "must be a valid semantic version"))
   179  		}
   180  	}
   181  
   182  	// Validate the metadata of the MachinePool template.
   183  	allErrs = append(allErrs, newObj.Spec.Template.ObjectMeta.Validate(specPath.Child("template", "metadata"))...)
   184  
   185  	if len(allErrs) == 0 {
   186  		return nil
   187  	}
   188  	return apierrors.NewInvalid(clusterv1.GroupVersion.WithKind("MachinePool").GroupKind(), newObj.Name, allErrs)
   189  }