sigs.k8s.io/cluster-api-provider-azure@v1.14.3/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  	}
   114  
   115  	var errs []error
   116  	for _, validator := range validators {
   117  		if err := validator(); err != nil {
   118  			errs = append(errs, err)
   119  		}
   120  	}
   121  
   122  	return kerrors.NewAggregate(errs)
   123  }
   124  
   125  // ValidateNetwork of an AzureMachinePool.
   126  func (amp *AzureMachinePool) ValidateNetwork() error {
   127  	if (amp.Spec.Template.NetworkInterfaces != nil) && len(amp.Spec.Template.NetworkInterfaces) > 0 && amp.Spec.Template.SubnetName != "" {
   128  		return errors.New("cannot set both NetworkInterfaces and machine SubnetName")
   129  	}
   130  	return nil
   131  }
   132  
   133  // ValidateImage of an AzureMachinePool.
   134  func (amp *AzureMachinePool) ValidateImage() error {
   135  	if amp.Spec.Template.Image != nil {
   136  		image := amp.Spec.Template.Image
   137  		if errs := infrav1.ValidateImage(image, field.NewPath("image")); len(errs) > 0 {
   138  			agg := kerrors.NewAggregate(errs.ToAggregate().Errors())
   139  			return agg
   140  		}
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  // ValidateTerminateNotificationTimeout termination notification timeout to be between 5 and 15.
   147  func (amp *AzureMachinePool) ValidateTerminateNotificationTimeout() error {
   148  	if amp.Spec.Template.TerminateNotificationTimeout == nil {
   149  		return nil
   150  	}
   151  	if *amp.Spec.Template.TerminateNotificationTimeout < 5 {
   152  		return errors.New("minimum timeout 5 is allowed for TerminateNotificationTimeout")
   153  	}
   154  
   155  	if *amp.Spec.Template.TerminateNotificationTimeout > 15 {
   156  		return errors.New("maximum timeout 15 is allowed for TerminateNotificationTimeout")
   157  	}
   158  
   159  	return nil
   160  }
   161  
   162  // ValidateSSHKey validates an SSHKey.
   163  func (amp *AzureMachinePool) ValidateSSHKey() error {
   164  	if amp.Spec.Template.SSHPublicKey != "" {
   165  		sshKey := amp.Spec.Template.SSHPublicKey
   166  		if errs := infrav1.ValidateSSHKey(sshKey, field.NewPath("sshKey")); len(errs) > 0 {
   167  			agg := kerrors.NewAggregate(errs.ToAggregate().Errors())
   168  			return agg
   169  		}
   170  	}
   171  
   172  	return nil
   173  }
   174  
   175  // ValidateUserAssignedIdentity validates the user-assigned identities list.
   176  func (amp *AzureMachinePool) ValidateUserAssignedIdentity() error {
   177  	fldPath := field.NewPath("UserAssignedIdentities")
   178  	if errs := infrav1.ValidateUserAssignedIdentity(amp.Spec.Identity, amp.Spec.UserAssignedIdentities, fldPath); len(errs) > 0 {
   179  		return kerrors.NewAggregate(errs.ToAggregate().Errors())
   180  	}
   181  
   182  	return nil
   183  }
   184  
   185  // ValidateStrategy validates the strategy.
   186  func (amp *AzureMachinePool) ValidateStrategy() func() error {
   187  	return func() error {
   188  		if amp.Spec.Strategy.Type == RollingUpdateAzureMachinePoolDeploymentStrategyType && amp.Spec.Strategy.RollingUpdate != nil {
   189  			rollingUpdateStrategy := amp.Spec.Strategy.RollingUpdate
   190  			maxSurge := rollingUpdateStrategy.MaxSurge
   191  			maxUnavailable := rollingUpdateStrategy.MaxUnavailable
   192  			if maxSurge.Type == intstr.Int && maxSurge.IntVal == 0 &&
   193  				maxUnavailable.Type == intstr.Int && maxUnavailable.IntVal == 0 {
   194  				return errors.New("rolling update strategy MaxUnavailable must not be 0 if MaxSurge is 0")
   195  			}
   196  		}
   197  
   198  		return nil
   199  	}
   200  }
   201  
   202  // ValidateSystemAssignedIdentity validates system-assigned identity role.
   203  func (amp *AzureMachinePool) ValidateSystemAssignedIdentity(old runtime.Object) func() error {
   204  	return func() error {
   205  		var oldRole string
   206  		if old != nil {
   207  			oldMachinePool, ok := old.(*AzureMachinePool)
   208  			if !ok {
   209  				return fmt.Errorf("unexpected type for old azure machine pool object. Expected: %q, Got: %q",
   210  					"AzureMachinePool", reflect.TypeOf(old))
   211  			}
   212  			if amp.Spec.SystemAssignedIdentityRole != nil {
   213  				oldRole = oldMachinePool.Spec.SystemAssignedIdentityRole.Name
   214  			}
   215  		}
   216  
   217  		roleAssignmentName := ""
   218  		if amp.Spec.SystemAssignedIdentityRole != nil {
   219  			roleAssignmentName = amp.Spec.SystemAssignedIdentityRole.Name
   220  		}
   221  
   222  		fldPath := field.NewPath("roleAssignmentName")
   223  		if errs := infrav1.ValidateSystemAssignedIdentity(amp.Spec.Identity, oldRole, roleAssignmentName, fldPath); len(errs) > 0 {
   224  			return kerrors.NewAggregate(errs.ToAggregate().Errors())
   225  		}
   226  
   227  		return nil
   228  	}
   229  }
   230  
   231  // ValidateSystemAssignedIdentityRole validates the scope and roleDefinitionID for the system-assigned identity.
   232  func (amp *AzureMachinePool) ValidateSystemAssignedIdentityRole() error {
   233  	var allErrs field.ErrorList
   234  	if amp.Spec.RoleAssignmentName != "" && amp.Spec.SystemAssignedIdentityRole != nil && amp.Spec.SystemAssignedIdentityRole.Name != "" {
   235  		allErrs = append(allErrs, field.Invalid(field.NewPath("systemAssignedIdentityRole"), amp.Spec.SystemAssignedIdentityRole.Name, "cannot set both roleAssignmentName and systemAssignedIdentityRole.name"))
   236  	}
   237  	if amp.Spec.Identity == infrav1.VMIdentitySystemAssigned {
   238  		if amp.Spec.SystemAssignedIdentityRole.DefinitionID == "" {
   239  			allErrs = append(allErrs, field.Invalid(field.NewPath("systemAssignedIdentityRole", "DefinitionID"), amp.Spec.SystemAssignedIdentityRole.DefinitionID, "the roleDefinitionID field cannot be empty"))
   240  		}
   241  		if amp.Spec.SystemAssignedIdentityRole.Scope == "" {
   242  			allErrs = append(allErrs, field.Invalid(field.NewPath("systemAssignedIdentityRole", "Scope"), amp.Spec.SystemAssignedIdentityRole.Scope, "the scope field cannot be empty"))
   243  		}
   244  	}
   245  	if amp.Spec.Identity != infrav1.VMIdentitySystemAssigned && amp.Spec.SystemAssignedIdentityRole != nil {
   246  		allErrs = append(allErrs, field.Invalid(field.NewPath("systemAssignedIdentityRole"), amp.Spec.SystemAssignedIdentityRole, "systemAssignedIdentityRole can only be set when identity is set to 'SystemAssigned'"))
   247  	}
   248  
   249  	if len(allErrs) > 0 {
   250  		return kerrors.NewAggregate(allErrs.ToAggregate().Errors())
   251  	}
   252  
   253  	return nil
   254  }
   255  
   256  // ValidateDiagnostics validates the Diagnostic spec.
   257  func (amp *AzureMachinePool) ValidateDiagnostics() error {
   258  	var allErrs field.ErrorList
   259  	fieldPath := field.NewPath("diagnostics")
   260  
   261  	diagnostics := amp.Spec.Template.Diagnostics
   262  
   263  	if diagnostics != nil && diagnostics.Boot != nil {
   264  		switch diagnostics.Boot.StorageAccountType {
   265  		case infrav1.UserManagedDiagnosticsStorage:
   266  			if diagnostics.Boot.UserManaged == nil {
   267  				allErrs = append(allErrs, field.Required(fieldPath.Child("UserManaged"),
   268  					fmt.Sprintf("userManaged must be specified when storageAccountType is '%s'", infrav1.UserManagedDiagnosticsStorage)))
   269  			} else if diagnostics.Boot.UserManaged.StorageAccountURI == "" {
   270  				allErrs = append(allErrs, field.Required(fieldPath.Child("StorageAccountURI"),
   271  					fmt.Sprintf("StorageAccountURI cannot be empty when storageAccountType is '%s'", infrav1.UserManagedDiagnosticsStorage)))
   272  			}
   273  		case infrav1.ManagedDiagnosticsStorage:
   274  			if diagnostics.Boot.UserManaged != nil &&
   275  				diagnostics.Boot.UserManaged.StorageAccountURI != "" {
   276  				allErrs = append(allErrs, field.Invalid(fieldPath.Child("StorageAccountURI"), diagnostics.Boot.UserManaged.StorageAccountURI,
   277  					fmt.Sprintf("StorageAccountURI cannot be set when storageAccountType is '%s'",
   278  						infrav1.ManagedDiagnosticsStorage)))
   279  			}
   280  		case infrav1.DisabledDiagnosticsStorage:
   281  			if diagnostics.Boot.UserManaged != nil &&
   282  				diagnostics.Boot.UserManaged.StorageAccountURI != "" {
   283  				allErrs = append(allErrs, field.Invalid(fieldPath.Child("StorageAccountURI"), diagnostics.Boot.UserManaged.StorageAccountURI,
   284  					fmt.Sprintf("StorageAccountURI cannot be set when storageAccountType is '%s'",
   285  						infrav1.ManagedDiagnosticsStorage)))
   286  			}
   287  		}
   288  	}
   289  
   290  	if len(allErrs) > 0 {
   291  		return kerrors.NewAggregate(allErrs.ToAggregate().Errors())
   292  	}
   293  
   294  	return nil
   295  }
   296  
   297  // ValidateOrchestrationMode validates requirements for the VMSS orchestration mode.
   298  func (amp *AzureMachinePool) ValidateOrchestrationMode(c client.Client) func() error {
   299  	return func() error {
   300  		// Only Flexible orchestration mode requires validation.
   301  		if amp.Spec.OrchestrationMode == infrav1.OrchestrationModeType(armcompute.OrchestrationModeFlexible) {
   302  			parent, err := azureutil.FindParentMachinePoolWithRetry(amp.Name, c, 5)
   303  			if err != nil {
   304  				return errors.Wrap(err, "failed to find parent MachinePool")
   305  			}
   306  			// Kubernetes must be >= 1.26.0 for cloud-provider-azure Helm chart support.
   307  			if parent.Spec.Template.Spec.Version == nil {
   308  				return errors.New("could not find Kubernetes version in MachinePool")
   309  			}
   310  			k8sVersion, err := semver.ParseTolerant(*parent.Spec.Template.Spec.Version)
   311  			if err != nil {
   312  				return errors.Wrap(err, "failed to parse Kubernetes version")
   313  			}
   314  			if k8sVersion.LT(semver.MustParse("1.26.0")) {
   315  				return errors.New(fmt.Sprintf("specified Kubernetes version %s must be >= 1.26.0 for Flexible orchestration mode", k8sVersion))
   316  			}
   317  		}
   318  
   319  		return nil
   320  	}
   321  }