sigs.k8s.io/cluster-api-provider-azure@v1.17.0/exp/api/v1beta1/azuremachinepool_webhook.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 v1beta1
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"reflect"
    23  
    24  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
    25  	"github.com/blang/semver"
    26  	"github.com/pkg/errors"
    27  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    30  	"k8s.io/apimachinery/pkg/util/intstr"
    31  	"k8s.io/apimachinery/pkg/util/validation/field"
    32  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    33  	"sigs.k8s.io/cluster-api-provider-azure/feature"
    34  	azureutil "sigs.k8s.io/cluster-api-provider-azure/util/azure"
    35  	capifeature "sigs.k8s.io/cluster-api/feature"
    36  	ctrl "sigs.k8s.io/controller-runtime"
    37  	"sigs.k8s.io/controller-runtime/pkg/client"
    38  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    39  )
    40  
    41  // SetupAzureMachinePoolWebhookWithManager sets up and registers the webhook with the manager.
    42  func SetupAzureMachinePoolWebhookWithManager(mgr ctrl.Manager) error {
    43  	ampw := &azureMachinePoolWebhook{Client: mgr.GetClient()}
    44  	return ctrl.NewWebhookManagedBy(mgr).
    45  		For(&AzureMachinePool{}).
    46  		WithDefaulter(ampw).
    47  		WithValidator(ampw).
    48  		Complete()
    49  }
    50  
    51  // +kubebuilder:webhook:path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-azuremachinepool,mutating=true,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=azuremachinepools,verbs=create;update,versions=v1beta1,name=default.azuremachinepool.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    52  
    53  // azureMachinePoolWebhook implements a validating and defaulting webhook for AzureMachinePool.
    54  type azureMachinePoolWebhook struct {
    55  	Client client.Client
    56  }
    57  
    58  // Default implements webhook.Defaulter so a webhook will be registered for the type.
    59  func (ampw *azureMachinePoolWebhook) Default(ctx context.Context, obj runtime.Object) error {
    60  	amp, ok := obj.(*AzureMachinePool)
    61  	if !ok {
    62  		return apierrors.NewBadRequest("expected an AzureMachinePool")
    63  	}
    64  	return amp.SetDefaults(ampw.Client)
    65  }
    66  
    67  // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-azuremachinepool,mutating=false,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=azuremachinepools,versions=v1beta1,name=validation.azuremachinepool.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    68  
    69  // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
    70  func (ampw *azureMachinePoolWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
    71  	amp, ok := obj.(*AzureMachinePool)
    72  	if !ok {
    73  		return nil, apierrors.NewBadRequest("expected an AzureMachinePool")
    74  	}
    75  	// NOTE: AzureMachinePool is behind MachinePool feature gate flag; the webhook
    76  	// must prevent creating new objects in case the feature flag is disabled.
    77  	if !feature.Gates.Enabled(capifeature.MachinePool) {
    78  		return nil, field.Forbidden(
    79  			field.NewPath("spec"),
    80  			"can be set only if the MachinePool feature flag is enabled",
    81  		)
    82  	}
    83  	return nil, amp.Validate(nil, ampw.Client)
    84  }
    85  
    86  // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
    87  func (ampw *azureMachinePoolWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
    88  	amp, ok := newObj.(*AzureMachinePool)
    89  	if !ok {
    90  		return nil, apierrors.NewBadRequest("expected an AzureMachinePool")
    91  	}
    92  	return nil, amp.Validate(oldObj, ampw.Client)
    93  }
    94  
    95  // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
    96  func (ampw *azureMachinePoolWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
    97  	return nil, nil
    98  }
    99  
   100  // Validate the Azure Machine Pool and return an aggregate error.
   101  func (amp *AzureMachinePool) Validate(old runtime.Object, client client.Client) error {
   102  	validators := []func() error{
   103  		amp.ValidateImage,
   104  		amp.ValidateTerminateNotificationTimeout,
   105  		amp.ValidateSSHKey,
   106  		amp.ValidateUserAssignedIdentity,
   107  		amp.ValidateDiagnostics,
   108  		amp.ValidateOrchestrationMode(client),
   109  		amp.ValidateStrategy(),
   110  		amp.ValidateSystemAssignedIdentity(old),
   111  		amp.ValidateSystemAssignedIdentityRole,
   112  		amp.ValidateNetwork,
   113  		amp.ValidateOSDisk,
   114  	}
   115  
   116  	var errs []error
   117  	for _, validator := range validators {
   118  		if err := validator(); err != nil {
   119  			errs = append(errs, err)
   120  		}
   121  	}
   122  
   123  	return kerrors.NewAggregate(errs)
   124  }
   125  
   126  // ValidateNetwork of an AzureMachinePool.
   127  func (amp *AzureMachinePool) ValidateNetwork() error {
   128  	if (amp.Spec.Template.NetworkInterfaces != nil) && len(amp.Spec.Template.NetworkInterfaces) > 0 && amp.Spec.Template.SubnetName != "" {
   129  		return errors.New("cannot set both NetworkInterfaces and machine SubnetName")
   130  	}
   131  	return nil
   132  }
   133  
   134  // ValidateOSDisk of an AzureMachinePool.
   135  func (amp *AzureMachinePool) ValidateOSDisk() error {
   136  	if errs := infrav1.ValidateOSDisk(amp.Spec.Template.OSDisk, field.NewPath("osDisk")); len(errs) > 0 {
   137  		return errs.ToAggregate()
   138  	}
   139  	return nil
   140  }
   141  
   142  // ValidateImage of an AzureMachinePool.
   143  func (amp *AzureMachinePool) ValidateImage() error {
   144  	if amp.Spec.Template.Image != nil {
   145  		image := amp.Spec.Template.Image
   146  		if errs := infrav1.ValidateImage(image, field.NewPath("image")); len(errs) > 0 {
   147  			return errs.ToAggregate()
   148  		}
   149  	}
   150  
   151  	return nil
   152  }
   153  
   154  // ValidateTerminateNotificationTimeout termination notification timeout to be between 5 and 15.
   155  func (amp *AzureMachinePool) ValidateTerminateNotificationTimeout() error {
   156  	if amp.Spec.Template.TerminateNotificationTimeout == nil {
   157  		return nil
   158  	}
   159  	if *amp.Spec.Template.TerminateNotificationTimeout < 5 {
   160  		return errors.New("minimum timeout 5 is allowed for TerminateNotificationTimeout")
   161  	}
   162  
   163  	if *amp.Spec.Template.TerminateNotificationTimeout > 15 {
   164  		return errors.New("maximum timeout 15 is allowed for TerminateNotificationTimeout")
   165  	}
   166  
   167  	return nil
   168  }
   169  
   170  // ValidateSSHKey validates an SSHKey.
   171  func (amp *AzureMachinePool) ValidateSSHKey() error {
   172  	if amp.Spec.Template.SSHPublicKey != "" {
   173  		sshKey := amp.Spec.Template.SSHPublicKey
   174  		if errs := infrav1.ValidateSSHKey(sshKey, field.NewPath("sshKey")); len(errs) > 0 {
   175  			agg := kerrors.NewAggregate(errs.ToAggregate().Errors())
   176  			return agg
   177  		}
   178  	}
   179  
   180  	return nil
   181  }
   182  
   183  // ValidateUserAssignedIdentity validates the user-assigned identities list.
   184  func (amp *AzureMachinePool) ValidateUserAssignedIdentity() error {
   185  	fldPath := field.NewPath("userAssignedIdentities")
   186  	if errs := infrav1.ValidateUserAssignedIdentity(amp.Spec.Identity, amp.Spec.UserAssignedIdentities, fldPath); len(errs) > 0 {
   187  		return kerrors.NewAggregate(errs.ToAggregate().Errors())
   188  	}
   189  
   190  	return nil
   191  }
   192  
   193  // ValidateStrategy validates the strategy.
   194  func (amp *AzureMachinePool) ValidateStrategy() func() error {
   195  	return func() error {
   196  		if amp.Spec.Strategy.Type == RollingUpdateAzureMachinePoolDeploymentStrategyType && amp.Spec.Strategy.RollingUpdate != nil {
   197  			rollingUpdateStrategy := amp.Spec.Strategy.RollingUpdate
   198  			maxSurge := rollingUpdateStrategy.MaxSurge
   199  			maxUnavailable := rollingUpdateStrategy.MaxUnavailable
   200  			if maxSurge.Type == intstr.Int && maxSurge.IntVal == 0 &&
   201  				maxUnavailable.Type == intstr.Int && maxUnavailable.IntVal == 0 {
   202  				return errors.New("rolling update strategy MaxUnavailable must not be 0 if MaxSurge is 0")
   203  			}
   204  		}
   205  
   206  		return nil
   207  	}
   208  }
   209  
   210  // ValidateSystemAssignedIdentity validates system-assigned identity role.
   211  func (amp *AzureMachinePool) ValidateSystemAssignedIdentity(old runtime.Object) func() error {
   212  	return func() error {
   213  		var oldRole string
   214  		if old != nil {
   215  			oldMachinePool, ok := old.(*AzureMachinePool)
   216  			if !ok {
   217  				return fmt.Errorf("unexpected type for old azure machine pool object. Expected: %q, Got: %q",
   218  					"AzureMachinePool", reflect.TypeOf(old))
   219  			}
   220  			if amp.Spec.SystemAssignedIdentityRole != nil {
   221  				oldRole = oldMachinePool.Spec.SystemAssignedIdentityRole.Name
   222  			}
   223  		}
   224  
   225  		roleAssignmentName := ""
   226  		if amp.Spec.SystemAssignedIdentityRole != nil {
   227  			roleAssignmentName = amp.Spec.SystemAssignedIdentityRole.Name
   228  		}
   229  
   230  		fldPath := field.NewPath("roleAssignmentName")
   231  		if errs := infrav1.ValidateSystemAssignedIdentity(amp.Spec.Identity, oldRole, roleAssignmentName, fldPath); len(errs) > 0 {
   232  			return kerrors.NewAggregate(errs.ToAggregate().Errors())
   233  		}
   234  
   235  		return nil
   236  	}
   237  }
   238  
   239  // ValidateSystemAssignedIdentityRole validates the scope and roleDefinitionID for the system-assigned identity.
   240  func (amp *AzureMachinePool) ValidateSystemAssignedIdentityRole() error {
   241  	var allErrs field.ErrorList
   242  	if amp.Spec.RoleAssignmentName != "" && amp.Spec.SystemAssignedIdentityRole != nil && amp.Spec.SystemAssignedIdentityRole.Name != "" {
   243  		allErrs = append(allErrs, field.Invalid(field.NewPath("systemAssignedIdentityRole"), amp.Spec.SystemAssignedIdentityRole.Name, "cannot set both roleAssignmentName and systemAssignedIdentityRole.name"))
   244  	}
   245  	if amp.Spec.Identity == infrav1.VMIdentitySystemAssigned {
   246  		if amp.Spec.SystemAssignedIdentityRole.DefinitionID == "" {
   247  			allErrs = append(allErrs, field.Invalid(field.NewPath("systemAssignedIdentityRole", "definitionID"), amp.Spec.SystemAssignedIdentityRole.DefinitionID, "the roleDefinitionID field cannot be empty"))
   248  		}
   249  		if amp.Spec.SystemAssignedIdentityRole.Scope == "" {
   250  			allErrs = append(allErrs, field.Invalid(field.NewPath("systemAssignedIdentityRole", "scope"), amp.Spec.SystemAssignedIdentityRole.Scope, "the scope field cannot be empty"))
   251  		}
   252  	}
   253  	if amp.Spec.Identity != infrav1.VMIdentitySystemAssigned && amp.Spec.SystemAssignedIdentityRole != nil {
   254  		allErrs = append(allErrs, field.Invalid(field.NewPath("systemAssignedIdentityRole"), amp.Spec.SystemAssignedIdentityRole, "systemAssignedIdentityRole can only be set when identity is set to 'SystemAssigned'"))
   255  	}
   256  
   257  	if len(allErrs) > 0 {
   258  		return kerrors.NewAggregate(allErrs.ToAggregate().Errors())
   259  	}
   260  
   261  	return nil
   262  }
   263  
   264  // ValidateDiagnostics validates the Diagnostic spec.
   265  func (amp *AzureMachinePool) ValidateDiagnostics() error {
   266  	var allErrs field.ErrorList
   267  	fieldPath := field.NewPath("diagnostics")
   268  
   269  	diagnostics := amp.Spec.Template.Diagnostics
   270  
   271  	if diagnostics != nil && diagnostics.Boot != nil {
   272  		switch diagnostics.Boot.StorageAccountType {
   273  		case infrav1.UserManagedDiagnosticsStorage:
   274  			if diagnostics.Boot.UserManaged == nil {
   275  				allErrs = append(allErrs, field.Required(fieldPath.Child("UserManaged"),
   276  					fmt.Sprintf("userManaged must be specified when storageAccountType is '%s'", infrav1.UserManagedDiagnosticsStorage)))
   277  			} else if diagnostics.Boot.UserManaged.StorageAccountURI == "" {
   278  				allErrs = append(allErrs, field.Required(fieldPath.Child("StorageAccountURI"),
   279  					fmt.Sprintf("StorageAccountURI cannot be empty when storageAccountType is '%s'", infrav1.UserManagedDiagnosticsStorage)))
   280  			}
   281  		case infrav1.ManagedDiagnosticsStorage:
   282  			if diagnostics.Boot.UserManaged != nil &&
   283  				diagnostics.Boot.UserManaged.StorageAccountURI != "" {
   284  				allErrs = append(allErrs, field.Invalid(fieldPath.Child("StorageAccountURI"), diagnostics.Boot.UserManaged.StorageAccountURI,
   285  					fmt.Sprintf("StorageAccountURI cannot be set when storageAccountType is '%s'",
   286  						infrav1.ManagedDiagnosticsStorage)))
   287  			}
   288  		case infrav1.DisabledDiagnosticsStorage:
   289  			if diagnostics.Boot.UserManaged != nil &&
   290  				diagnostics.Boot.UserManaged.StorageAccountURI != "" {
   291  				allErrs = append(allErrs, field.Invalid(fieldPath.Child("StorageAccountURI"), diagnostics.Boot.UserManaged.StorageAccountURI,
   292  					fmt.Sprintf("StorageAccountURI cannot be set when storageAccountType is '%s'",
   293  						infrav1.ManagedDiagnosticsStorage)))
   294  			}
   295  		}
   296  	}
   297  
   298  	if len(allErrs) > 0 {
   299  		return kerrors.NewAggregate(allErrs.ToAggregate().Errors())
   300  	}
   301  
   302  	return nil
   303  }
   304  
   305  // ValidateOrchestrationMode validates requirements for the VMSS orchestration mode.
   306  func (amp *AzureMachinePool) ValidateOrchestrationMode(c client.Client) func() error {
   307  	return func() error {
   308  		// Only Flexible orchestration mode requires validation.
   309  		if amp.Spec.OrchestrationMode == infrav1.OrchestrationModeType(armcompute.OrchestrationModeFlexible) {
   310  			parent, err := azureutil.FindParentMachinePoolWithRetry(amp.Name, c, 5)
   311  			if err != nil {
   312  				return errors.Wrap(err, "failed to find parent MachinePool")
   313  			}
   314  			// Kubernetes must be >= 1.26.0 for cloud-provider-azure Helm chart support.
   315  			if parent.Spec.Template.Spec.Version == nil {
   316  				return errors.New("could not find Kubernetes version in MachinePool")
   317  			}
   318  			k8sVersion, err := semver.ParseTolerant(*parent.Spec.Template.Spec.Version)
   319  			if err != nil {
   320  				return errors.Wrap(err, "failed to parse Kubernetes version")
   321  			}
   322  			if k8sVersion.LT(semver.MustParse("1.26.0")) {
   323  				return errors.New(fmt.Sprintf("specified Kubernetes version %s must be >= 1.26.0 for Flexible orchestration mode", k8sVersion))
   324  			}
   325  		}
   326  
   327  		return nil
   328  	}
   329  }