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 }