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 }