sigs.k8s.io/cluster-api-provider-azure@v1.14.3/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.validateFleetsMember(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 return allErrs.ToAggregate() 296 } 297 298 func (m *AzureManagedControlPlane) validateDNSPrefix(_ client.Client) field.ErrorList { 299 if m.Spec.DNSPrefix == nil { 300 return nil 301 } 302 303 // Regex pattern for DNS prefix validation 304 // 1. Between 1 and 54 characters long: {1,54} 305 // 2. Alphanumerics and hyphens: [a-zA-Z0-9-] 306 // 3. Start and end with alphanumeric: ^[a-zA-Z0-9].*[a-zA-Z0-9]$ 307 pattern := `^[a-zA-Z0-9][a-zA-Z0-9-]{0,52}[a-zA-Z0-9]$` 308 regex := regexp.MustCompile(pattern) 309 if regex.MatchString(ptr.Deref(m.Spec.DNSPrefix, "")) { 310 return nil 311 } 312 allErrs := field.ErrorList{ 313 field.Invalid(field.NewPath("Spec", "DNSPrefix"), *m.Spec.DNSPrefix, "DNSPrefix is invalid, does not match regex: "+pattern), 314 } 315 return allErrs 316 } 317 318 // validateSecurityProfile validates SecurityProfile. 319 func (m *AzureManagedControlPlaneClassSpec) validateSecurityProfile() field.ErrorList { 320 allErrs := field.ErrorList{} 321 if err := m.validateAzureKeyVaultKms(); err != nil { 322 allErrs = append(allErrs, err...) 323 } 324 if err := m.validateWorkloadIdentity(); err != nil { 325 allErrs = append(allErrs, err...) 326 } 327 return allErrs 328 } 329 330 // validateAzureKeyVaultKms validates AzureKeyVaultKms. 331 func (m *AzureManagedControlPlaneClassSpec) validateAzureKeyVaultKms() field.ErrorList { 332 if m.SecurityProfile != nil && m.SecurityProfile.AzureKeyVaultKms != nil { 333 if !m.isUserManagedIdentityEnabled() { 334 allErrs := field.ErrorList{ 335 field.Invalid(field.NewPath("Spec", "SecurityProfile", "AzureKeyVaultKms", "KeyVaultResourceID"), 336 m.SecurityProfile.AzureKeyVaultKms.KeyVaultResourceID, 337 "Spec.SecurityProfile.AzureKeyVaultKms can be set only when Spec.Identity.Type is UserAssigned"), 338 } 339 return allErrs 340 } 341 keyVaultNetworkAccess := ptr.Deref(m.SecurityProfile.AzureKeyVaultKms.KeyVaultNetworkAccess, KeyVaultNetworkAccessTypesPublic) 342 keyVaultResourceID := ptr.Deref(m.SecurityProfile.AzureKeyVaultKms.KeyVaultResourceID, "") 343 if keyVaultNetworkAccess == KeyVaultNetworkAccessTypesPrivate && keyVaultResourceID == "" { 344 allErrs := field.ErrorList{ 345 field.Invalid(field.NewPath("Spec", "SecurityProfile", "AzureKeyVaultKms", "KeyVaultResourceID"), 346 m.SecurityProfile.AzureKeyVaultKms.KeyVaultResourceID, 347 "Spec.SecurityProfile.AzureKeyVaultKms.KeyVaultResourceID cannot be empty when Spec.SecurityProfile.AzureKeyVaultKms.KeyVaultNetworkAccess is Private"), 348 } 349 return allErrs 350 } 351 if keyVaultNetworkAccess == KeyVaultNetworkAccessTypesPublic && keyVaultResourceID != "" { 352 allErrs := field.ErrorList{ 353 field.Invalid(field.NewPath("Spec", "SecurityProfile", "AzureKeyVaultKms", "KeyVaultResourceID"), m.SecurityProfile.AzureKeyVaultKms.KeyVaultResourceID, 354 "Spec.SecurityProfile.AzureKeyVaultKms.KeyVaultResourceID should be empty when Spec.SecurityProfile.AzureKeyVaultKms.KeyVaultNetworkAccess is Public"), 355 } 356 return allErrs 357 } 358 } 359 return nil 360 } 361 362 // validateWorkloadIdentity validates WorkloadIdentity. 363 func (m *AzureManagedControlPlaneClassSpec) validateWorkloadIdentity() field.ErrorList { 364 if m.SecurityProfile != nil && m.SecurityProfile.WorkloadIdentity != nil && !m.isOIDCEnabled() { 365 allErrs := field.ErrorList{ 366 field.Invalid(field.NewPath("Spec", "SecurityProfile", "WorkloadIdentity"), m.SecurityProfile.WorkloadIdentity, 367 "Spec.SecurityProfile.WorkloadIdentity cannot be enabled when Spec.OIDCIssuerProfile is disabled"), 368 } 369 return allErrs 370 } 371 return nil 372 } 373 374 // validateDisableLocalAccounts disabling local accounts for AAD based clusters. 375 func (m *AzureManagedControlPlane) validateDisableLocalAccounts(_ client.Client) field.ErrorList { 376 if m.Spec.DisableLocalAccounts != nil && m.Spec.AADProfile == nil { 377 return field.ErrorList{ 378 field.Invalid(field.NewPath("Spec", "DisableLocalAccounts"), *m.Spec.DisableLocalAccounts, "DisableLocalAccounts should be set only for AAD enabled clusters"), 379 } 380 } 381 return nil 382 } 383 384 // validateVersion validates the Kubernetes version. 385 func validateVersion(version string, fldPath *field.Path) field.ErrorList { 386 var allErrs field.ErrorList 387 if !kubeSemver.MatchString(version) { 388 allErrs = append(allErrs, field.Invalid(fldPath, version, "must be a valid semantic version")) 389 } 390 391 return allErrs 392 } 393 394 // validateSSHKey validates an SSHKey. 395 func (m *AzureManagedControlPlane) validateSSHKey(_ client.Client) field.ErrorList { 396 if sshKey := m.Spec.SSHPublicKey; sshKey != nil && *sshKey != "" { 397 if errs := ValidateSSHKey(*sshKey, field.NewPath("sshKey")); len(errs) > 0 { 398 return errs 399 } 400 } 401 402 return nil 403 } 404 405 // validateLoadBalancerProfile validates a LoadBalancerProfile. 406 func validateLoadBalancerProfile(loadBalancerProfile *LoadBalancerProfile, fldPath *field.Path) field.ErrorList { 407 var allErrs field.ErrorList 408 if loadBalancerProfile != nil { 409 numOutboundIPTypes := 0 410 411 if loadBalancerProfile.ManagedOutboundIPs != nil { 412 if *loadBalancerProfile.ManagedOutboundIPs < 1 || *loadBalancerProfile.ManagedOutboundIPs > 100 { 413 allErrs = append(allErrs, field.Invalid(fldPath.Child("ManagedOutboundIPs"), *loadBalancerProfile.ManagedOutboundIPs, "value should be in between 1 and 100")) 414 } 415 } 416 417 if loadBalancerProfile.AllocatedOutboundPorts != nil { 418 if *loadBalancerProfile.AllocatedOutboundPorts < 0 || *loadBalancerProfile.AllocatedOutboundPorts > 64000 { 419 allErrs = append(allErrs, field.Invalid(fldPath.Child("AllocatedOutboundPorts"), *loadBalancerProfile.AllocatedOutboundPorts, "value should be in between 0 and 64000")) 420 } 421 } 422 423 if loadBalancerProfile.IdleTimeoutInMinutes != nil { 424 if *loadBalancerProfile.IdleTimeoutInMinutes < 4 || *loadBalancerProfile.IdleTimeoutInMinutes > 120 { 425 allErrs = append(allErrs, field.Invalid(fldPath.Child("IdleTimeoutInMinutes"), *loadBalancerProfile.IdleTimeoutInMinutes, "value should be in between 4 and 120")) 426 } 427 } 428 429 if loadBalancerProfile.ManagedOutboundIPs != nil { 430 numOutboundIPTypes++ 431 } 432 if len(loadBalancerProfile.OutboundIPPrefixes) > 0 { 433 numOutboundIPTypes++ 434 } 435 if len(loadBalancerProfile.OutboundIPs) > 0 { 436 numOutboundIPTypes++ 437 } 438 if numOutboundIPTypes > 1 { 439 allErrs = append(allErrs, field.Forbidden(fldPath, "load balancer profile must specify at most one of ManagedOutboundIPs, OutboundIPPrefixes and OutboundIPs")) 440 } 441 } 442 443 return allErrs 444 } 445 446 func validateAMCPVirtualNetwork(virtualNetwork ManagedControlPlaneVirtualNetwork, fldPath *field.Path) field.ErrorList { 447 var allErrs field.ErrorList 448 449 // VirtualNetwork and the CIDR blocks get defaulted in the defaulting webhook, so we can assume they are always set. 450 if !reflect.DeepEqual(virtualNetwork, ManagedControlPlaneVirtualNetwork{}) { 451 _, parentNet, vnetErr := net.ParseCIDR(virtualNetwork.CIDRBlock) 452 if vnetErr != nil { 453 allErrs = append(allErrs, field.Invalid(fldPath.Child("CIDRBlock"), virtualNetwork.CIDRBlock, "pre-existing virtual networks CIDR block is invalid")) 454 } 455 subnetIP, _, subnetErr := net.ParseCIDR(virtualNetwork.Subnet.CIDRBlock) 456 if subnetErr != nil { 457 allErrs = append(allErrs, field.Invalid(fldPath.Child("Subnet", "CIDRBlock"), virtualNetwork.CIDRBlock, "pre-existing subnets CIDR block is invalid")) 458 } 459 if vnetErr == nil && subnetErr == nil && !parentNet.Contains(subnetIP) { 460 allErrs = append(allErrs, field.Invalid(fldPath.Child("CIDRBlock"), virtualNetwork.CIDRBlock, "pre-existing virtual networks CIDR block should contain the subnet CIDR block")) 461 } 462 } 463 return allErrs 464 } 465 466 // validateAPIServerAccessProfile validates an APIServerAccessProfile. 467 func validateAPIServerAccessProfile(apiServerAccessProfile *APIServerAccessProfile, fldPath *field.Path) field.ErrorList { 468 var allErrs field.ErrorList 469 if apiServerAccessProfile != nil { 470 for _, ipRange := range apiServerAccessProfile.AuthorizedIPRanges { 471 if _, _, err := net.ParseCIDR(ipRange); err != nil { 472 allErrs = append(allErrs, field.Invalid(fldPath, ipRange, "invalid CIDR format")) 473 } 474 } 475 476 // privateDNSZone should either be "System" or "None" or the private dns zone name should be in either of these 477 // formats: 'private.<location>.azmk8s.io,privatelink.<location>.azmk8s.io,[a-zA-Z0-9-]{1,32}.private.<location>.azmk8s.io, 478 // [a-zA-Z0-9-]{1,32}.privatelink.<location>.azmk8s.io'. The validation below follows the guidelines mentioned at 479 // https://learn.microsoft.com/azure/aks/private-clusters?tabs=azure-portal#configure-a-private-dns-zone. 480 // Performing a lower case comparison to avoid case sensitivity. 481 if apiServerAccessProfile.PrivateDNSZone != nil { 482 privateDNSZone := strings.ToLower(ptr.Deref(apiServerAccessProfile.PrivateDNSZone, "")) 483 if !strings.EqualFold(strings.ToLower(privateDNSZone), "system") && 484 !strings.EqualFold(strings.ToLower(privateDNSZone), "none") { 485 // Extract substring starting from "privatednszones/" 486 startIndex := strings.Index(strings.ToLower(privateDNSZone), "privatednszones/") 487 if startIndex == -1 { 488 allErrs = append(allErrs, field.Invalid(fldPath, privateDNSZone, "invalid private DNS zone")) 489 return allErrs 490 } 491 492 // Private DNS Zones can only be used by private clusters. 493 if !ptr.Deref(apiServerAccessProfile.EnablePrivateCluster, false) { 494 allErrs = append(allErrs, field.Invalid(fldPath, apiServerAccessProfile.EnablePrivateCluster, "Private Cluster should be enabled to use PrivateDNSZone")) 495 return allErrs 496 } 497 498 extractedPrivateDNSZone := privateDNSZone[startIndex+len("privatednszones/"):] 499 500 patternWithLocation := `^(privatelink|private)\.[a-zA-Z0-9]+\.(azmk8s\.io)$` 501 locationRegex := regexp.MustCompile(patternWithLocation) 502 patternWithSubzone := `^[a-zA-Z0-9-]{1,32}\.(privatelink|private)\.[a-zA-Z0-9]+\.(azmk8s\.io)$` 503 subzoneRegex := regexp.MustCompile(patternWithSubzone) 504 505 // check if privateDNSZone is a valid resource ID 506 if !locationRegex.MatchString(extractedPrivateDNSZone) && !subzoneRegex.MatchString(extractedPrivateDNSZone) { 507 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'")) 508 } 509 } 510 } 511 } 512 return allErrs 513 } 514 515 // validateManagedClusterNetwork validates the Cluster network values. 516 func validateManagedClusterNetwork(cli client.Client, labels map[string]string, namespace string, dnsServiceIP *string, subnet ManagedControlPlaneSubnet, fldPath *field.Path) field.ErrorList { 517 var ( 518 allErrs field.ErrorList 519 serviceCIDR string 520 ) 521 522 ctx := context.Background() 523 524 // Fetch the Cluster. 525 clusterName, ok := labels[clusterv1.ClusterNameLabel] 526 if !ok { 527 return nil 528 } 529 530 ownerCluster := &clusterv1.Cluster{} 531 key := client.ObjectKey{ 532 Namespace: namespace, 533 Name: clusterName, 534 } 535 536 if err := cli.Get(ctx, key, ownerCluster); err != nil { 537 allErrs = append(allErrs, field.InternalError(field.NewPath("Cluster", "Spec", "ClusterNetwork"), err)) 538 return allErrs 539 } 540 541 if clusterNetwork := ownerCluster.Spec.ClusterNetwork; clusterNetwork != nil { 542 if clusterNetwork.Services != nil { 543 // A user may provide zero or one CIDR blocks. If they provide an empty array, 544 // we ignore it and use the default. AKS doesn't support > 1 Service/Pod CIDR. 545 if len(clusterNetwork.Services.CIDRBlocks) > 1 { 546 allErrs = append(allErrs, field.TooMany(field.NewPath("Cluster", "Spec", "ClusterNetwork", "Services", "CIDRBlocks"), len(clusterNetwork.Services.CIDRBlocks), 1)) 547 } 548 if len(clusterNetwork.Services.CIDRBlocks) == 1 { 549 serviceCIDR = clusterNetwork.Services.CIDRBlocks[0] 550 } 551 } 552 if clusterNetwork.Pods != nil { 553 // A user may provide zero or one CIDR blocks. If they provide an empty array, 554 // we ignore it and use the default. AKS doesn't support > 1 Service/Pod CIDR. 555 if len(clusterNetwork.Pods.CIDRBlocks) > 1 { 556 allErrs = append(allErrs, field.TooMany(field.NewPath("Cluster", "Spec", "ClusterNetwork", "Pods", "CIDRBlocks"), len(clusterNetwork.Pods.CIDRBlocks), 1)) 557 } 558 } 559 } 560 561 if dnsServiceIP != nil { 562 if serviceCIDR == "" { 563 allErrs = append(allErrs, field.Required(field.NewPath("Cluster", "Spec", "ClusterNetwork", "Services", "CIDRBlocks"), "service CIDR must be specified if specifying DNSServiceIP")) 564 } 565 _, cidr, err := net.ParseCIDR(serviceCIDR) 566 if err != nil { 567 allErrs = append(allErrs, field.Invalid(field.NewPath("Cluster", "Spec", "ClusterNetwork", "Services", "CIDRBlocks"), serviceCIDR, fmt.Sprintf("failed to parse cluster service cidr: %v", err))) 568 } 569 570 dnsIP := net.ParseIP(*dnsServiceIP) 571 if dnsIP == nil { // dnsIP will be nil if the string is not a valid IP 572 allErrs = append(allErrs, field.Invalid(field.NewPath("Cluster", "Spec", "ClusterNetwork", "Services", "DNSServiceIP"), *dnsServiceIP, "must be a valid IP address")) 573 } 574 575 if dnsIP != nil && !cidr.Contains(dnsIP) { 576 allErrs = append(allErrs, field.Invalid(field.NewPath("Cluster", "Spec", "ClusterNetwork", "Services", "CIDRBlocks"), serviceCIDR, "DNSServiceIP must reside within the associated cluster serviceCIDR")) 577 } 578 579 // AKS only supports .10 as the last octet for the DNSServiceIP. 580 // Refer to: https://learn.microsoft.com/en-us/azure/aks/configure-kubenet#create-an-aks-cluster-with-system-assigned-managed-identities 581 targetSuffix := ".10" 582 if dnsIP != nil && !strings.HasSuffix(dnsIP.String(), targetSuffix) { 583 allErrs = append(allErrs, field.Invalid(field.NewPath("Cluster", "Spec", "ClusterNetwork", "Services", "DNSServiceIP"), *dnsServiceIP, fmt.Sprintf("must end with %q", targetSuffix))) 584 } 585 } 586 587 if errs := validatePrivateEndpoints(subnet.PrivateEndpoints, []string{subnet.CIDRBlock}, fldPath.Child("VirtualNetwork.Subnet.PrivateEndpoints")); len(errs) > 0 { 588 allErrs = append(allErrs, errs...) 589 } 590 591 return allErrs 592 } 593 594 // validateAutoUpgradeProfile validates auto upgrade profile. 595 func (m *AzureManagedControlPlane) validateAutoUpgradeProfile(old *AzureManagedControlPlane) field.ErrorList { 596 var allErrs field.ErrorList 597 if old.Spec.AutoUpgradeProfile != nil { 598 if old.Spec.AutoUpgradeProfile.UpgradeChannel != nil && (m.Spec.AutoUpgradeProfile == nil || m.Spec.AutoUpgradeProfile.UpgradeChannel == nil) { 599 // Prevent AutoUpgradeProfile.UpgradeChannel to be set to nil. 600 // Unsetting the field is not allowed. 601 allErrs = append(allErrs, 602 field.Invalid( 603 field.NewPath("Spec", "AutoUpgradeProfile", "UpgradeChannel"), 604 old.Spec.AutoUpgradeProfile.UpgradeChannel, 605 "field cannot be set to nil, to disable auto upgrades set the channel to none.")) 606 } 607 } 608 return allErrs 609 } 610 611 // validateK8sVersionUpdate validates K8s version. 612 func (m *AzureManagedControlPlane) validateK8sVersionUpdate(old *AzureManagedControlPlane) field.ErrorList { 613 var allErrs field.ErrorList 614 if hv := versions.GetHigherK8sVersion(m.Spec.Version, old.Spec.Version); hv != m.Spec.Version { 615 allErrs = append(allErrs, field.Invalid(field.NewPath("Spec", "Version"), 616 m.Spec.Version, "field version cannot be downgraded"), 617 ) 618 } 619 620 if old.Status.AutoUpgradeVersion != "" && m.Spec.Version != old.Spec.Version { 621 if hv := versions.GetHigherK8sVersion(m.Spec.Version, old.Status.AutoUpgradeVersion); hv != m.Spec.Version { 622 allErrs = append(allErrs, field.Invalid(field.NewPath("Spec", "Version"), 623 m.Spec.Version, "version is auto-upgraded to "+old.Status.AutoUpgradeVersion+", cannot be downgraded"), 624 ) 625 } 626 } 627 return allErrs 628 } 629 630 // validateAPIServerAccessProfileUpdate validates update to APIServerAccessProfile. 631 func (m *AzureManagedControlPlane) validateAPIServerAccessProfileUpdate(old *AzureManagedControlPlane) field.ErrorList { 632 var allErrs field.ErrorList 633 634 newAPIServerAccessProfileNormalized := &APIServerAccessProfile{} 635 oldAPIServerAccessProfileNormalized := &APIServerAccessProfile{} 636 if m.Spec.APIServerAccessProfile != nil { 637 newAPIServerAccessProfileNormalized = &APIServerAccessProfile{ 638 APIServerAccessProfileClassSpec: APIServerAccessProfileClassSpec{ 639 EnablePrivateCluster: m.Spec.APIServerAccessProfile.EnablePrivateCluster, 640 PrivateDNSZone: m.Spec.APIServerAccessProfile.PrivateDNSZone, 641 EnablePrivateClusterPublicFQDN: m.Spec.APIServerAccessProfile.EnablePrivateClusterPublicFQDN, 642 }, 643 } 644 } 645 if old.Spec.APIServerAccessProfile != nil { 646 oldAPIServerAccessProfileNormalized = &APIServerAccessProfile{ 647 APIServerAccessProfileClassSpec: APIServerAccessProfileClassSpec{ 648 EnablePrivateCluster: old.Spec.APIServerAccessProfile.EnablePrivateCluster, 649 PrivateDNSZone: old.Spec.APIServerAccessProfile.PrivateDNSZone, 650 EnablePrivateClusterPublicFQDN: old.Spec.APIServerAccessProfile.EnablePrivateClusterPublicFQDN, 651 }, 652 } 653 } 654 655 if !reflect.DeepEqual(newAPIServerAccessProfileNormalized, oldAPIServerAccessProfileNormalized) { 656 allErrs = append(allErrs, 657 field.Invalid(field.NewPath("Spec", "APIServerAccessProfile"), 658 m.Spec.APIServerAccessProfile, "fields (except for AuthorizedIPRanges) are immutable"), 659 ) 660 } 661 662 return allErrs 663 } 664 665 // validateAddonProfilesUpdate validates update to AddonProfiles. 666 func (m *AzureManagedControlPlane) validateAddonProfilesUpdate(old *AzureManagedControlPlane) field.ErrorList { 667 var allErrs field.ErrorList 668 newAddonProfileMap := map[string]struct{}{} 669 if len(old.Spec.AddonProfiles) != 0 { 670 for _, addonProfile := range m.Spec.AddonProfiles { 671 newAddonProfileMap[addonProfile.Name] = struct{}{} 672 } 673 for i, addonProfile := range old.Spec.AddonProfiles { 674 if _, ok := newAddonProfileMap[addonProfile.Name]; !ok { 675 allErrs = append(allErrs, field.Invalid( 676 field.NewPath("Spec", "AddonProfiles"), 677 m.Spec.AddonProfiles, 678 fmt.Sprintf("cannot remove addonProfile %s, To disable this AddonProfile, update Spec.AddonProfiles[%v].Enabled to false", addonProfile.Name, i))) 679 } 680 } 681 } 682 return allErrs 683 } 684 685 // validateVirtualNetworkUpdate validates update to VirtualNetwork. 686 func (m *AzureManagedControlPlane) validateVirtualNetworkUpdate(old *AzureManagedControlPlane) field.ErrorList { 687 var allErrs field.ErrorList 688 if old.Spec.VirtualNetwork.Name != m.Spec.VirtualNetwork.Name { 689 allErrs = append(allErrs, 690 field.Invalid( 691 field.NewPath("Spec", "VirtualNetwork.Name"), 692 m.Spec.VirtualNetwork.Name, 693 "Virtual Network Name is immutable")) 694 } 695 696 if old.Spec.VirtualNetwork.CIDRBlock != m.Spec.VirtualNetwork.CIDRBlock { 697 allErrs = append(allErrs, 698 field.Invalid( 699 field.NewPath("Spec", "VirtualNetwork.CIDRBlock"), 700 m.Spec.VirtualNetwork.CIDRBlock, 701 "Virtual Network CIDRBlock is immutable")) 702 } 703 704 if old.Spec.VirtualNetwork.Subnet.Name != m.Spec.VirtualNetwork.Subnet.Name { 705 allErrs = append(allErrs, 706 field.Invalid( 707 field.NewPath("Spec", "VirtualNetwork.Subnet.Name"), 708 m.Spec.VirtualNetwork.Subnet.Name, 709 "Subnet Name is immutable")) 710 } 711 712 // NOTE: This only works because we force the user to set the CIDRBlock for both the 713 // managed and unmanaged Vnets. If we ever update the subnet cidr based on what's 714 // actually set in the subnet, and it is different from what's in the Spec, for 715 // unmanaged Vnets like we do with the AzureCluster this logic will break. 716 if old.Spec.VirtualNetwork.Subnet.CIDRBlock != m.Spec.VirtualNetwork.Subnet.CIDRBlock { 717 allErrs = append(allErrs, 718 field.Invalid( 719 field.NewPath("Spec", "VirtualNetwork.Subnet.CIDRBlock"), 720 m.Spec.VirtualNetwork.Subnet.CIDRBlock, 721 "Subnet CIDRBlock is immutable")) 722 } 723 724 if old.Spec.VirtualNetwork.ResourceGroup != m.Spec.VirtualNetwork.ResourceGroup { 725 allErrs = append(allErrs, 726 field.Invalid( 727 field.NewPath("Spec", "VirtualNetwork.ResourceGroup"), 728 m.Spec.VirtualNetwork.ResourceGroup, 729 "Virtual Network Resource Group is immutable")) 730 } 731 return allErrs 732 } 733 734 // validateNetworkPluginModeUpdate validates update to NetworkPluginMode. 735 func (m *AzureManagedControlPlane) validateNetworkPluginModeUpdate(old *AzureManagedControlPlane) field.ErrorList { 736 var allErrs field.ErrorList 737 738 if ptr.Deref(old.Spec.NetworkPluginMode, "") != NetworkPluginModeOverlay && 739 ptr.Deref(m.Spec.NetworkPluginMode, "") == NetworkPluginModeOverlay && 740 old.Spec.NetworkPolicy != nil { 741 allErrs = append(allErrs, field.Forbidden(field.NewPath("Spec", "NetworkPluginMode"), fmt.Sprintf("%q NetworkPluginMode cannot be enabled when NetworkPolicy is set", NetworkPluginModeOverlay))) 742 } 743 744 return allErrs 745 } 746 747 // validateAADProfileUpdateAndLocalAccounts validates updates for AADProfile. 748 func (m *AzureManagedControlPlane) validateAADProfileUpdateAndLocalAccounts(old *AzureManagedControlPlane) field.ErrorList { 749 var allErrs field.ErrorList 750 if old.Spec.AADProfile != nil { 751 if m.Spec.AADProfile == nil { 752 allErrs = append(allErrs, 753 field.Invalid( 754 field.NewPath("Spec", "AADProfile"), 755 m.Spec.AADProfile, 756 "field cannot be nil, cannot disable AADProfile")) 757 } else { 758 if !m.Spec.AADProfile.Managed && old.Spec.AADProfile.Managed { 759 allErrs = append(allErrs, 760 field.Invalid( 761 field.NewPath("Spec", "AADProfile.Managed"), 762 m.Spec.AADProfile.Managed, 763 "cannot set AADProfile.Managed to false")) 764 } 765 if len(m.Spec.AADProfile.AdminGroupObjectIDs) == 0 { 766 allErrs = append(allErrs, 767 field.Invalid( 768 field.NewPath("Spec", "AADProfile.AdminGroupObjectIDs"), 769 m.Spec.AADProfile.AdminGroupObjectIDs, 770 "length of AADProfile.AdminGroupObjectIDs cannot be zero")) 771 } 772 } 773 } 774 775 if old.Spec.DisableLocalAccounts == nil && 776 m.Spec.DisableLocalAccounts != nil && 777 m.Spec.AADProfile == nil { 778 allErrs = append(allErrs, 779 field.Invalid( 780 field.NewPath("Spec", "DisableLocalAccounts"), 781 m.Spec.DisableLocalAccounts, 782 "DisableLocalAccounts can be set only for AAD enabled clusters")) 783 } 784 785 if old.Spec.DisableLocalAccounts != nil { 786 // Prevent DisableLocalAccounts modification if it was already set to some value 787 if err := webhookutils.ValidateImmutable( 788 field.NewPath("Spec", "DisableLocalAccounts"), 789 m.Spec.DisableLocalAccounts, 790 old.Spec.DisableLocalAccounts, 791 ); err != nil { 792 allErrs = append(allErrs, err) 793 } 794 } 795 796 return allErrs 797 } 798 799 // validateSecurityProfileUpdate validates a SecurityProfile update. 800 func (m *AzureManagedControlPlaneClassSpec) validateSecurityProfileUpdate(old *AzureManagedControlPlaneClassSpec) field.ErrorList { 801 var allErrs field.ErrorList 802 if old.SecurityProfile != nil { 803 if errAzureKeyVaultKms := m.validateAzureKeyVaultKmsUpdate(old); errAzureKeyVaultKms != nil { 804 allErrs = append(allErrs, errAzureKeyVaultKms...) 805 } 806 if errWorkloadIdentity := m.validateWorkloadIdentityUpdate(old); errWorkloadIdentity != nil { 807 allErrs = append(allErrs, errWorkloadIdentity...) 808 } 809 if errWorkloadIdentity := m.validateImageCleanerUpdate(old); errWorkloadIdentity != nil { 810 allErrs = append(allErrs, errWorkloadIdentity...) 811 } 812 if errWorkloadIdentity := m.validateDefender(old); errWorkloadIdentity != nil { 813 allErrs = append(allErrs, errWorkloadIdentity...) 814 } 815 } 816 return allErrs 817 } 818 819 // validateAzureKeyVaultKmsUpdate validates AzureKeyVaultKmsUpdate profile. 820 func (m *AzureManagedControlPlaneClassSpec) validateAzureKeyVaultKmsUpdate(old *AzureManagedControlPlaneClassSpec) field.ErrorList { 821 var allErrs field.ErrorList 822 if old.SecurityProfile.AzureKeyVaultKms != nil { 823 if m.SecurityProfile == nil || m.SecurityProfile.AzureKeyVaultKms == nil { 824 allErrs = append(allErrs, field.Invalid(field.NewPath("Spec", "SecurityProfile", "AzureKeyVaultKms"), 825 nil, "cannot unset Spec.SecurityProfile.AzureKeyVaultKms profile to disable the profile please set Spec.SecurityProfile.AzureKeyVaultKms.Enabled to false")) 826 return allErrs 827 } 828 } 829 return allErrs 830 } 831 832 // validateWorkloadIdentityUpdate validates WorkloadIdentityUpdate profile. 833 func (m *AzureManagedControlPlaneClassSpec) validateWorkloadIdentityUpdate(old *AzureManagedControlPlaneClassSpec) field.ErrorList { 834 var allErrs field.ErrorList 835 if old.SecurityProfile.WorkloadIdentity != nil { 836 if m.SecurityProfile == nil || m.SecurityProfile.WorkloadIdentity == nil { 837 allErrs = append(allErrs, field.Invalid(field.NewPath("Spec", "SecurityProfile", "WorkloadIdentity"), 838 nil, "cannot unset Spec.SecurityProfile.WorkloadIdentity, to disable workloadIdentity please set Spec.SecurityProfile.WorkloadIdentity.Enabled to false")) 839 } 840 } 841 return allErrs 842 } 843 844 // validateImageCleanerUpdate validates ImageCleanerUpdate profile. 845 func (m *AzureManagedControlPlaneClassSpec) validateImageCleanerUpdate(old *AzureManagedControlPlaneClassSpec) field.ErrorList { 846 var allErrs field.ErrorList 847 if old.SecurityProfile.ImageCleaner != nil { 848 if m.SecurityProfile == nil || m.SecurityProfile.ImageCleaner == nil { 849 allErrs = append(allErrs, field.Invalid(field.NewPath("Spec", "SecurityProfile", "ImageCleaner"), 850 nil, "cannot unset Spec.SecurityProfile.ImageCleaner, to disable imageCleaner please set Spec.SecurityProfile.ImageCleaner.Enabled to false")) 851 } 852 } 853 return allErrs 854 } 855 856 // validateDefender validates defender profile. 857 func (m *AzureManagedControlPlaneClassSpec) validateDefender(old *AzureManagedControlPlaneClassSpec) field.ErrorList { 858 var allErrs field.ErrorList 859 if old.SecurityProfile.Defender != nil { 860 if m.SecurityProfile == nil || m.SecurityProfile.Defender == nil { 861 allErrs = append(allErrs, field.Invalid(field.NewPath("Spec", "SecurityProfile", "Defender"), 862 nil, "cannot unset Spec.SecurityProfile.Defender, to disable defender please set Spec.SecurityProfile.Defender.SecurityMonitoring.Enabled to false")) 863 } 864 } 865 return allErrs 866 } 867 868 // validateOIDCIssuerProfile validates an OIDCIssuerProfile. 869 func (m *AzureManagedControlPlane) validateOIDCIssuerProfileUpdate(old *AzureManagedControlPlane) field.ErrorList { 870 var allErrs field.ErrorList 871 if m.Spec.OIDCIssuerProfile != nil && old.Spec.OIDCIssuerProfile != nil { 872 if m.Spec.OIDCIssuerProfile.Enabled != nil && old.Spec.OIDCIssuerProfile.Enabled != nil && 873 !*m.Spec.OIDCIssuerProfile.Enabled && *old.Spec.OIDCIssuerProfile.Enabled { 874 allErrs = append(allErrs, 875 field.Forbidden( 876 field.NewPath("Spec", "OIDCIssuerProfile", "Enabled"), 877 "cannot be disabled", 878 ), 879 ) 880 } 881 } 882 return allErrs 883 } 884 885 // validateFleetsMember validates a FleetsMember. 886 func (m *AzureManagedControlPlane) validateFleetsMember(old *AzureManagedControlPlane) field.ErrorList { 887 var allErrs field.ErrorList 888 889 if old.Spec.FleetsMember == nil || m.Spec.FleetsMember == nil { 890 return allErrs 891 } 892 if old.Spec.FleetsMember.Name != "" && old.Spec.FleetsMember.Name != m.Spec.FleetsMember.Name { 893 allErrs = append(allErrs, 894 field.Forbidden( 895 field.NewPath("Spec", "FleetsMember", "Name"), 896 "Name is immutable", 897 ), 898 ) 899 } 900 901 return allErrs 902 } 903 904 // validateAKSExtensionsUpdate validates update to AKS extensions. 905 func validateAKSExtensionsUpdate(old []AKSExtension, current []AKSExtension) field.ErrorList { 906 var allErrs field.ErrorList 907 908 oldAKSExtensionsMap := make(map[string]AKSExtension, len(old)) 909 oldAKSExtensionsIndex := make(map[string]int, len(old)) 910 for i, extension := range old { 911 oldAKSExtensionsMap[extension.Name] = extension 912 oldAKSExtensionsIndex[extension.Name] = i 913 } 914 for i, extension := range current { 915 oldExtension, ok := oldAKSExtensionsMap[extension.Name] 916 if !ok { 917 continue 918 } 919 if extension.Name != oldExtension.Name { 920 allErrs = append(allErrs, 921 field.Invalid( 922 field.NewPath("Spec", "Extensions", fmt.Sprintf("[%d]", i), "Name"), 923 extension.Name, 924 "field is immutable", 925 ), 926 ) 927 } 928 if (oldExtension.ExtensionType != nil && extension.ExtensionType != nil) && *extension.ExtensionType != *oldExtension.ExtensionType { 929 allErrs = append(allErrs, 930 field.Invalid( 931 field.NewPath("Spec", "Extensions", fmt.Sprintf("[%d]", i), "ExtensionType"), 932 extension.ExtensionType, 933 "field is immutable", 934 ), 935 ) 936 } 937 if (extension.Plan != nil && oldExtension.Plan != nil) && *extension.Plan != *oldExtension.Plan { 938 allErrs = append(allErrs, 939 field.Invalid( 940 field.NewPath("Spec", "Extensions", fmt.Sprintf("[%d]", i), "Plan"), 941 extension.Plan, 942 "field is immutable", 943 ), 944 ) 945 } 946 if extension.Scope != oldExtension.Scope { 947 allErrs = append(allErrs, 948 field.Invalid( 949 field.NewPath("Spec", "Extensions", fmt.Sprintf("[%d]", i), "Scope"), 950 extension.Scope, 951 "field is immutable", 952 ), 953 ) 954 } 955 if (extension.ReleaseTrain != nil && oldExtension.ReleaseTrain != nil) && *extension.ReleaseTrain != *oldExtension.ReleaseTrain { 956 allErrs = append(allErrs, 957 field.Invalid( 958 field.NewPath("Spec", "Extensions", fmt.Sprintf("[%d]", i), "ReleaseTrain"), 959 extension.ReleaseTrain, 960 "field is immutable", 961 ), 962 ) 963 } 964 if (extension.Version != nil && oldExtension.Version != nil) && *extension.Version != *oldExtension.Version { 965 allErrs = append(allErrs, 966 field.Invalid( 967 field.NewPath("Spec", "Extensions", fmt.Sprintf("[%d]", i), "Version"), 968 extension.Version, 969 "field is immutable", 970 ), 971 ) 972 } 973 if extension.Identity != oldExtension.Identity { 974 allErrs = append(allErrs, 975 field.Invalid( 976 field.NewPath("Spec", "Extensions", fmt.Sprintf("[%d]", i), "Identity"), 977 extension.Identity, 978 "field is immutable", 979 ), 980 ) 981 } 982 } 983 984 return allErrs 985 } 986 987 func validateName(name string, fldPath *field.Path) field.ErrorList { 988 var allErrs field.ErrorList 989 if lName := strings.ToLower(name); strings.Contains(lName, "microsoft") || 990 strings.Contains(lName, "windows") { 991 allErrs = append(allErrs, field.Invalid(fldPath.Child("Name"), name, 992 "cluster name is invalid because 'MICROSOFT' and 'WINDOWS' can't be used as either a whole word or a substring in the name")) 993 } 994 995 return allErrs 996 } 997 998 // validateAKSExtensions validates the AKS extensions. 999 func validateAKSExtensions(extensions []AKSExtension, fldPath *field.Path) field.ErrorList { 1000 var allErrs field.ErrorList 1001 for _, extension := range extensions { 1002 if extension.Version != nil && (extension.AutoUpgradeMinorVersion == nil || (extension.AutoUpgradeMinorVersion != nil && *extension.AutoUpgradeMinorVersion)) { 1003 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)")) 1004 } 1005 if extension.AutoUpgradeMinorVersion == ptr.To(false) && extension.ReleaseTrain != nil { 1006 allErrs = append(allErrs, field.Forbidden(fldPath.Child("ReleaseTrain"), "ReleaseTrain must not be given if AutoUpgradeMinorVersion is false")) 1007 } 1008 if extension.Scope != nil { 1009 if extension.Scope.ScopeType == ExtensionScopeCluster { 1010 if extension.Scope.ReleaseNamespace == "" { 1011 allErrs = append(allErrs, field.Required(fldPath.Child("Scope", "ReleaseNamespace"), "ReleaseNamespace must be provided if Scope is Cluster")) 1012 } 1013 if extension.Scope.TargetNamespace != "" { 1014 allErrs = append(allErrs, field.Forbidden(fldPath.Child("Scope", "TargetNamespace"), "TargetNamespace can only be given if Scope is Namespace")) 1015 } 1016 } else if extension.Scope.ScopeType == ExtensionScopeNamespace { 1017 if extension.Scope.TargetNamespace == "" { 1018 allErrs = append(allErrs, field.Required(fldPath.Child("Scope", "TargetNamespace"), "TargetNamespace must be provided if Scope is Namespace")) 1019 } 1020 if extension.Scope.ReleaseNamespace != "" { 1021 allErrs = append(allErrs, field.Forbidden(fldPath.Child("Scope", "ReleaseNamespace"), "ReleaseNamespace can only be given if Scope is Cluster")) 1022 } 1023 } 1024 } 1025 } 1026 1027 return allErrs 1028 } 1029 1030 // validateNetworkPolicy validates the networkPolicy. 1031 func validateNetworkPolicy(networkPolicy *string, networkDataplane *NetworkDataplaneType, fldPath *field.Path) field.ErrorList { 1032 var allErrs field.ErrorList 1033 1034 if networkPolicy == nil { 1035 return nil 1036 } 1037 1038 if *networkPolicy == "cilium" && networkDataplane != nil && *networkDataplane != NetworkDataplaneTypeCilium { 1039 allErrs = append(allErrs, field.Invalid(fldPath, networkPolicy, "cilium network policy can only be used with cilium network dataplane")) 1040 } 1041 1042 return allErrs 1043 } 1044 1045 // validateNetworkDataplane validates the NetworkDataplane. 1046 func validateNetworkDataplane(networkDataplane *NetworkDataplaneType, networkPolicy *string, networkPluginMode *NetworkPluginMode, fldPath *field.Path) field.ErrorList { 1047 var allErrs field.ErrorList 1048 1049 if networkDataplane == nil { 1050 return nil 1051 } 1052 1053 if *networkDataplane == NetworkDataplaneTypeCilium && (networkPluginMode == nil || *networkPluginMode != NetworkPluginModeOverlay) { 1054 allErrs = append(allErrs, field.Invalid(fldPath, networkDataplane, "cilium network dataplane can only be used with overlay network plugin mode")) 1055 } 1056 if *networkDataplane == NetworkDataplaneTypeCilium && (networkPolicy == nil || *networkPolicy != "cilium") { 1057 allErrs = append(allErrs, field.Invalid(fldPath, networkDataplane, "cilium dataplane requires network policy cilium.")) 1058 } 1059 1060 return allErrs 1061 } 1062 1063 // validateAutoScalerProfile validates an AutoScalerProfile. 1064 func validateAutoScalerProfile(autoScalerProfile *AutoScalerProfile, fldPath *field.Path) field.ErrorList { 1065 var allErrs field.ErrorList 1066 1067 if autoScalerProfile == nil { 1068 return nil 1069 } 1070 1071 if errs := validateIntegerStringGreaterThanZero(autoScalerProfile.MaxEmptyBulkDelete, fldPath, "MaxEmptyBulkDelete"); len(errs) > 0 { 1072 allErrs = append(allErrs, errs...) 1073 } 1074 1075 if errs := validateIntegerStringGreaterThanZero(autoScalerProfile.MaxGracefulTerminationSec, fldPath, "MaxGracefulTerminationSec"); len(errs) > 0 { 1076 allErrs = append(allErrs, errs...) 1077 } 1078 1079 if errs := validateMaxNodeProvisionTime(autoScalerProfile.MaxNodeProvisionTime, fldPath); len(errs) > 0 { 1080 allErrs = append(allErrs, errs...) 1081 } 1082 1083 if autoScalerProfile.MaxTotalUnreadyPercentage != nil { 1084 val, err := strconv.Atoi(*autoScalerProfile.MaxTotalUnreadyPercentage) 1085 if err != nil || val < 0 || val > 100 { 1086 allErrs = append(allErrs, field.Invalid(field.NewPath("Spec", "AutoscalerProfile", "MaxTotalUnreadyPercentage"), autoScalerProfile.MaxTotalUnreadyPercentage, "invalid value")) 1087 } 1088 } 1089 1090 if errs := validateNewPodScaleUpDelay(autoScalerProfile.NewPodScaleUpDelay, fldPath); len(errs) > 0 { 1091 allErrs = append(allErrs, errs...) 1092 } 1093 1094 if errs := validateIntegerStringGreaterThanZero(autoScalerProfile.OkTotalUnreadyCount, fldPath, "OkTotalUnreadyCount"); len(errs) > 0 { 1095 allErrs = append(allErrs, errs...) 1096 } 1097 1098 if errs := validateScanInterval(autoScalerProfile.ScanInterval, fldPath); len(errs) > 0 { 1099 allErrs = append(allErrs, errs...) 1100 } 1101 1102 if errs := validateScaleDownTime(autoScalerProfile.ScaleDownDelayAfterAdd, fldPath, "ScaleDownDelayAfterAdd"); len(errs) > 0 { 1103 allErrs = append(allErrs, errs...) 1104 } 1105 1106 if errs := validateScaleDownDelayAfterDelete(autoScalerProfile.ScaleDownDelayAfterDelete, fldPath); len(errs) > 0 { 1107 allErrs = append(allErrs, errs...) 1108 } 1109 1110 if errs := validateScaleDownTime(autoScalerProfile.ScaleDownDelayAfterFailure, fldPath, "ScaleDownDelayAfterFailure"); len(errs) > 0 { 1111 allErrs = append(allErrs, errs...) 1112 } 1113 1114 if errs := validateScaleDownTime(autoScalerProfile.ScaleDownUnneededTime, fldPath, "ScaleDownUnneededTime"); len(errs) > 0 { 1115 allErrs = append(allErrs, errs...) 1116 } 1117 1118 if errs := validateScaleDownTime(autoScalerProfile.ScaleDownUnreadyTime, fldPath, "ScaleDownUnreadyTime"); len(errs) > 0 { 1119 allErrs = append(allErrs, errs...) 1120 } 1121 1122 if autoScalerProfile.ScaleDownUtilizationThreshold != nil { 1123 val, err := strconv.ParseFloat(*autoScalerProfile.ScaleDownUtilizationThreshold, 32) 1124 if err != nil || val < 0 || val > 1 { 1125 allErrs = append(allErrs, field.Invalid(field.NewPath("Spec", "AutoscalerProfile", "ScaleDownUtilizationThreshold"), autoScalerProfile.ScaleDownUtilizationThreshold, "invalid value")) 1126 } 1127 } 1128 1129 return allErrs 1130 } 1131 1132 // validateMaxNodeProvisionTime validates update to AutoscalerProfile.MaxNodeProvisionTime. 1133 func validateMaxNodeProvisionTime(maxNodeProvisionTime *string, fldPath *field.Path) field.ErrorList { 1134 var allErrs field.ErrorList 1135 if ptr.Deref(maxNodeProvisionTime, "") != "" { 1136 if !rMaxNodeProvisionTime.MatchString(ptr.Deref(maxNodeProvisionTime, "")) { 1137 allErrs = append(allErrs, field.Invalid(fldPath.Child("MaxNodeProvisionTime"), maxNodeProvisionTime, "invalid value")) 1138 } 1139 } 1140 return allErrs 1141 } 1142 1143 // validateScanInterval validates update to AutoscalerProfile.ScanInterval. 1144 func validateScanInterval(scanInterval *string, fldPath *field.Path) field.ErrorList { 1145 var allErrs field.ErrorList 1146 if ptr.Deref(scanInterval, "") != "" { 1147 if !rScanInterval.MatchString(ptr.Deref(scanInterval, "")) { 1148 allErrs = append(allErrs, field.Invalid(fldPath.Child("ScanInterval"), scanInterval, "invalid value")) 1149 } 1150 } 1151 return allErrs 1152 } 1153 1154 // validateNewPodScaleUpDelay validates update to AutoscalerProfile.NewPodScaleUpDelay. 1155 func validateNewPodScaleUpDelay(newPodScaleUpDelay *string, fldPath *field.Path) field.ErrorList { 1156 var allErrs field.ErrorList 1157 if ptr.Deref(newPodScaleUpDelay, "") != "" { 1158 _, err := time.ParseDuration(ptr.Deref(newPodScaleUpDelay, "")) 1159 if err != nil { 1160 allErrs = append(allErrs, field.Invalid(fldPath.Child("NewPodScaleUpDelay"), newPodScaleUpDelay, "invalid value")) 1161 } 1162 } 1163 return allErrs 1164 } 1165 1166 // validateScaleDownDelayAfterDelete validates update to AutoscalerProfile.ScaleDownDelayAfterDelete value. 1167 func validateScaleDownDelayAfterDelete(scaleDownDelayAfterDelete *string, fldPath *field.Path) field.ErrorList { 1168 var allErrs field.ErrorList 1169 if ptr.Deref(scaleDownDelayAfterDelete, "") != "" { 1170 if !rScaleDownDelayAfterDelete.MatchString(ptr.Deref(scaleDownDelayAfterDelete, "")) { 1171 allErrs = append(allErrs, field.Invalid(fldPath.Child("ScaleDownDelayAfterDelete"), ptr.Deref(scaleDownDelayAfterDelete, ""), "invalid value")) 1172 } 1173 } 1174 return allErrs 1175 } 1176 1177 // validateScaleDownTime validates update to AutoscalerProfile.ScaleDown* values. 1178 func validateScaleDownTime(scaleDownValue *string, fldPath *field.Path, fieldName string) field.ErrorList { 1179 var allErrs field.ErrorList 1180 if ptr.Deref(scaleDownValue, "") != "" { 1181 if !rScaleDownTime.MatchString(ptr.Deref(scaleDownValue, "")) { 1182 allErrs = append(allErrs, field.Invalid(fldPath.Child(fieldName), ptr.Deref(scaleDownValue, ""), "invalid value")) 1183 } 1184 } 1185 return allErrs 1186 } 1187 1188 // validateIntegerStringGreaterThanZero validates that a string value is an integer greater than zero. 1189 func validateIntegerStringGreaterThanZero(input *string, fldPath *field.Path, fieldName string) field.ErrorList { 1190 var allErrs field.ErrorList 1191 1192 if input != nil { 1193 val, err := strconv.Atoi(*input) 1194 if err != nil || val < 0 { 1195 allErrs = append(allErrs, field.Invalid(fldPath.Child(fieldName), input, "invalid value")) 1196 } 1197 } 1198 1199 return allErrs 1200 } 1201 1202 // validateIdentity validates an Identity. 1203 func (m *AzureManagedControlPlane) validateIdentity(_ client.Client) field.ErrorList { 1204 var allErrs field.ErrorList 1205 1206 if m.Spec.Identity != nil { 1207 if m.Spec.Identity.Type == ManagedControlPlaneIdentityTypeUserAssigned { 1208 if m.Spec.Identity.UserAssignedIdentityResourceID == "" { 1209 allErrs = append(allErrs, field.Invalid(field.NewPath("Spec", "Identity", "UserAssignedIdentityResourceID"), m.Spec.Identity.UserAssignedIdentityResourceID, "cannot be empty if Identity.Type is UserAssigned")) 1210 } 1211 } else { 1212 if m.Spec.Identity.UserAssignedIdentityResourceID != "" { 1213 allErrs = append(allErrs, field.Invalid(field.NewPath("Spec", "Identity", "UserAssignedIdentityResourceID"), m.Spec.Identity.UserAssignedIdentityResourceID, "should be empty if Identity.Type is SystemAssigned")) 1214 } 1215 } 1216 } 1217 1218 if len(allErrs) > 0 { 1219 return allErrs 1220 } 1221 1222 return nil 1223 } 1224 1225 // validateNetworkPluginMode validates a NetworkPluginMode. 1226 func (m *AzureManagedControlPlane) validateNetworkPluginMode(_ client.Client) field.ErrorList { 1227 var allErrs field.ErrorList 1228 1229 const kubenet = "kubenet" 1230 if ptr.Deref(m.Spec.NetworkPluginMode, "") == NetworkPluginModeOverlay && 1231 ptr.Deref(m.Spec.NetworkPlugin, "") == kubenet { 1232 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))) 1233 } 1234 1235 if len(allErrs) > 0 { 1236 return allErrs 1237 } 1238 1239 return nil 1240 } 1241 1242 // isOIDCEnabled return true if OIDC issuer is enabled. 1243 func (m *AzureManagedControlPlaneClassSpec) isOIDCEnabled() bool { 1244 if m.OIDCIssuerProfile == nil { 1245 return false 1246 } 1247 if m.OIDCIssuerProfile.Enabled == nil { 1248 return false 1249 } 1250 return *m.OIDCIssuerProfile.Enabled 1251 } 1252 1253 // isUserManagedIdentityEnabled checks if user assigned identity is set. 1254 func (m *AzureManagedControlPlaneClassSpec) isUserManagedIdentityEnabled() bool { 1255 if m.Identity == nil { 1256 return false 1257 } 1258 if m.Identity.Type != ManagedControlPlaneIdentityTypeUserAssigned { 1259 return false 1260 } 1261 return true 1262 }