sigs.k8s.io/cluster-api-provider-azure@v1.17.0/api/v1beta1/azuremanagedcontrolplane_webhook.go (about) 1 /* 2 Copyright 2023 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 "net" 23 "reflect" 24 "regexp" 25 "strconv" 26 "strings" 27 "time" 28 29 apierrors "k8s.io/apimachinery/pkg/api/errors" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/util/validation/field" 32 "k8s.io/utils/ptr" 33 "sigs.k8s.io/cluster-api-provider-azure/feature" 34 "sigs.k8s.io/cluster-api-provider-azure/util/versions" 35 webhookutils "sigs.k8s.io/cluster-api-provider-azure/util/webhook" 36 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 37 capifeature "sigs.k8s.io/cluster-api/feature" 38 ctrl "sigs.k8s.io/controller-runtime" 39 "sigs.k8s.io/controller-runtime/pkg/client" 40 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 41 ) 42 43 var ( 44 kubeSemver = regexp.MustCompile(`^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)([-0-9a-zA-Z_\.+]*)?$`) 45 rMaxNodeProvisionTime = regexp.MustCompile(`^(\d+)m$`) 46 rScaleDownTime = regexp.MustCompile(`^(\d+)m$`) 47 rScaleDownDelayAfterDelete = regexp.MustCompile(`^(\d+)s$`) 48 rScanInterval = regexp.MustCompile(`^(\d+)s$`) 49 ) 50 51 // SetupAzureManagedControlPlaneWebhookWithManager sets up and registers the webhook with the manager. 52 func SetupAzureManagedControlPlaneWebhookWithManager(mgr ctrl.Manager) error { 53 mw := &azureManagedControlPlaneWebhook{Client: mgr.GetClient()} 54 return ctrl.NewWebhookManagedBy(mgr). 55 For(&AzureManagedControlPlane{}). 56 WithDefaulter(mw). 57 WithValidator(mw). 58 Complete() 59 } 60 61 // +kubebuilder:webhook:path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-azuremanagedcontrolplane,mutating=true,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=azuremanagedcontrolplanes,verbs=create;update,versions=v1beta1,name=default.azuremanagedcontrolplanes.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 62 63 // azureManagedControlPlaneWebhook implements a validating and defaulting webhook for AzureManagedControlPlane. 64 type azureManagedControlPlaneWebhook struct { 65 Client client.Client 66 } 67 68 // Default implements webhook.Defaulter so a webhook will be registered for the type. 69 func (mw *azureManagedControlPlaneWebhook) Default(ctx context.Context, obj runtime.Object) error { 70 m, ok := obj.(*AzureManagedControlPlane) 71 if !ok { 72 return apierrors.NewBadRequest("expected an AzureManagedControlPlane") 73 } 74 if m.Spec.NetworkPlugin == nil { 75 networkPlugin := AzureNetworkPluginName 76 m.Spec.NetworkPlugin = &networkPlugin 77 } 78 79 setDefault[*string](&m.Spec.NetworkPlugin, ptr.To(AzureNetworkPluginName)) 80 setDefault[*string](&m.Spec.LoadBalancerSKU, ptr.To("Standard")) 81 setDefault[*Identity](&m.Spec.Identity, &Identity{ 82 Type: ManagedControlPlaneIdentityTypeSystemAssigned, 83 }) 84 setDefault[*bool](&m.Spec.EnablePreviewFeatures, ptr.To(false)) 85 m.Spec.Version = setDefaultVersion(m.Spec.Version) 86 m.Spec.SKU = setDefaultSku(m.Spec.SKU) 87 m.Spec.AutoScalerProfile = setDefaultAutoScalerProfile(m.Spec.AutoScalerProfile) 88 m.Spec.FleetsMember = setDefaultFleetsMember(m.Spec.FleetsMember, m.Labels) 89 90 if err := m.setDefaultSSHPublicKey(); err != nil { 91 ctrl.Log.WithName("AzureManagedControlPlaneWebHookLogger").Error(err, "setDefaultSSHPublicKey failed") 92 } 93 94 m.setDefaultResourceGroupName() 95 m.setDefaultNodeResourceGroupName() 96 m.setDefaultVirtualNetwork() 97 m.setDefaultSubnet() 98 m.setDefaultOIDCIssuerProfile() 99 m.setDefaultDNSPrefix() 100 m.setDefaultAKSExtensions() 101 102 return nil 103 } 104 105 // +kubebuilder:webhook:verbs=create;update,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-azuremanagedcontrolplane,mutating=false,failurePolicy=fail,groups=infrastructure.cluster.x-k8s.io,resources=azuremanagedcontrolplanes,versions=v1beta1,name=validation.azuremanagedcontrolplanes.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1 106 107 // ValidateCreate implements webhook.Validator so a webhook will be registered for the type. 108 func (mw *azureManagedControlPlaneWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 109 m, ok := obj.(*AzureManagedControlPlane) 110 if !ok { 111 return nil, apierrors.NewBadRequest("expected an AzureManagedControlPlane") 112 } 113 // NOTE: AzureManagedControlPlane relies upon MachinePools, which is behind a feature gate flag. 114 // The webhook must prevent creating new objects in case the feature flag is disabled. 115 if !feature.Gates.Enabled(capifeature.MachinePool) { 116 return nil, field.Forbidden( 117 field.NewPath("spec"), 118 "can be set only if the Cluster API 'MachinePool' feature flag is enabled", 119 ) 120 } 121 122 return nil, m.Validate(mw.Client) 123 } 124 125 // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type. 126 func (mw *azureManagedControlPlaneWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { 127 var allErrs field.ErrorList 128 old, ok := oldObj.(*AzureManagedControlPlane) 129 if !ok { 130 return nil, apierrors.NewBadRequest("expected an AzureManagedControlPlane") 131 } 132 m, ok := newObj.(*AzureManagedControlPlane) 133 if !ok { 134 return nil, apierrors.NewBadRequest("expected an AzureManagedControlPlane") 135 } 136 137 immutableFields := []struct { 138 path *field.Path 139 old interface{} 140 new interface{} 141 }{ 142 {field.NewPath("spec", "subscriptionID"), old.Spec.SubscriptionID, m.Spec.SubscriptionID}, 143 {field.NewPath("spec", "resourceGroupName"), old.Spec.ResourceGroupName, m.Spec.ResourceGroupName}, 144 {field.NewPath("spec", "nodeResourceGroupName"), old.Spec.NodeResourceGroupName, m.Spec.NodeResourceGroupName}, 145 {field.NewPath("spec", "location"), old.Spec.Location, m.Spec.Location}, 146 {field.NewPath("spec", "sshPublicKey"), old.Spec.SSHPublicKey, m.Spec.SSHPublicKey}, 147 {field.NewPath("spec", "dnsServiceIP"), old.Spec.DNSServiceIP, m.Spec.DNSServiceIP}, 148 {field.NewPath("spec", "networkPlugin"), old.Spec.NetworkPlugin, m.Spec.NetworkPlugin}, 149 {field.NewPath("spec", "networkPolicy"), old.Spec.NetworkPolicy, m.Spec.NetworkPolicy}, 150 {field.NewPath("spec", "networkDataplane"), old.Spec.NetworkDataplane, m.Spec.NetworkDataplane}, 151 {field.NewPath("spec", "loadBalancerSKU"), old.Spec.LoadBalancerSKU, m.Spec.LoadBalancerSKU}, 152 {field.NewPath("spec", "httpProxyConfig"), old.Spec.HTTPProxyConfig, m.Spec.HTTPProxyConfig}, 153 {field.NewPath("spec", "azureEnvironment"), old.Spec.AzureEnvironment, m.Spec.AzureEnvironment}, 154 } 155 156 for _, f := range immutableFields { 157 if err := webhookutils.ValidateImmutable(f.path, f.old, f.new); err != nil { 158 allErrs = append(allErrs, err) 159 } 160 } 161 162 // This nil check is only to streamline tests from having to define this correctly in every test case. 163 // Normally, the defaulting webhooks will always set the new DNSPrefix so users can never entirely unset it. 164 if m.Spec.DNSPrefix != nil { 165 // Pre-1.12 versions of CAPZ do not set this field while 1.12+ defaults it, so emulate the current 166 // defaulting here to avoid unrelated updates from failing this immutability check due to the 167 // nil -> non-nil transition. 168 oldDNSPrefix := old.Spec.DNSPrefix 169 if oldDNSPrefix == nil { 170 oldDNSPrefix = ptr.To(old.Name) 171 } 172 if err := webhookutils.ValidateImmutable( 173 field.NewPath("spec", "dnsPrefix"), 174 oldDNSPrefix, 175 m.Spec.DNSPrefix, 176 ); err != nil { 177 allErrs = append(allErrs, err) 178 } 179 } 180 181 // Consider removing this once moves out of preview 182 // Updating outboundType after cluster creation (PREVIEW) 183 // https://learn.microsoft.com/en-us/azure/aks/egress-outboundtype#updating-outboundtype-after-cluster-creation-preview 184 if err := webhookutils.ValidateImmutable( 185 field.NewPath("spec", "outboundType"), 186 old.Spec.OutboundType, 187 m.Spec.OutboundType); err != nil { 188 allErrs = append(allErrs, err) 189 } 190 191 if errs := m.validateVirtualNetworkUpdate(old); len(errs) > 0 { 192 allErrs = append(allErrs, errs...) 193 } 194 195 if errs := m.validateAddonProfilesUpdate(old); len(errs) > 0 { 196 allErrs = append(allErrs, errs...) 197 } 198 199 if errs := m.validateAPIServerAccessProfileUpdate(old); len(errs) > 0 { 200 allErrs = append(allErrs, errs...) 201 } 202 203 if errs := m.validateNetworkPluginModeUpdate(old); len(errs) > 0 { 204 allErrs = append(allErrs, errs...) 205 } 206 207 if errs := m.validateAADProfileUpdateAndLocalAccounts(old); len(errs) > 0 { 208 allErrs = append(allErrs, errs...) 209 } 210 211 if errs := m.validateAutoUpgradeProfile(old); len(errs) > 0 { 212 allErrs = append(allErrs, errs...) 213 } 214 215 if errs := m.validateK8sVersionUpdate(old); len(errs) > 0 { 216 allErrs = append(allErrs, errs...) 217 } 218 219 if errs := m.validateOIDCIssuerProfileUpdate(old); len(errs) > 0 { 220 allErrs = append(allErrs, errs...) 221 } 222 223 if errs := m.validateFleetsMemberUpdate(old); len(errs) > 0 { 224 allErrs = append(allErrs, errs...) 225 } 226 227 if errs := validateAKSExtensionsUpdate(old.Spec.Extensions, m.Spec.Extensions); len(errs) > 0 { 228 allErrs = append(allErrs, errs...) 229 } 230 231 if errs := m.Spec.AzureManagedControlPlaneClassSpec.validateSecurityProfileUpdate(&old.Spec.AzureManagedControlPlaneClassSpec); len(errs) > 0 { 232 allErrs = append(allErrs, errs...) 233 } 234 235 if len(allErrs) == 0 { 236 return nil, m.Validate(mw.Client) 237 } 238 239 return nil, apierrors.NewInvalid(GroupVersion.WithKind(AzureManagedControlPlaneKind).GroupKind(), m.Name, allErrs) 240 } 241 242 // ValidateDelete implements webhook.Validator so a webhook will be registered for the type. 243 func (mw *azureManagedControlPlaneWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { 244 return nil, nil 245 } 246 247 // Validate the Azure Managed Control Plane and return an aggregate error. 248 func (m *AzureManagedControlPlane) Validate(cli client.Client) error { 249 var allErrs field.ErrorList 250 validators := []func(client client.Client) field.ErrorList{ 251 m.validateSSHKey, 252 m.validateIdentity, 253 m.validateNetworkPluginMode, 254 m.validateDNSPrefix, 255 m.validateDisableLocalAccounts, 256 } 257 for _, validator := range validators { 258 if err := validator(cli); err != nil { 259 allErrs = append(allErrs, err...) 260 } 261 } 262 263 allErrs = append(allErrs, validateVersion( 264 m.Spec.Version, 265 field.NewPath("spec").Child("version"))...) 266 267 allErrs = append(allErrs, validateLoadBalancerProfile( 268 m.Spec.LoadBalancerProfile, 269 field.NewPath("spec").Child("loadBalancerProfile"))...) 270 271 allErrs = append(allErrs, validateManagedClusterNetwork( 272 cli, 273 m.Labels, 274 m.Namespace, 275 m.Spec.DNSServiceIP, 276 m.Spec.VirtualNetwork.Subnet, 277 field.NewPath("spec"))...) 278 279 allErrs = append(allErrs, validateName(m.Name, field.NewPath("name"))...) 280 281 allErrs = append(allErrs, validateAutoScalerProfile(m.Spec.AutoScalerProfile, field.NewPath("spec").Child("autoScalerProfile"))...) 282 283 allErrs = append(allErrs, validateAKSExtensions(m.Spec.Extensions, field.NewPath("spec").Child("aksExtensions"))...) 284 285 allErrs = append(allErrs, m.Spec.AzureManagedControlPlaneClassSpec.validateSecurityProfile()...) 286 287 allErrs = append(allErrs, validateNetworkPolicy(m.Spec.NetworkPolicy, m.Spec.NetworkDataplane, field.NewPath("spec").Child("networkPolicy"))...) 288 289 allErrs = append(allErrs, validateNetworkDataplane(m.Spec.NetworkDataplane, m.Spec.NetworkPolicy, m.Spec.NetworkPluginMode, field.NewPath("spec").Child("networkDataplane"))...) 290 291 allErrs = append(allErrs, validateAPIServerAccessProfile(m.Spec.APIServerAccessProfile, field.NewPath("spec").Child("apiServerAccessProfile"))...) 292 293 allErrs = append(allErrs, validateAMCPVirtualNetwork(m.Spec.VirtualNetwork, field.NewPath("spec").Child("virtualNetwork"))...) 294 295 allErrs = append(allErrs, validateFleetsMember(m.Spec.FleetsMember, field.NewPath("spec").Child("fleetsMember"))...) 296 297 return allErrs.ToAggregate() 298 } 299 300 func (m *AzureManagedControlPlane) validateDNSPrefix(_ client.Client) field.ErrorList { 301 if m.Spec.DNSPrefix == nil { 302 return nil 303 } 304 305 // Regex pattern for DNS prefix validation 306 // 1. Between 1 and 54 characters long: {1,54} 307 // 2. Alphanumerics and hyphens: [a-zA-Z0-9-] 308 // 3. Start and end with alphanumeric: ^[a-zA-Z0-9].*[a-zA-Z0-9]$ 309 pattern := `^[a-zA-Z0-9][a-zA-Z0-9-]{0,52}[a-zA-Z0-9]$` 310 regex := regexp.MustCompile(pattern) 311 if regex.MatchString(ptr.Deref(m.Spec.DNSPrefix, "")) { 312 return nil 313 } 314 allErrs := field.ErrorList{ 315 field.Invalid(field.NewPath("spec", "dnsPrefix"), *m.Spec.DNSPrefix, "DNSPrefix is invalid, does not match regex: "+pattern), 316 } 317 return allErrs 318 } 319 320 // validateSecurityProfile validates SecurityProfile. 321 func (m *AzureManagedControlPlaneClassSpec) validateSecurityProfile() field.ErrorList { 322 allErrs := field.ErrorList{} 323 if err := m.validateAzureKeyVaultKms(); err != nil { 324 allErrs = append(allErrs, err...) 325 } 326 if err := m.validateWorkloadIdentity(); err != nil { 327 allErrs = append(allErrs, err...) 328 } 329 return allErrs 330 } 331 332 // validateAzureKeyVaultKms validates AzureKeyVaultKms. 333 func (m *AzureManagedControlPlaneClassSpec) validateAzureKeyVaultKms() field.ErrorList { 334 if m.SecurityProfile != nil && m.SecurityProfile.AzureKeyVaultKms != nil { 335 if !m.isUserManagedIdentityEnabled() { 336 allErrs := field.ErrorList{ 337 field.Invalid(field.NewPath("spec", "securityProfile", "azureKeyVaultKms", "keyVaultResourceID"), 338 m.SecurityProfile.AzureKeyVaultKms.KeyVaultResourceID, 339 "Spec.SecurityProfile.AzureKeyVaultKms can be set only when Spec.Identity.Type is UserAssigned"), 340 } 341 return allErrs 342 } 343 keyVaultNetworkAccess := ptr.Deref(m.SecurityProfile.AzureKeyVaultKms.KeyVaultNetworkAccess, KeyVaultNetworkAccessTypesPublic) 344 keyVaultResourceID := ptr.Deref(m.SecurityProfile.AzureKeyVaultKms.KeyVaultResourceID, "") 345 if keyVaultNetworkAccess == KeyVaultNetworkAccessTypesPrivate && keyVaultResourceID == "" { 346 allErrs := field.ErrorList{ 347 field.Invalid(field.NewPath("spec", "securityProfile", "azureKeyVaultKms", "keyVaultResourceID"), 348 m.SecurityProfile.AzureKeyVaultKms.KeyVaultResourceID, 349 "Spec.SecurityProfile.AzureKeyVaultKms.KeyVaultResourceID cannot be empty when Spec.SecurityProfile.AzureKeyVaultKms.KeyVaultNetworkAccess is Private"), 350 } 351 return allErrs 352 } 353 if keyVaultNetworkAccess == KeyVaultNetworkAccessTypesPublic && keyVaultResourceID != "" { 354 allErrs := field.ErrorList{ 355 field.Invalid(field.NewPath("spec", "securityProfile", "azureKeyVaultKms", "keyVaultResourceID"), m.SecurityProfile.AzureKeyVaultKms.KeyVaultResourceID, 356 "Spec.SecurityProfile.AzureKeyVaultKms.KeyVaultResourceID should be empty when Spec.SecurityProfile.AzureKeyVaultKms.KeyVaultNetworkAccess is Public"), 357 } 358 return allErrs 359 } 360 } 361 return nil 362 } 363 364 // validateWorkloadIdentity validates WorkloadIdentity. 365 func (m *AzureManagedControlPlaneClassSpec) validateWorkloadIdentity() field.ErrorList { 366 if m.SecurityProfile != nil && m.SecurityProfile.WorkloadIdentity != nil && !m.isOIDCEnabled() { 367 allErrs := field.ErrorList{ 368 field.Invalid(field.NewPath("spec", "securityProfile", "workloadIdentity"), m.SecurityProfile.WorkloadIdentity, 369 "Spec.SecurityProfile.WorkloadIdentity cannot be enabled when Spec.OIDCIssuerProfile is disabled"), 370 } 371 return allErrs 372 } 373 return nil 374 } 375 376 // validateDisableLocalAccounts disabling local accounts for AAD based clusters. 377 func (m *AzureManagedControlPlane) validateDisableLocalAccounts(_ client.Client) field.ErrorList { 378 if m.Spec.DisableLocalAccounts != nil && m.Spec.AADProfile == nil { 379 return field.ErrorList{ 380 field.Invalid(field.NewPath("spec", "disableLocalAccounts"), *m.Spec.DisableLocalAccounts, "DisableLocalAccounts should be set only for AAD enabled clusters"), 381 } 382 } 383 return nil 384 } 385 386 // validateVersion validates the Kubernetes version. 387 func validateVersion(version string, fldPath *field.Path) field.ErrorList { 388 var allErrs field.ErrorList 389 if !kubeSemver.MatchString(version) { 390 allErrs = append(allErrs, field.Invalid(fldPath, version, "must be a valid semantic version")) 391 } 392 393 return allErrs 394 } 395 396 // validateSSHKey validates an SSHKey. 397 func (m *AzureManagedControlPlane) validateSSHKey(_ client.Client) field.ErrorList { 398 if sshKey := m.Spec.SSHPublicKey; sshKey != nil && *sshKey != "" { 399 if errs := ValidateSSHKey(*sshKey, field.NewPath("sshKey")); len(errs) > 0 { 400 return errs 401 } 402 } 403 404 return nil 405 } 406 407 // validateLoadBalancerProfile validates a LoadBalancerProfile. 408 func validateLoadBalancerProfile(loadBalancerProfile *LoadBalancerProfile, fldPath *field.Path) field.ErrorList { 409 var allErrs field.ErrorList 410 if loadBalancerProfile != nil { 411 numOutboundIPTypes := 0 412 413 if loadBalancerProfile.ManagedOutboundIPs != nil { 414 if *loadBalancerProfile.ManagedOutboundIPs < 1 || *loadBalancerProfile.ManagedOutboundIPs > 100 { 415 allErrs = append(allErrs, field.Invalid(fldPath.Child("ManagedOutboundIPs"), *loadBalancerProfile.ManagedOutboundIPs, "value should be in between 1 and 100")) 416 } 417 } 418 419 if loadBalancerProfile.AllocatedOutboundPorts != nil { 420 if *loadBalancerProfile.AllocatedOutboundPorts < 0 || *loadBalancerProfile.AllocatedOutboundPorts > 64000 { 421 allErrs = append(allErrs, field.Invalid(fldPath.Child("AllocatedOutboundPorts"), *loadBalancerProfile.AllocatedOutboundPorts, "value should be in between 0 and 64000")) 422 } 423 } 424 425 if loadBalancerProfile.IdleTimeoutInMinutes != nil { 426 if *loadBalancerProfile.IdleTimeoutInMinutes < 4 || *loadBalancerProfile.IdleTimeoutInMinutes > 120 { 427 allErrs = append(allErrs, field.Invalid(fldPath.Child("IdleTimeoutInMinutes"), *loadBalancerProfile.IdleTimeoutInMinutes, "value should be in between 4 and 120")) 428 } 429 } 430 431 if loadBalancerProfile.ManagedOutboundIPs != nil { 432 numOutboundIPTypes++ 433 } 434 if len(loadBalancerProfile.OutboundIPPrefixes) > 0 { 435 numOutboundIPTypes++ 436 } 437 if len(loadBalancerProfile.OutboundIPs) > 0 { 438 numOutboundIPTypes++ 439 } 440 if numOutboundIPTypes > 1 { 441 allErrs = append(allErrs, field.Forbidden(fldPath, "load balancer profile must specify at most one of ManagedOutboundIPs, OutboundIPPrefixes and OutboundIPs")) 442 } 443 } 444 445 return allErrs 446 } 447 448 func validateAMCPVirtualNetwork(virtualNetwork ManagedControlPlaneVirtualNetwork, fldPath *field.Path) field.ErrorList { 449 var allErrs field.ErrorList 450 451 // VirtualNetwork and the CIDR blocks get defaulted in the defaulting webhook, so we can assume they are always set. 452 if !reflect.DeepEqual(virtualNetwork, ManagedControlPlaneVirtualNetwork{}) { 453 _, parentNet, vnetErr := net.ParseCIDR(virtualNetwork.CIDRBlock) 454 if vnetErr != nil { 455 allErrs = append(allErrs, field.Invalid(fldPath.Child("CIDRBlock"), virtualNetwork.CIDRBlock, "pre-existing virtual networks CIDR block is invalid")) 456 } 457 subnetIP, _, subnetErr := net.ParseCIDR(virtualNetwork.Subnet.CIDRBlock) 458 if subnetErr != nil { 459 allErrs = append(allErrs, field.Invalid(fldPath.Child("Subnet", "CIDRBlock"), virtualNetwork.CIDRBlock, "pre-existing subnets CIDR block is invalid")) 460 } 461 if vnetErr == nil && subnetErr == nil && !parentNet.Contains(subnetIP) { 462 allErrs = append(allErrs, field.Invalid(fldPath.Child("CIDRBlock"), virtualNetwork.CIDRBlock, "pre-existing virtual networks CIDR block should contain the subnet CIDR block")) 463 } 464 } 465 return allErrs 466 } 467 468 func validateFleetsMember(fleetsMember *FleetsMember, fldPath *field.Path) field.ErrorList { 469 var allErrs field.ErrorList 470 471 if fleetsMember != nil && fleetsMember.Name != "" { 472 match, _ := regexp.MatchString(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`, fleetsMember.Name) 473 if !match { 474 allErrs = append(allErrs, 475 field.Invalid( 476 fldPath.Child("Name"), 477 fleetsMember.Name, 478 "Name must match ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", 479 ), 480 ) 481 } 482 } 483 484 return allErrs 485 } 486 487 // validateAPIServerAccessProfile validates an APIServerAccessProfile. 488 func validateAPIServerAccessProfile(apiServerAccessProfile *APIServerAccessProfile, fldPath *field.Path) field.ErrorList { 489 var allErrs field.ErrorList 490 if apiServerAccessProfile != nil { 491 for _, ipRange := range apiServerAccessProfile.AuthorizedIPRanges { 492 if _, _, err := net.ParseCIDR(ipRange); err != nil { 493 allErrs = append(allErrs, field.Invalid(fldPath, ipRange, "invalid CIDR format")) 494 } 495 } 496 497 // privateDNSZone should either be "System" or "None" or the private dns zone name should be in either of these 498 // formats: 'private.<location>.azmk8s.io,privatelink.<location>.azmk8s.io,[a-zA-Z0-9-]{1,32}.private.<location>.azmk8s.io, 499 // [a-zA-Z0-9-]{1,32}.privatelink.<location>.azmk8s.io'. The validation below follows the guidelines mentioned at 500 // https://learn.microsoft.com/azure/aks/private-clusters?tabs=azure-portal#configure-a-private-dns-zone. 501 // Performing a lower case comparison to avoid case sensitivity. 502 if apiServerAccessProfile.PrivateDNSZone != nil { 503 privateDNSZone := strings.ToLower(ptr.Deref(apiServerAccessProfile.PrivateDNSZone, "")) 504 if !strings.EqualFold(strings.ToLower(privateDNSZone), "system") && 505 !strings.EqualFold(strings.ToLower(privateDNSZone), "none") { 506 // Extract substring starting from "privatednszones/" 507 startIndex := strings.Index(strings.ToLower(privateDNSZone), "privatednszones/") 508 if startIndex == -1 { 509 allErrs = append(allErrs, field.Invalid(fldPath, privateDNSZone, "invalid private DNS zone")) 510 return allErrs 511 } 512 513 // Private DNS Zones can only be used by private clusters. 514 if !ptr.Deref(apiServerAccessProfile.EnablePrivateCluster, false) { 515 allErrs = append(allErrs, field.Invalid(fldPath, apiServerAccessProfile.EnablePrivateCluster, "Private Cluster should be enabled to use PrivateDNSZone")) 516 return allErrs 517 } 518 519 extractedPrivateDNSZone := privateDNSZone[startIndex+len("privatednszones/"):] 520 521 patternWithLocation := `^(privatelink|private)\.[a-zA-Z0-9]+\.(azmk8s\.io)$` 522 locationRegex := regexp.MustCompile(patternWithLocation) 523 patternWithSubzone := `^[a-zA-Z0-9-]{1,32}\.(privatelink|private)\.[a-zA-Z0-9]+\.(azmk8s\.io)$` 524 subzoneRegex := regexp.MustCompile(patternWithSubzone) 525 526 // check if privateDNSZone is a valid resource ID 527 if !locationRegex.MatchString(extractedPrivateDNSZone) && !subzoneRegex.MatchString(extractedPrivateDNSZone) { 528 allErrs = append(allErrs, field.Invalid(fldPath, privateDNSZone, "invalid privateDnsZone resource ID. Each label the private dns zone name should be in either of these formats: 'private.<location>.azmk8s.io,privatelink.<location>.azmk8s.io,[a-zA-Z0-9-]{1,32}.private.<location>.azmk8s.io,[a-zA-Z0-9-]{1,32}.privatelink.<location>.azmk8s.io'")) 529 } 530 } 531 } 532 } 533 return allErrs 534 } 535 536 // validateManagedClusterNetwork validates the Cluster network values. 537 func validateManagedClusterNetwork(cli client.Client, labels map[string]string, namespace string, dnsServiceIP *string, subnet ManagedControlPlaneSubnet, fldPath *field.Path) field.ErrorList { 538 var ( 539 allErrs field.ErrorList 540 serviceCIDR string 541 ) 542 543 ctx := context.Background() 544 545 // Fetch the Cluster. 546 clusterName, ok := labels[clusterv1.ClusterNameLabel] 547 if !ok { 548 return nil 549 } 550 551 ownerCluster := &clusterv1.Cluster{} 552 key := client.ObjectKey{ 553 Namespace: namespace, 554 Name: clusterName, 555 } 556 557 if err := cli.Get(ctx, key, ownerCluster); err != nil { 558 allErrs = append(allErrs, field.InternalError(field.NewPath("Cluster", "spec", "clusterNetwork"), err)) 559 return allErrs 560 } 561 562 if clusterNetwork := ownerCluster.Spec.ClusterNetwork; clusterNetwork != nil { 563 if clusterNetwork.Services != nil { 564 // A user may provide zero or one CIDR blocks. If they provide an empty array, 565 // we ignore it and use the default. AKS doesn't support > 1 Service/Pod CIDR. 566 if len(clusterNetwork.Services.CIDRBlocks) > 1 { 567 allErrs = append(allErrs, field.TooMany(field.NewPath("Cluster", "spec", "clusterNetwork", "services", "cidrBlocks"), len(clusterNetwork.Services.CIDRBlocks), 1)) 568 } 569 if len(clusterNetwork.Services.CIDRBlocks) == 1 { 570 serviceCIDR = clusterNetwork.Services.CIDRBlocks[0] 571 } 572 } 573 if clusterNetwork.Pods != nil { 574 // A user may provide zero or one CIDR blocks. If they provide an empty array, 575 // we ignore it and use the default. AKS doesn't support > 1 Service/Pod CIDR. 576 if len(clusterNetwork.Pods.CIDRBlocks) > 1 { 577 allErrs = append(allErrs, field.TooMany(field.NewPath("Cluster", "spec", "clusterNetwork", "pods", "cidrBlocks"), len(clusterNetwork.Pods.CIDRBlocks), 1)) 578 } 579 } 580 } 581 582 if dnsServiceIP != nil { 583 if serviceCIDR == "" { 584 allErrs = append(allErrs, field.Required(field.NewPath("Cluster", "spec", "clusterNetwork", "services", "cidrBlocks"), "service CIDR must be specified if specifying DNSServiceIP")) 585 } 586 _, cidr, err := net.ParseCIDR(serviceCIDR) 587 if err != nil { 588 allErrs = append(allErrs, field.Invalid(field.NewPath("Cluster", "spec", "clusterNetwork", "services", "cidrBlocks"), serviceCIDR, fmt.Sprintf("failed to parse cluster service cidr: %v", err))) 589 } 590 591 dnsIP := net.ParseIP(*dnsServiceIP) 592 if dnsIP == nil { // dnsIP will be nil if the string is not a valid IP 593 allErrs = append(allErrs, field.Invalid(field.NewPath("Cluster", "spec", "clusterNetwork", "services", "dnsServiceIP"), *dnsServiceIP, "must be a valid IP address")) 594 } 595 596 if dnsIP != nil && !cidr.Contains(dnsIP) { 597 allErrs = append(allErrs, field.Invalid(field.NewPath("Cluster", "spec", "clusterNetwork", "services", "cidrBlocks"), serviceCIDR, "DNSServiceIP must reside within the associated cluster serviceCIDR")) 598 } 599 600 // AKS only supports .10 as the last octet for the DNSServiceIP. 601 // Refer to: https://learn.microsoft.com/en-us/azure/aks/configure-kubenet#create-an-aks-cluster-with-system-assigned-managed-identities 602 targetSuffix := ".10" 603 if dnsIP != nil && !strings.HasSuffix(dnsIP.String(), targetSuffix) { 604 allErrs = append(allErrs, field.Invalid(field.NewPath("Cluster", "spec", "clusterNetwork", "services", "dnsServiceIP"), *dnsServiceIP, fmt.Sprintf("must end with %q", targetSuffix))) 605 } 606 } 607 608 if errs := validatePrivateEndpoints(subnet.PrivateEndpoints, []string{subnet.CIDRBlock}, fldPath.Child("VirtualNetwork.Subnet.PrivateEndpoints")); len(errs) > 0 { 609 allErrs = append(allErrs, errs...) 610 } 611 612 return allErrs 613 } 614 615 // validateAutoUpgradeProfile validates auto upgrade profile. 616 func (m *AzureManagedControlPlane) validateAutoUpgradeProfile(old *AzureManagedControlPlane) field.ErrorList { 617 var allErrs field.ErrorList 618 if old.Spec.AutoUpgradeProfile != nil { 619 if old.Spec.AutoUpgradeProfile.UpgradeChannel != nil && (m.Spec.AutoUpgradeProfile == nil || m.Spec.AutoUpgradeProfile.UpgradeChannel == nil) { 620 // Prevent AutoUpgradeProfile.UpgradeChannel to be set to nil. 621 // Unsetting the field is not allowed. 622 allErrs = append(allErrs, 623 field.Invalid( 624 field.NewPath("Spec", "AutoUpgradeProfile", "UpgradeChannel"), 625 old.Spec.AutoUpgradeProfile.UpgradeChannel, 626 "field cannot be set to nil, to disable auto upgrades set the channel to none.")) 627 } 628 } 629 return allErrs 630 } 631 632 // validateK8sVersionUpdate validates K8s version. 633 func (m *AzureManagedControlPlane) validateK8sVersionUpdate(old *AzureManagedControlPlane) field.ErrorList { 634 var allErrs field.ErrorList 635 if hv := versions.GetHigherK8sVersion(m.Spec.Version, old.Spec.Version); hv != m.Spec.Version { 636 allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "version"), 637 m.Spec.Version, "field version cannot be downgraded"), 638 ) 639 } 640 641 if old.Status.AutoUpgradeVersion != "" && m.Spec.Version != old.Spec.Version { 642 if hv := versions.GetHigherK8sVersion(m.Spec.Version, old.Status.AutoUpgradeVersion); hv != m.Spec.Version { 643 allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "version"), 644 m.Spec.Version, "version is auto-upgraded to "+old.Status.AutoUpgradeVersion+", cannot be downgraded"), 645 ) 646 } 647 } 648 return allErrs 649 } 650 651 // validateAPIServerAccessProfileUpdate validates update to APIServerAccessProfile. 652 func (m *AzureManagedControlPlane) validateAPIServerAccessProfileUpdate(old *AzureManagedControlPlane) field.ErrorList { 653 var allErrs field.ErrorList 654 655 newAPIServerAccessProfileNormalized := &APIServerAccessProfile{} 656 oldAPIServerAccessProfileNormalized := &APIServerAccessProfile{} 657 if m.Spec.APIServerAccessProfile != nil { 658 newAPIServerAccessProfileNormalized = &APIServerAccessProfile{ 659 APIServerAccessProfileClassSpec: APIServerAccessProfileClassSpec{ 660 EnablePrivateCluster: m.Spec.APIServerAccessProfile.EnablePrivateCluster, 661 PrivateDNSZone: m.Spec.APIServerAccessProfile.PrivateDNSZone, 662 EnablePrivateClusterPublicFQDN: m.Spec.APIServerAccessProfile.EnablePrivateClusterPublicFQDN, 663 }, 664 } 665 } 666 if old.Spec.APIServerAccessProfile != nil { 667 oldAPIServerAccessProfileNormalized = &APIServerAccessProfile{ 668 APIServerAccessProfileClassSpec: APIServerAccessProfileClassSpec{ 669 EnablePrivateCluster: old.Spec.APIServerAccessProfile.EnablePrivateCluster, 670 PrivateDNSZone: old.Spec.APIServerAccessProfile.PrivateDNSZone, 671 EnablePrivateClusterPublicFQDN: old.Spec.APIServerAccessProfile.EnablePrivateClusterPublicFQDN, 672 }, 673 } 674 } 675 676 if !reflect.DeepEqual(newAPIServerAccessProfileNormalized, oldAPIServerAccessProfileNormalized) { 677 allErrs = append(allErrs, 678 field.Invalid(field.NewPath("spec", "apiServerAccessProfile"), 679 m.Spec.APIServerAccessProfile, "fields (except for AuthorizedIPRanges) are immutable"), 680 ) 681 } 682 683 return allErrs 684 } 685 686 // validateAddonProfilesUpdate validates update to AddonProfiles. 687 func (m *AzureManagedControlPlane) validateAddonProfilesUpdate(old *AzureManagedControlPlane) field.ErrorList { 688 var allErrs field.ErrorList 689 newAddonProfileMap := map[string]struct{}{} 690 if len(old.Spec.AddonProfiles) != 0 { 691 for _, addonProfile := range m.Spec.AddonProfiles { 692 newAddonProfileMap[addonProfile.Name] = struct{}{} 693 } 694 for i, addonProfile := range old.Spec.AddonProfiles { 695 if _, ok := newAddonProfileMap[addonProfile.Name]; !ok { 696 allErrs = append(allErrs, field.Invalid( 697 field.NewPath("spec", "addonProfiles"), 698 m.Spec.AddonProfiles, 699 fmt.Sprintf("cannot remove addonProfile %s, To disable this AddonProfile, update Spec.AddonProfiles[%v].Enabled to false", addonProfile.Name, i))) 700 } 701 } 702 } 703 return allErrs 704 } 705 706 // validateVirtualNetworkUpdate validates update to VirtualNetwork. 707 func (m *AzureManagedControlPlane) validateVirtualNetworkUpdate(old *AzureManagedControlPlane) field.ErrorList { 708 var allErrs field.ErrorList 709 if old.Spec.VirtualNetwork.Name != m.Spec.VirtualNetwork.Name { 710 allErrs = append(allErrs, 711 field.Invalid( 712 field.NewPath("spec", "virtualNetwork", "name"), 713 m.Spec.VirtualNetwork.Name, 714 "Virtual Network Name is immutable")) 715 } 716 717 if old.Spec.VirtualNetwork.CIDRBlock != m.Spec.VirtualNetwork.CIDRBlock { 718 allErrs = append(allErrs, 719 field.Invalid( 720 field.NewPath("spec", "virtualNetwork", "cidrBlock"), 721 m.Spec.VirtualNetwork.CIDRBlock, 722 "Virtual Network CIDRBlock is immutable")) 723 } 724 725 if old.Spec.VirtualNetwork.Subnet.Name != m.Spec.VirtualNetwork.Subnet.Name { 726 allErrs = append(allErrs, 727 field.Invalid( 728 field.NewPath("spec", "virtualNetwork", "subnet", "name"), 729 m.Spec.VirtualNetwork.Subnet.Name, 730 "Subnet Name is immutable")) 731 } 732 733 // NOTE: This only works because we force the user to set the CIDRBlock for both the 734 // managed and unmanaged Vnets. If we ever update the subnet cidr based on what's 735 // actually set in the subnet, and it is different from what's in the Spec, for 736 // unmanaged Vnets like we do with the AzureCluster this logic will break. 737 if old.Spec.VirtualNetwork.Subnet.CIDRBlock != m.Spec.VirtualNetwork.Subnet.CIDRBlock { 738 allErrs = append(allErrs, 739 field.Invalid( 740 field.NewPath("spec", "virtualNetwork", "subnet", "cidrBlock"), 741 m.Spec.VirtualNetwork.Subnet.CIDRBlock, 742 "Subnet CIDRBlock is immutable")) 743 } 744 745 if old.Spec.VirtualNetwork.ResourceGroup != m.Spec.VirtualNetwork.ResourceGroup { 746 allErrs = append(allErrs, 747 field.Invalid( 748 field.NewPath("spec", "virtualNetwork", "resourceGroup"), 749 m.Spec.VirtualNetwork.ResourceGroup, 750 "Virtual Network Resource Group is immutable")) 751 } 752 return allErrs 753 } 754 755 // validateNetworkPluginModeUpdate validates update to NetworkPluginMode. 756 func (m *AzureManagedControlPlane) validateNetworkPluginModeUpdate(old *AzureManagedControlPlane) field.ErrorList { 757 var allErrs field.ErrorList 758 759 if ptr.Deref(old.Spec.NetworkPluginMode, "") != NetworkPluginModeOverlay && 760 ptr.Deref(m.Spec.NetworkPluginMode, "") == NetworkPluginModeOverlay && 761 old.Spec.NetworkPolicy != nil { 762 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "networkPluginMode"), fmt.Sprintf("%q NetworkPluginMode cannot be enabled when NetworkPolicy is set", NetworkPluginModeOverlay))) 763 } 764 765 return allErrs 766 } 767 768 // validateAADProfileUpdateAndLocalAccounts validates updates for AADProfile. 769 func (m *AzureManagedControlPlane) validateAADProfileUpdateAndLocalAccounts(old *AzureManagedControlPlane) field.ErrorList { 770 var allErrs field.ErrorList 771 if old.Spec.AADProfile != nil { 772 if m.Spec.AADProfile == nil { 773 allErrs = append(allErrs, 774 field.Invalid( 775 field.NewPath("spec", "aadProfile"), 776 m.Spec.AADProfile, 777 "field cannot be nil, cannot disable AADProfile")) 778 } else { 779 if !m.Spec.AADProfile.Managed && old.Spec.AADProfile.Managed { 780 allErrs = append(allErrs, 781 field.Invalid( 782 field.NewPath("spec", "aadProfile", "managed"), 783 m.Spec.AADProfile.Managed, 784 "cannot set AADProfile.Managed to false")) 785 } 786 if len(m.Spec.AADProfile.AdminGroupObjectIDs) == 0 { 787 allErrs = append(allErrs, 788 field.Invalid( 789 field.NewPath("spec", "aadProfile", "adminGroupObjectIDs"), 790 m.Spec.AADProfile.AdminGroupObjectIDs, 791 "length of AADProfile.AdminGroupObjectIDs cannot be zero")) 792 } 793 } 794 } 795 796 if old.Spec.DisableLocalAccounts == nil && 797 m.Spec.DisableLocalAccounts != nil && 798 m.Spec.AADProfile == nil { 799 allErrs = append(allErrs, 800 field.Invalid( 801 field.NewPath("spec", "disableLocalAccounts"), 802 m.Spec.DisableLocalAccounts, 803 "DisableLocalAccounts can be set only for AAD enabled clusters")) 804 } 805 806 if old.Spec.DisableLocalAccounts != nil { 807 // Prevent DisableLocalAccounts modification if it was already set to some value 808 if err := webhookutils.ValidateImmutable( 809 field.NewPath("spec", "disableLocalAccounts"), 810 m.Spec.DisableLocalAccounts, 811 old.Spec.DisableLocalAccounts, 812 ); err != nil { 813 allErrs = append(allErrs, err) 814 } 815 } 816 817 return allErrs 818 } 819 820 // validateSecurityProfileUpdate validates a SecurityProfile update. 821 func (m *AzureManagedControlPlaneClassSpec) validateSecurityProfileUpdate(old *AzureManagedControlPlaneClassSpec) field.ErrorList { 822 var allErrs field.ErrorList 823 if old.SecurityProfile != nil { 824 if errAzureKeyVaultKms := m.validateAzureKeyVaultKmsUpdate(old); errAzureKeyVaultKms != nil { 825 allErrs = append(allErrs, errAzureKeyVaultKms...) 826 } 827 if errWorkloadIdentity := m.validateWorkloadIdentityUpdate(old); errWorkloadIdentity != nil { 828 allErrs = append(allErrs, errWorkloadIdentity...) 829 } 830 if errWorkloadIdentity := m.validateImageCleanerUpdate(old); errWorkloadIdentity != nil { 831 allErrs = append(allErrs, errWorkloadIdentity...) 832 } 833 if errWorkloadIdentity := m.validateDefender(old); errWorkloadIdentity != nil { 834 allErrs = append(allErrs, errWorkloadIdentity...) 835 } 836 } 837 return allErrs 838 } 839 840 // validateAzureKeyVaultKmsUpdate validates AzureKeyVaultKmsUpdate profile. 841 func (m *AzureManagedControlPlaneClassSpec) validateAzureKeyVaultKmsUpdate(old *AzureManagedControlPlaneClassSpec) field.ErrorList { 842 var allErrs field.ErrorList 843 if old.SecurityProfile.AzureKeyVaultKms != nil { 844 if m.SecurityProfile == nil || m.SecurityProfile.AzureKeyVaultKms == nil { 845 allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "securityProfile", "azureKeyVaultKms"), 846 nil, "cannot unset Spec.SecurityProfile.AzureKeyVaultKms profile to disable the profile please set Spec.SecurityProfile.AzureKeyVaultKms.Enabled to false")) 847 return allErrs 848 } 849 } 850 return allErrs 851 } 852 853 // validateWorkloadIdentityUpdate validates WorkloadIdentityUpdate profile. 854 func (m *AzureManagedControlPlaneClassSpec) validateWorkloadIdentityUpdate(old *AzureManagedControlPlaneClassSpec) field.ErrorList { 855 var allErrs field.ErrorList 856 if old.SecurityProfile.WorkloadIdentity != nil { 857 if m.SecurityProfile == nil || m.SecurityProfile.WorkloadIdentity == nil { 858 allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "securityProfile", "workloadIdentity"), 859 nil, "cannot unset Spec.SecurityProfile.WorkloadIdentity, to disable workloadIdentity please set Spec.SecurityProfile.WorkloadIdentity.Enabled to false")) 860 } 861 } 862 return allErrs 863 } 864 865 // validateImageCleanerUpdate validates ImageCleanerUpdate profile. 866 func (m *AzureManagedControlPlaneClassSpec) validateImageCleanerUpdate(old *AzureManagedControlPlaneClassSpec) field.ErrorList { 867 var allErrs field.ErrorList 868 if old.SecurityProfile.ImageCleaner != nil { 869 if m.SecurityProfile == nil || m.SecurityProfile.ImageCleaner == nil { 870 allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "securityProfile", "imageCleaner"), 871 nil, "cannot unset Spec.SecurityProfile.ImageCleaner, to disable imageCleaner please set Spec.SecurityProfile.ImageCleaner.Enabled to false")) 872 } 873 } 874 return allErrs 875 } 876 877 // validateDefender validates defender profile. 878 func (m *AzureManagedControlPlaneClassSpec) validateDefender(old *AzureManagedControlPlaneClassSpec) field.ErrorList { 879 var allErrs field.ErrorList 880 if old.SecurityProfile.Defender != nil { 881 if m.SecurityProfile == nil || m.SecurityProfile.Defender == nil { 882 allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "securityProfile", "defender"), 883 nil, "cannot unset Spec.SecurityProfile.Defender, to disable defender please set Spec.SecurityProfile.Defender.SecurityMonitoring.Enabled to false")) 884 } 885 } 886 return allErrs 887 } 888 889 // validateOIDCIssuerProfile validates an OIDCIssuerProfile. 890 func (m *AzureManagedControlPlane) validateOIDCIssuerProfileUpdate(old *AzureManagedControlPlane) field.ErrorList { 891 var allErrs field.ErrorList 892 if m.Spec.OIDCIssuerProfile != nil && old.Spec.OIDCIssuerProfile != nil { 893 if m.Spec.OIDCIssuerProfile.Enabled != nil && old.Spec.OIDCIssuerProfile.Enabled != nil && 894 !*m.Spec.OIDCIssuerProfile.Enabled && *old.Spec.OIDCIssuerProfile.Enabled { 895 allErrs = append(allErrs, 896 field.Forbidden( 897 field.NewPath("spec", "oidcIssuerProfile", "enabled"), 898 "cannot be disabled", 899 ), 900 ) 901 } 902 } 903 return allErrs 904 } 905 906 // validateFleetsMemberUpdate validates a FleetsMember. 907 func (m *AzureManagedControlPlane) validateFleetsMemberUpdate(old *AzureManagedControlPlane) field.ErrorList { 908 var allErrs field.ErrorList 909 910 if old.Spec.FleetsMember == nil || m.Spec.FleetsMember == nil { 911 return allErrs 912 } 913 if old.Spec.FleetsMember.Name != "" && old.Spec.FleetsMember.Name != m.Spec.FleetsMember.Name { 914 allErrs = append(allErrs, 915 field.Forbidden( 916 field.NewPath("spec", "fleetsMember", "name"), 917 "Name is immutable", 918 ), 919 ) 920 } 921 922 return allErrs 923 } 924 925 // validateAKSExtensionsUpdate validates update to AKS extensions. 926 func validateAKSExtensionsUpdate(old []AKSExtension, current []AKSExtension) field.ErrorList { 927 var allErrs field.ErrorList 928 929 oldAKSExtensionsMap := make(map[string]AKSExtension, len(old)) 930 oldAKSExtensionsIndex := make(map[string]int, len(old)) 931 for i, extension := range old { 932 oldAKSExtensionsMap[extension.Name] = extension 933 oldAKSExtensionsIndex[extension.Name] = i 934 } 935 for i, extension := range current { 936 oldExtension, ok := oldAKSExtensionsMap[extension.Name] 937 if !ok { 938 continue 939 } 940 if extension.Name != oldExtension.Name { 941 allErrs = append(allErrs, 942 field.Invalid( 943 field.NewPath("spec", "extensions", fmt.Sprintf("[%d]", i), "name"), 944 extension.Name, 945 "field is immutable", 946 ), 947 ) 948 } 949 if (oldExtension.ExtensionType != nil && extension.ExtensionType != nil) && *extension.ExtensionType != *oldExtension.ExtensionType { 950 allErrs = append(allErrs, 951 field.Invalid( 952 field.NewPath("spec", "extensions", fmt.Sprintf("[%d]", i), "extensionType"), 953 extension.ExtensionType, 954 "field is immutable", 955 ), 956 ) 957 } 958 if (extension.Plan != nil && oldExtension.Plan != nil) && *extension.Plan != *oldExtension.Plan { 959 allErrs = append(allErrs, 960 field.Invalid( 961 field.NewPath("spec", "extensions", fmt.Sprintf("[%d]", i), "plan"), 962 extension.Plan, 963 "field is immutable", 964 ), 965 ) 966 } 967 if extension.Scope != oldExtension.Scope { 968 allErrs = append(allErrs, 969 field.Invalid( 970 field.NewPath("spec", "extensions", fmt.Sprintf("[%d]", i), "scope"), 971 extension.Scope, 972 "field is immutable", 973 ), 974 ) 975 } 976 if (extension.ReleaseTrain != nil && oldExtension.ReleaseTrain != nil) && *extension.ReleaseTrain != *oldExtension.ReleaseTrain { 977 allErrs = append(allErrs, 978 field.Invalid( 979 field.NewPath("spec", "extensions", fmt.Sprintf("[%d]", i), "releaseTrain"), 980 extension.ReleaseTrain, 981 "field is immutable", 982 ), 983 ) 984 } 985 if (extension.Version != nil && oldExtension.Version != nil) && *extension.Version != *oldExtension.Version { 986 allErrs = append(allErrs, 987 field.Invalid( 988 field.NewPath("spec", "extensions", fmt.Sprintf("[%d]", i), "version"), 989 extension.Version, 990 "field is immutable", 991 ), 992 ) 993 } 994 if extension.Identity != oldExtension.Identity { 995 allErrs = append(allErrs, 996 field.Invalid( 997 field.NewPath("spec", "extensions", fmt.Sprintf("[%d]", i), "identity"), 998 extension.Identity, 999 "field is immutable", 1000 ), 1001 ) 1002 } 1003 } 1004 1005 return allErrs 1006 } 1007 1008 func validateName(name string, fldPath *field.Path) field.ErrorList { 1009 var allErrs field.ErrorList 1010 if lName := strings.ToLower(name); strings.Contains(lName, "microsoft") || 1011 strings.Contains(lName, "windows") { 1012 allErrs = append(allErrs, field.Invalid(fldPath.Child("Name"), name, 1013 "cluster name is invalid because 'MICROSOFT' and 'WINDOWS' can't be used as either a whole word or a substring in the name")) 1014 } 1015 1016 return allErrs 1017 } 1018 1019 // validateAKSExtensions validates the AKS extensions. 1020 func validateAKSExtensions(extensions []AKSExtension, fldPath *field.Path) field.ErrorList { 1021 var allErrs field.ErrorList 1022 for _, extension := range extensions { 1023 if extension.Version != nil && (extension.AutoUpgradeMinorVersion == nil || (extension.AutoUpgradeMinorVersion != nil && *extension.AutoUpgradeMinorVersion)) { 1024 allErrs = append(allErrs, field.Forbidden(fldPath.Child("Version"), "Version must not be given if AutoUpgradeMinorVersion is true (or not provided, as it is true by default)")) 1025 } 1026 if extension.AutoUpgradeMinorVersion == ptr.To(false) && extension.ReleaseTrain != nil { 1027 allErrs = append(allErrs, field.Forbidden(fldPath.Child("ReleaseTrain"), "ReleaseTrain must not be given if AutoUpgradeMinorVersion is false")) 1028 } 1029 if extension.Scope != nil { 1030 if extension.Scope.ScopeType == ExtensionScopeCluster { 1031 if extension.Scope.ReleaseNamespace == "" { 1032 allErrs = append(allErrs, field.Required(fldPath.Child("Scope", "ReleaseNamespace"), "ReleaseNamespace must be provided if Scope is Cluster")) 1033 } 1034 if extension.Scope.TargetNamespace != "" { 1035 allErrs = append(allErrs, field.Forbidden(fldPath.Child("Scope", "TargetNamespace"), "TargetNamespace can only be given if Scope is Namespace")) 1036 } 1037 } else if extension.Scope.ScopeType == ExtensionScopeNamespace { 1038 if extension.Scope.TargetNamespace == "" { 1039 allErrs = append(allErrs, field.Required(fldPath.Child("Scope", "TargetNamespace"), "TargetNamespace must be provided if Scope is Namespace")) 1040 } 1041 if extension.Scope.ReleaseNamespace != "" { 1042 allErrs = append(allErrs, field.Forbidden(fldPath.Child("Scope", "ReleaseNamespace"), "ReleaseNamespace can only be given if Scope is Cluster")) 1043 } 1044 } 1045 } 1046 } 1047 1048 return allErrs 1049 } 1050 1051 // validateNetworkPolicy validates the networkPolicy. 1052 func validateNetworkPolicy(networkPolicy *string, networkDataplane *NetworkDataplaneType, fldPath *field.Path) field.ErrorList { 1053 var allErrs field.ErrorList 1054 1055 if networkPolicy == nil { 1056 return nil 1057 } 1058 1059 if *networkPolicy == "cilium" && networkDataplane != nil && *networkDataplane != NetworkDataplaneTypeCilium { 1060 allErrs = append(allErrs, field.Invalid(fldPath, networkPolicy, "cilium network policy can only be used with cilium network dataplane")) 1061 } 1062 1063 return allErrs 1064 } 1065 1066 // validateNetworkDataplane validates the NetworkDataplane. 1067 func validateNetworkDataplane(networkDataplane *NetworkDataplaneType, networkPolicy *string, networkPluginMode *NetworkPluginMode, fldPath *field.Path) field.ErrorList { 1068 var allErrs field.ErrorList 1069 1070 if networkDataplane == nil { 1071 return nil 1072 } 1073 1074 if *networkDataplane == NetworkDataplaneTypeCilium && (networkPluginMode == nil || *networkPluginMode != NetworkPluginModeOverlay) { 1075 allErrs = append(allErrs, field.Invalid(fldPath, networkDataplane, "cilium network dataplane can only be used with overlay network plugin mode")) 1076 } 1077 if *networkDataplane == NetworkDataplaneTypeCilium && (networkPolicy == nil || *networkPolicy != "cilium") { 1078 allErrs = append(allErrs, field.Invalid(fldPath, networkDataplane, "cilium dataplane requires network policy cilium.")) 1079 } 1080 1081 return allErrs 1082 } 1083 1084 // validateAutoScalerProfile validates an AutoScalerProfile. 1085 func validateAutoScalerProfile(autoScalerProfile *AutoScalerProfile, fldPath *field.Path) field.ErrorList { 1086 var allErrs field.ErrorList 1087 1088 if autoScalerProfile == nil { 1089 return nil 1090 } 1091 1092 if errs := validateIntegerStringGreaterThanZero(autoScalerProfile.MaxEmptyBulkDelete, fldPath, "MaxEmptyBulkDelete"); len(errs) > 0 { 1093 allErrs = append(allErrs, errs...) 1094 } 1095 1096 if errs := validateIntegerStringGreaterThanZero(autoScalerProfile.MaxGracefulTerminationSec, fldPath, "MaxGracefulTerminationSec"); len(errs) > 0 { 1097 allErrs = append(allErrs, errs...) 1098 } 1099 1100 if errs := validateMaxNodeProvisionTime(autoScalerProfile.MaxNodeProvisionTime, fldPath); len(errs) > 0 { 1101 allErrs = append(allErrs, errs...) 1102 } 1103 1104 if autoScalerProfile.MaxTotalUnreadyPercentage != nil { 1105 val, err := strconv.Atoi(*autoScalerProfile.MaxTotalUnreadyPercentage) 1106 if err != nil || val < 0 || val > 100 { 1107 allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "autoscalerProfile", "maxTotalUnreadyPercentage"), autoScalerProfile.MaxTotalUnreadyPercentage, "invalid value")) 1108 } 1109 } 1110 1111 if errs := validateNewPodScaleUpDelay(autoScalerProfile.NewPodScaleUpDelay, fldPath); len(errs) > 0 { 1112 allErrs = append(allErrs, errs...) 1113 } 1114 1115 if errs := validateIntegerStringGreaterThanZero(autoScalerProfile.OkTotalUnreadyCount, fldPath, "okTotalUnreadyCount"); len(errs) > 0 { 1116 allErrs = append(allErrs, errs...) 1117 } 1118 1119 if errs := validateScanInterval(autoScalerProfile.ScanInterval, fldPath); len(errs) > 0 { 1120 allErrs = append(allErrs, errs...) 1121 } 1122 1123 if errs := validateScaleDownTime(autoScalerProfile.ScaleDownDelayAfterAdd, fldPath, "scaleDownDelayAfterAdd"); len(errs) > 0 { 1124 allErrs = append(allErrs, errs...) 1125 } 1126 1127 if errs := validateScaleDownDelayAfterDelete(autoScalerProfile.ScaleDownDelayAfterDelete, fldPath); len(errs) > 0 { 1128 allErrs = append(allErrs, errs...) 1129 } 1130 1131 if errs := validateScaleDownTime(autoScalerProfile.ScaleDownDelayAfterFailure, fldPath, "scaleDownDelayAfterFailure"); len(errs) > 0 { 1132 allErrs = append(allErrs, errs...) 1133 } 1134 1135 if errs := validateScaleDownTime(autoScalerProfile.ScaleDownUnneededTime, fldPath, "scaleDownUnneededTime"); len(errs) > 0 { 1136 allErrs = append(allErrs, errs...) 1137 } 1138 1139 if errs := validateScaleDownTime(autoScalerProfile.ScaleDownUnreadyTime, fldPath, "scaleDownUnreadyTime"); len(errs) > 0 { 1140 allErrs = append(allErrs, errs...) 1141 } 1142 1143 if autoScalerProfile.ScaleDownUtilizationThreshold != nil { 1144 val, err := strconv.ParseFloat(*autoScalerProfile.ScaleDownUtilizationThreshold, 32) 1145 if err != nil || val < 0 || val > 1 { 1146 allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "autoscalerProfile", "scaleDownUtilizationThreshold"), autoScalerProfile.ScaleDownUtilizationThreshold, "invalid value")) 1147 } 1148 } 1149 1150 return allErrs 1151 } 1152 1153 // validateMaxNodeProvisionTime validates update to AutoscalerProfile.MaxNodeProvisionTime. 1154 func validateMaxNodeProvisionTime(maxNodeProvisionTime *string, fldPath *field.Path) field.ErrorList { 1155 var allErrs field.ErrorList 1156 if ptr.Deref(maxNodeProvisionTime, "") != "" { 1157 if !rMaxNodeProvisionTime.MatchString(ptr.Deref(maxNodeProvisionTime, "")) { 1158 allErrs = append(allErrs, field.Invalid(fldPath.Child("MaxNodeProvisionTime"), maxNodeProvisionTime, "invalid value")) 1159 } 1160 } 1161 return allErrs 1162 } 1163 1164 // validateScanInterval validates update to AutoscalerProfile.ScanInterval. 1165 func validateScanInterval(scanInterval *string, fldPath *field.Path) field.ErrorList { 1166 var allErrs field.ErrorList 1167 if ptr.Deref(scanInterval, "") != "" { 1168 if !rScanInterval.MatchString(ptr.Deref(scanInterval, "")) { 1169 allErrs = append(allErrs, field.Invalid(fldPath.Child("ScanInterval"), scanInterval, "invalid value")) 1170 } 1171 } 1172 return allErrs 1173 } 1174 1175 // validateNewPodScaleUpDelay validates update to AutoscalerProfile.NewPodScaleUpDelay. 1176 func validateNewPodScaleUpDelay(newPodScaleUpDelay *string, fldPath *field.Path) field.ErrorList { 1177 var allErrs field.ErrorList 1178 if ptr.Deref(newPodScaleUpDelay, "") != "" { 1179 _, err := time.ParseDuration(ptr.Deref(newPodScaleUpDelay, "")) 1180 if err != nil { 1181 allErrs = append(allErrs, field.Invalid(fldPath.Child("NewPodScaleUpDelay"), newPodScaleUpDelay, "invalid value")) 1182 } 1183 } 1184 return allErrs 1185 } 1186 1187 // validateScaleDownDelayAfterDelete validates update to AutoscalerProfile.ScaleDownDelayAfterDelete value. 1188 func validateScaleDownDelayAfterDelete(scaleDownDelayAfterDelete *string, fldPath *field.Path) field.ErrorList { 1189 var allErrs field.ErrorList 1190 if ptr.Deref(scaleDownDelayAfterDelete, "") != "" { 1191 if !rScaleDownDelayAfterDelete.MatchString(ptr.Deref(scaleDownDelayAfterDelete, "")) { 1192 allErrs = append(allErrs, field.Invalid(fldPath.Child("ScaleDownDelayAfterDelete"), ptr.Deref(scaleDownDelayAfterDelete, ""), "invalid value")) 1193 } 1194 } 1195 return allErrs 1196 } 1197 1198 // validateScaleDownTime validates update to AutoscalerProfile.ScaleDown* values. 1199 func validateScaleDownTime(scaleDownValue *string, fldPath *field.Path, fieldName string) field.ErrorList { 1200 var allErrs field.ErrorList 1201 if ptr.Deref(scaleDownValue, "") != "" { 1202 if !rScaleDownTime.MatchString(ptr.Deref(scaleDownValue, "")) { 1203 allErrs = append(allErrs, field.Invalid(fldPath.Child(fieldName), ptr.Deref(scaleDownValue, ""), "invalid value")) 1204 } 1205 } 1206 return allErrs 1207 } 1208 1209 // validateIntegerStringGreaterThanZero validates that a string value is an integer greater than zero. 1210 func validateIntegerStringGreaterThanZero(input *string, fldPath *field.Path, fieldName string) field.ErrorList { 1211 var allErrs field.ErrorList 1212 1213 if input != nil { 1214 val, err := strconv.Atoi(*input) 1215 if err != nil || val < 0 { 1216 allErrs = append(allErrs, field.Invalid(fldPath.Child(fieldName), input, "invalid value")) 1217 } 1218 } 1219 1220 return allErrs 1221 } 1222 1223 // validateIdentity validates an Identity. 1224 func (m *AzureManagedControlPlane) validateIdentity(_ client.Client) field.ErrorList { 1225 var allErrs field.ErrorList 1226 1227 if m.Spec.Identity != nil { 1228 if m.Spec.Identity.Type == ManagedControlPlaneIdentityTypeUserAssigned { 1229 if m.Spec.Identity.UserAssignedIdentityResourceID == "" { 1230 allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "identity", "userAssignedIdentityResourceID"), m.Spec.Identity.UserAssignedIdentityResourceID, "cannot be empty if Identity.Type is UserAssigned")) 1231 } 1232 } else { 1233 if m.Spec.Identity.UserAssignedIdentityResourceID != "" { 1234 allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "identity", "userAssignedIdentityResourceID"), m.Spec.Identity.UserAssignedIdentityResourceID, "should be empty if Identity.Type is SystemAssigned")) 1235 } 1236 } 1237 } 1238 1239 if len(allErrs) > 0 { 1240 return allErrs 1241 } 1242 1243 return nil 1244 } 1245 1246 // validateNetworkPluginMode validates a NetworkPluginMode. 1247 func (m *AzureManagedControlPlane) validateNetworkPluginMode(_ client.Client) field.ErrorList { 1248 var allErrs field.ErrorList 1249 1250 const kubenet = "kubenet" 1251 if ptr.Deref(m.Spec.NetworkPluginMode, "") == NetworkPluginModeOverlay && 1252 ptr.Deref(m.Spec.NetworkPlugin, "") == kubenet { 1253 allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "networkPluginMode"), m.Spec.NetworkPluginMode, fmt.Sprintf("cannot be set to %q when NetworkPlugin is %q", NetworkPluginModeOverlay, kubenet))) 1254 } 1255 1256 if len(allErrs) > 0 { 1257 return allErrs 1258 } 1259 1260 return nil 1261 } 1262 1263 // isOIDCEnabled return true if OIDC issuer is enabled. 1264 func (m *AzureManagedControlPlaneClassSpec) isOIDCEnabled() bool { 1265 if m.OIDCIssuerProfile == nil { 1266 return false 1267 } 1268 if m.OIDCIssuerProfile.Enabled == nil { 1269 return false 1270 } 1271 return *m.OIDCIssuerProfile.Enabled 1272 } 1273 1274 // isUserManagedIdentityEnabled checks if user assigned identity is set. 1275 func (m *AzureManagedControlPlaneClassSpec) isUserManagedIdentityEnabled() bool { 1276 if m.Identity == nil { 1277 return false 1278 } 1279 if m.Identity.Type != ManagedControlPlaneIdentityTypeUserAssigned { 1280 return false 1281 } 1282 return true 1283 }