sigs.k8s.io/cluster-api-provider-azure@v1.14.3/api/v1beta1/azurecluster_validation.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package v1beta1 18 19 import ( 20 "fmt" 21 "net" 22 "reflect" 23 "regexp" 24 25 valid "github.com/asaskevich/govalidator" 26 corev1 "k8s.io/api/core/v1" 27 apierrors "k8s.io/apimachinery/pkg/api/errors" 28 "k8s.io/apimachinery/pkg/runtime/schema" 29 "k8s.io/apimachinery/pkg/util/validation/field" 30 "k8s.io/utils/ptr" 31 "sigs.k8s.io/cluster-api-provider-azure/feature" 32 "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 33 ) 34 35 const ( 36 // can't use: \/"'[]:|<>+=;,.?*@&, Can't start with underscore. Can't end with period or hyphen. 37 // not using . in the name to avoid issues when the name is part of DNS name. 38 clusterNameRegex = `^[a-z0-9][a-z0-9-]{0,42}[a-z0-9]$` 39 // max length of 44 to allow for cluster name to be used as a prefix for VMs and other resources that 40 // have limitations as outlined here https://learn.microsoft.com/azure/azure-resource-manager/management/resource-name-rules. 41 clusterNameMaxLength = 44 42 // obtained from https://learn.microsoft.com/rest/api/resources/resourcegroups/createorupdate#uri-parameters. 43 resourceGroupRegex = `^[-\w\._\(\)]+$` 44 // described in https://learn.microsoft.com/azure/azure-resource-manager/management/resource-name-rules. 45 subnetRegex = `^[-\w\._]+$` 46 loadBalancerRegex = `^[-\w\._]+$` 47 // MaxLoadBalancerOutboundIPs is the maximum number of outbound IPs in a Standard LoadBalancer frontend configuration. 48 MaxLoadBalancerOutboundIPs = 16 49 // MinLBIdleTimeoutInMinutes is the minimum number of minutes for the LB idle timeout. 50 MinLBIdleTimeoutInMinutes = 4 51 // MaxLBIdleTimeoutInMinutes is the maximum number of minutes for the LB idle timeout. 52 MaxLBIdleTimeoutInMinutes = 30 53 // Network security rules should be a number between 100 and 4096. 54 // https://learn.microsoft.com/azure/virtual-network/network-security-groups-overview#security-rules 55 minRulePriority = 100 56 maxRulePriority = 4096 57 // Must start with 'Microsoft.', then an alpha character, then can include alnum. 58 serviceEndpointServiceRegexPattern = `^Microsoft\.[a-zA-Z]{1,42}[a-zA-Z0-9]{0,42}$` 59 // Must start with an alpha character and then can include alnum OR be only *. 60 serviceEndpointLocationRegexPattern = `^([a-z]{1,42}\d{0,5}|[*])$` 61 // described in https://learn.microsoft.com/azure/azure-resource-manager/management/resource-name-rules. 62 privateEndpointRegex = `^[-\w\._]+$` 63 // resource ID Pattern. 64 resourceIDPattern = `(?i)subscriptions/(.+)/resourceGroups/(.+)/providers/(.+?)/(.+?)/(.+)` 65 ) 66 67 var ( 68 serviceEndpointServiceRegex = regexp.MustCompile(serviceEndpointServiceRegexPattern) 69 serviceEndpointLocationRegex = regexp.MustCompile(serviceEndpointLocationRegexPattern) 70 ) 71 72 // validateCluster validates a cluster. 73 func (c *AzureCluster) validateCluster(old *AzureCluster) (admission.Warnings, error) { 74 var allErrs field.ErrorList 75 allErrs = append(allErrs, c.validateClusterName()...) 76 allErrs = append(allErrs, c.validateClusterSpec(old)...) 77 if len(allErrs) == 0 { 78 return nil, nil 79 } 80 81 return nil, apierrors.NewInvalid( 82 schema.GroupKind{Group: "infrastructure.cluster.x-k8s.io", Kind: AzureClusterKind}, 83 c.Name, allErrs) 84 } 85 86 // validateClusterSpec validates a ClusterSpec. 87 func (c *AzureCluster) validateClusterSpec(old *AzureCluster) field.ErrorList { 88 var allErrs field.ErrorList 89 var oldNetworkSpec NetworkSpec 90 if old != nil { 91 oldNetworkSpec = old.Spec.NetworkSpec 92 } 93 allErrs = append(allErrs, validateNetworkSpec(c.Spec.NetworkSpec, oldNetworkSpec, field.NewPath("spec").Child("networkSpec"))...) 94 95 var oldCloudProviderConfigOverrides *CloudProviderConfigOverrides 96 if old != nil { 97 oldCloudProviderConfigOverrides = old.Spec.CloudProviderConfigOverrides 98 } 99 allErrs = append(allErrs, validateCloudProviderConfigOverrides(c.Spec.CloudProviderConfigOverrides, oldCloudProviderConfigOverrides, 100 field.NewPath("spec").Child("cloudProviderConfigOverrides"))...) 101 102 // If ClusterSpec has non-nil ExtendedLocation field but not enable EdgeZone feature gate flag, ClusterSpec validation failed. 103 if !feature.Gates.Enabled(feature.EdgeZone) && c.Spec.ExtendedLocation != nil { 104 allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "ExtendedLocation"), "can be set only if the EdgeZone feature flag is enabled")) 105 } 106 107 if err := validateBastionSpec(c.Spec.BastionSpec, field.NewPath("spec").Child("azureBastion").Child("bastionSpec")); err != nil { 108 allErrs = append(allErrs, err) 109 } 110 111 if err := validateIdentityRef(c.Spec.IdentityRef, field.NewPath("spec").Child("identityRef")); err != nil { 112 allErrs = append(allErrs, err) 113 } 114 115 return allErrs 116 } 117 118 // validateClusterName validates ClusterName. 119 func (c *AzureCluster) validateClusterName() field.ErrorList { 120 var allErrs field.ErrorList 121 if len(c.Name) > clusterNameMaxLength { 122 allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("Name"), c.Name, 123 fmt.Sprintf("Cluster Name longer than allowed length of %d characters", clusterNameMaxLength))) 124 } 125 if success, _ := regexp.MatchString(clusterNameRegex, c.Name); !success { 126 allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("Name"), c.Name, 127 fmt.Sprintf("Cluster Name doesn't match regex %s, can contain only lowercase alphanumeric characters and '-', must start/end with an alphanumeric character", 128 clusterNameRegex))) 129 } 130 if len(allErrs) == 0 { 131 return nil 132 } 133 return allErrs 134 } 135 136 // validateBastionSpec validates a BastionSpec. 137 func validateBastionSpec(bastionSpec BastionSpec, fldPath *field.Path) *field.Error { 138 if bastionSpec.AzureBastion != nil && bastionSpec.AzureBastion.Sku != StandardBastionHostSku && bastionSpec.AzureBastion.EnableTunneling { 139 return field.Invalid(fldPath.Child("sku"), bastionSpec.AzureBastion.Sku, 140 "sku must be Standard if tunneling is enabled") 141 } 142 return nil 143 } 144 145 // validateIdentityRef validates an IdentityRef. 146 func validateIdentityRef(identityRef *corev1.ObjectReference, fldPath *field.Path) *field.Error { 147 if identityRef == nil { 148 return field.Required(fldPath, "identityRef is required") 149 } 150 if identityRef.Kind != AzureClusterIdentityKind { 151 return field.NotSupported(fldPath.Child("name"), identityRef.Name, []string{"AzureClusterIdentity"}) 152 } 153 return nil 154 } 155 156 // validateNetworkSpec validates a NetworkSpec. 157 func validateNetworkSpec(networkSpec NetworkSpec, old NetworkSpec, fldPath *field.Path) field.ErrorList { 158 var allErrs field.ErrorList 159 // If the user specifies a resourceGroup for vnet, it means 160 // that they intend to use a pre-existing vnet. In this case, 161 // we need to verify the information they provide 162 if networkSpec.Vnet.ResourceGroup != "" { 163 if err := validateResourceGroup(networkSpec.Vnet.ResourceGroup, 164 fldPath.Child("vnet").Child("resourceGroup")); err != nil { 165 allErrs = append(allErrs, err) 166 } 167 168 allErrs = append(allErrs, validateVnetCIDR(networkSpec.Vnet.CIDRBlocks, fldPath.Child("cidrBlocks"))...) 169 170 allErrs = append(allErrs, validateSubnets(networkSpec.Subnets, networkSpec.Vnet, fldPath.Child("subnets"))...) 171 172 allErrs = append(allErrs, validateVnetPeerings(networkSpec.Vnet.Peerings, fldPath.Child("peerings"))...) 173 } 174 175 var cidrBlocks []string 176 controlPlaneSubnet, err := networkSpec.GetControlPlaneSubnet() 177 if err != nil { 178 allErrs = append(allErrs, field.Invalid(fldPath.Child("subnets"), networkSpec.Subnets, "ControlPlaneSubnet invalid")) 179 } 180 181 cidrBlocks = controlPlaneSubnet.CIDRBlocks 182 183 allErrs = append(allErrs, validateAPIServerLB(networkSpec.APIServerLB, old.APIServerLB, cidrBlocks, fldPath.Child("apiServerLB"))...) 184 185 var needOutboundLB bool 186 for _, subnet := range networkSpec.Subnets { 187 if (subnet.Role == SubnetNode || subnet.Role == SubnetCluster) && subnet.IsIPv6Enabled() { 188 needOutboundLB = true 189 break 190 } 191 } 192 if needOutboundLB { 193 allErrs = append(allErrs, validateNodeOutboundLB(networkSpec.NodeOutboundLB, old.NodeOutboundLB, networkSpec.APIServerLB, fldPath.Child("nodeOutboundLB"))...) 194 } 195 196 allErrs = append(allErrs, validateControlPlaneOutboundLB(networkSpec.ControlPlaneOutboundLB, networkSpec.APIServerLB, fldPath.Child("controlPlaneOutboundLB"))...) 197 198 allErrs = append(allErrs, validatePrivateDNSZoneName(networkSpec.PrivateDNSZoneName, networkSpec.APIServerLB.Type, fldPath.Child("privateDNSZoneName"))...) 199 200 if len(allErrs) == 0 { 201 return nil 202 } 203 return allErrs 204 } 205 206 // validateResourceGroup validates a ResourceGroup. 207 func validateResourceGroup(resourceGroup string, fldPath *field.Path) *field.Error { 208 if success, _ := regexp.MatchString(resourceGroupRegex, resourceGroup); !success { 209 return field.Invalid(fldPath, resourceGroup, 210 fmt.Sprintf("resourceGroup doesn't match regex %s", resourceGroupRegex)) 211 } 212 return nil 213 } 214 215 // validateSubnets validates a list of Subnets. 216 // When configuring a cluster, it is essential to include either a control-plane subnet and a node subnet, or a user can configure a cluster subnet which will be used as a control-plane subnet and a node subnet. 217 func validateSubnets(subnets Subnets, vnet VnetSpec, fldPath *field.Path) field.ErrorList { 218 var allErrs field.ErrorList 219 subnetNames := make(map[string]bool, len(subnets)) 220 requiredSubnetRoles := map[string]bool{ 221 "control-plane": false, 222 "node": false, 223 } 224 clusterSubnet := false 225 numberofClusterSubnets := 0 226 for i, subnet := range subnets { 227 if err := validateSubnetName(subnet.Name, fldPath.Index(i).Child("name")); err != nil { 228 allErrs = append(allErrs, err) 229 } 230 if _, ok := subnetNames[subnet.Name]; ok { 231 allErrs = append(allErrs, field.Duplicate(fldPath, subnet.Name)) 232 } 233 subnetNames[subnet.Name] = true 234 if subnet.Role == SubnetCluster { 235 clusterSubnet = true 236 numberofClusterSubnets++ 237 } else { 238 for role := range requiredSubnetRoles { 239 if role == string(subnet.Role) { 240 requiredSubnetRoles[role] = true 241 } 242 } 243 } 244 245 for _, rule := range subnet.SecurityGroup.SecurityRules { 246 if err := validateSecurityRule( 247 rule, 248 fldPath.Index(i).Child("securityGroup").Child("securityRules").Index(i), 249 ); err != nil { 250 allErrs = append(allErrs, err...) 251 } 252 } 253 allErrs = append(allErrs, validateSubnetCIDR(subnet.CIDRBlocks, vnet.CIDRBlocks, fldPath.Index(i).Child("cidrBlocks"))...) 254 255 if len(subnet.ServiceEndpoints) > 0 { 256 allErrs = append(allErrs, validateServiceEndpoints(subnet.ServiceEndpoints, fldPath.Index(i).Child("serviceEndpoints"))...) 257 } 258 259 if len(subnet.PrivateEndpoints) > 0 { 260 allErrs = append(allErrs, validatePrivateEndpoints(subnet.PrivateEndpoints, subnet.CIDRBlocks, fldPath.Index(i).Child("privateEndpoints"))...) 261 } 262 } 263 264 // The clusterSubnet is applicable to both the control-plane and node pools. 265 // Validation of requiredSubnetRoles is skipped since clusterSubnet is set to true. 266 if clusterSubnet { 267 return allErrs 268 } 269 270 for k, v := range requiredSubnetRoles { 271 if !v { 272 allErrs = append(allErrs, field.Required(fldPath, 273 fmt.Sprintf("required role %s not included in provided subnets", k))) 274 } 275 } 276 return allErrs 277 } 278 279 // validateSubnetName validates the Name of a Subnet. 280 func validateSubnetName(name string, fldPath *field.Path) *field.Error { 281 if success, _ := regexp.Match(subnetRegex, []byte(name)); !success { 282 return field.Invalid(fldPath, name, 283 fmt.Sprintf("name of subnet doesn't match regex %s", subnetRegex)) 284 } 285 return nil 286 } 287 288 // validateSubnetCIDR validates the CIDR blocks of a Subnet. 289 func validateSubnetCIDR(subnetCidrBlocks []string, vnetCidrBlocks []string, fldPath *field.Path) field.ErrorList { 290 var allErrs field.ErrorList 291 var vnetNws []*net.IPNet 292 293 for _, vnetCidr := range vnetCidrBlocks { 294 if _, vnetNw, err := net.ParseCIDR(vnetCidr); err == nil { 295 vnetNws = append(vnetNws, vnetNw) 296 } 297 } 298 299 for _, subnetCidr := range subnetCidrBlocks { 300 subnetCidrIP, _, err := net.ParseCIDR(subnetCidr) 301 if err != nil { 302 allErrs = append(allErrs, field.Invalid(fldPath, subnetCidr, "invalid CIDR format")) 303 } 304 305 var found bool 306 for _, vnetNw := range vnetNws { 307 if vnetNw.Contains(subnetCidrIP) { 308 found = true 309 break 310 } 311 } 312 313 if !found { 314 allErrs = append(allErrs, field.Invalid(fldPath, subnetCidr, fmt.Sprintf("subnet CIDR not in vnet address space: %s", vnetCidrBlocks))) 315 } 316 } 317 318 return allErrs 319 } 320 321 // validateVnetCIDR validates the CIDR blocks of a Vnet. 322 func validateVnetCIDR(vnetCIDRBlocks []string, fldPath *field.Path) field.ErrorList { 323 var allErrs field.ErrorList 324 for _, vnetCidr := range vnetCIDRBlocks { 325 if _, _, err := net.ParseCIDR(vnetCidr); err != nil { 326 allErrs = append(allErrs, field.Invalid(fldPath, vnetCidr, "invalid CIDR format")) 327 } 328 } 329 return allErrs 330 } 331 332 // validateVnetPeerings validates a list of virtual network peerings. 333 func validateVnetPeerings(peerings VnetPeerings, fldPath *field.Path) field.ErrorList { 334 var allErrs field.ErrorList 335 vnetIdentifiers := make(map[string]bool, len(peerings)) 336 337 for _, peering := range peerings { 338 vnetIdentifier := peering.ResourceGroup + "/" + peering.RemoteVnetName 339 if _, ok := vnetIdentifiers[vnetIdentifier]; ok { 340 allErrs = append(allErrs, field.Duplicate(fldPath, vnetIdentifier)) 341 } 342 vnetIdentifiers[vnetIdentifier] = true 343 } 344 return allErrs 345 } 346 347 // validateLoadBalancerName validates the Name of a Load Balancer. 348 func validateLoadBalancerName(name string, fldPath *field.Path) *field.Error { 349 if success, _ := regexp.Match(loadBalancerRegex, []byte(name)); !success { 350 return field.Invalid(fldPath, name, 351 fmt.Sprintf("name of load balancer doesn't match regex %s", loadBalancerRegex)) 352 } 353 return nil 354 } 355 356 // validateInternalLBIPAddress validates a InternalLBIPAddress. 357 func validateInternalLBIPAddress(address string, cidrs []string, fldPath *field.Path) *field.Error { 358 ip := net.ParseIP(address) 359 if ip == nil { 360 return field.Invalid(fldPath, address, 361 "Internal LB IP address isn't a valid IPv4 or IPv6 address") 362 } 363 for _, cidr := range cidrs { 364 _, subnet, _ := net.ParseCIDR(cidr) 365 if subnet.Contains(ip) { 366 return nil 367 } 368 } 369 return field.Invalid(fldPath, address, 370 fmt.Sprintf("Internal LB IP address needs to be in control plane subnet range (%s)", cidrs)) 371 } 372 373 // validateSecurityRule validates a SecurityRule. 374 func validateSecurityRule(rule SecurityRule, fldPath *field.Path) (allErrs field.ErrorList) { 375 if rule.Priority < minRulePriority || rule.Priority > maxRulePriority { 376 allErrs = append(allErrs, field.Invalid(fldPath, rule.Priority, fmt.Sprintf("security rule priorities should be between %d and %d", minRulePriority, maxRulePriority))) 377 } 378 379 if rule.Source != nil && rule.Sources != nil { 380 allErrs = append(allErrs, field.Invalid(fldPath, rule.Source, "security rule cannot have both source and sources")) 381 } 382 383 return allErrs 384 } 385 386 func validateAPIServerLB(lb LoadBalancerSpec, old LoadBalancerSpec, cidrs []string, fldPath *field.Path) field.ErrorList { 387 var allErrs field.ErrorList 388 389 lbClassSpec := lb.LoadBalancerClassSpec 390 olLBClassSpec := old.LoadBalancerClassSpec 391 allErrs = append(allErrs, validateClassSpecForAPIServerLB(lbClassSpec, &olLBClassSpec, fldPath)...) 392 393 // Name should be valid. 394 if err := validateLoadBalancerName(lb.Name, fldPath.Child("name")); err != nil { 395 allErrs = append(allErrs, err) 396 } 397 // Name should be immutable. 398 if old.Name != "" && old.Name != lb.Name { 399 allErrs = append(allErrs, field.Forbidden(fldPath.Child("name"), "API Server load balancer name should not be modified after AzureCluster creation.")) 400 } 401 402 // There should only be one IP config. 403 if len(lb.FrontendIPs) != 1 || ptr.Deref[int32](lb.FrontendIPsCount, 1) != 1 { 404 allErrs = append(allErrs, field.Invalid(fldPath.Child("frontendIPConfigs"), lb.FrontendIPs, 405 "API Server Load balancer should have 1 Frontend IP")) 406 } else { 407 // if Internal, IP config should not have a public IP. 408 if lb.Type == Internal { 409 if lb.FrontendIPs[0].PublicIP != nil { 410 allErrs = append(allErrs, field.Forbidden(fldPath.Child("frontendIPConfigs").Index(0).Child("publicIP"), 411 "Internal Load Balancers cannot have a Public IP")) 412 } 413 if lb.FrontendIPs[0].PrivateIPAddress != "" { 414 if err := validateInternalLBIPAddress(lb.FrontendIPs[0].PrivateIPAddress, cidrs, 415 fldPath.Child("frontendIPConfigs").Index(0).Child("privateIP")); err != nil { 416 allErrs = append(allErrs, err) 417 } 418 if len(old.FrontendIPs) != 0 && old.FrontendIPs[0].PrivateIPAddress != lb.FrontendIPs[0].PrivateIPAddress { 419 allErrs = append(allErrs, field.Forbidden(fldPath.Child("name"), "API Server load balancer private IP should not be modified after AzureCluster creation.")) 420 } 421 } 422 } 423 424 // if Public, IP config should not have a private IP. 425 if lb.Type == Public { 426 if lb.FrontendIPs[0].PrivateIPAddress != "" { 427 allErrs = append(allErrs, field.Forbidden(fldPath.Child("frontendIPConfigs").Index(0).Child("privateIP"), 428 "Public Load Balancers cannot have a Private IP")) 429 } 430 } 431 } 432 433 return allErrs 434 } 435 436 func validateNodeOutboundLB(lb *LoadBalancerSpec, old *LoadBalancerSpec, apiserverLB LoadBalancerSpec, fldPath *field.Path) field.ErrorList { 437 var allErrs field.ErrorList 438 439 var lbClassSpec, oldClassSpec *LoadBalancerClassSpec 440 if lb != nil { 441 lbClassSpec = &lb.LoadBalancerClassSpec 442 } 443 if old != nil { 444 oldClassSpec = &old.LoadBalancerClassSpec 445 } 446 apiserverLBClassSpec := apiserverLB.LoadBalancerClassSpec 447 448 allErrs = append(allErrs, validateClassSpecForNodeOutboundLB(lbClassSpec, oldClassSpec, apiserverLBClassSpec, fldPath)...) 449 450 if lb == nil { 451 return allErrs 452 } 453 454 if old != nil && old.ID != lb.ID { 455 allErrs = append(allErrs, field.Forbidden(fldPath.Child("id"), "Node outbound load balancer ID should not be modified after AzureCluster creation.")) 456 } 457 458 if old != nil && old.Name != lb.Name { 459 allErrs = append(allErrs, field.Forbidden(fldPath.Child("name"), "Node outbound load balancer Name should not be modified after AzureCluster creation.")) 460 } 461 462 if old != nil && old.FrontendIPsCount == lb.FrontendIPsCount { 463 if len(old.FrontendIPs) != len(lb.FrontendIPs) { 464 allErrs = append(allErrs, field.Forbidden(fldPath.Child("frontendIPs"), "Node outbound load balancer FrontendIPs cannot be modified after AzureCluster creation.")) 465 } 466 467 if len(old.FrontendIPs) == len(lb.FrontendIPs) { 468 for i, frontEndIP := range lb.FrontendIPs { 469 oldFrontendIP := old.FrontendIPs[i] 470 if oldFrontendIP.Name != frontEndIP.Name || !reflect.DeepEqual(*oldFrontendIP.PublicIP, *frontEndIP.PublicIP) { 471 allErrs = append(allErrs, field.Forbidden(fldPath.Child("frontendIPs").Index(i), 472 "Node outbound load balancer FrontendIPs cannot be modified after AzureCluster creation.")) 473 } 474 } 475 } 476 } 477 478 if lb.FrontendIPsCount != nil && *lb.FrontendIPsCount > MaxLoadBalancerOutboundIPs { 479 allErrs = append(allErrs, field.Invalid(fldPath.Child("frontendIPsCount"), *lb.FrontendIPsCount, 480 fmt.Sprintf("Max front end ips allowed is %d", MaxLoadBalancerOutboundIPs))) 481 } 482 483 return allErrs 484 } 485 486 func validateControlPlaneOutboundLB(lb *LoadBalancerSpec, apiserverLB LoadBalancerSpec, fldPath *field.Path) field.ErrorList { 487 var allErrs field.ErrorList 488 489 var lbClassSpec *LoadBalancerClassSpec 490 if lb != nil { 491 lbClassSpec = &lb.LoadBalancerClassSpec 492 } 493 apiServerLBClassSpec := apiserverLB.LoadBalancerClassSpec 494 495 allErrs = append(allErrs, validateClassSpecForControlPlaneOutboundLB(lbClassSpec, apiServerLBClassSpec, fldPath)...) 496 497 if apiServerLBClassSpec.Type == Internal && lb != nil { 498 if lb.FrontendIPsCount != nil && *lb.FrontendIPsCount > MaxLoadBalancerOutboundIPs { 499 allErrs = append(allErrs, field.Invalid(fldPath.Child("frontendIPsCount"), *lb.FrontendIPsCount, 500 fmt.Sprintf("Max front end ips allowed is %d", MaxLoadBalancerOutboundIPs))) 501 } 502 } 503 504 return allErrs 505 } 506 507 // validatePrivateDNSZoneName validates the PrivateDNSZoneName. 508 func validatePrivateDNSZoneName(privateDNSZoneName string, apiserverLBType LBType, fldPath *field.Path) field.ErrorList { 509 var allErrs field.ErrorList 510 511 if len(privateDNSZoneName) > 0 { 512 if apiserverLBType != Internal { 513 allErrs = append(allErrs, field.Invalid(fldPath, apiserverLBType, 514 "PrivateDNSZoneName is available only if APIServerLB.Type is Internal")) 515 } 516 if !valid.IsDNSName(privateDNSZoneName) { 517 allErrs = append(allErrs, field.Invalid(fldPath, privateDNSZoneName, 518 "PrivateDNSZoneName can only contain alphanumeric characters, underscores and dashes, must end with an alphanumeric character", 519 )) 520 } 521 } 522 523 return allErrs 524 } 525 526 // validateCloudProviderConfigOverrides validates CloudProviderConfigOverrides. 527 func validateCloudProviderConfigOverrides(oldConfig, newConfig *CloudProviderConfigOverrides, fldPath *field.Path) field.ErrorList { 528 var allErrs field.ErrorList 529 if !reflect.DeepEqual(oldConfig, newConfig) { 530 allErrs = append(allErrs, field.Invalid(fldPath, newConfig, "cannot change cloudProviderConfigOverrides cluster creation")) 531 } 532 return allErrs 533 } 534 535 func validateClassSpecForAPIServerLB(lb LoadBalancerClassSpec, old *LoadBalancerClassSpec, apiServerLBPath *field.Path) field.ErrorList { 536 var allErrs field.ErrorList 537 538 // SKU should be Standard 539 if lb.SKU != SKUStandard { 540 allErrs = append(allErrs, field.NotSupported(apiServerLBPath.Child("sku"), lb.SKU, []string{string(SKUStandard)})) 541 } 542 543 // Type should be Public or Internal. 544 if lb.Type != Internal && lb.Type != Public { 545 allErrs = append(allErrs, field.NotSupported(apiServerLBPath.Child("type"), lb.Type, 546 []string{string(Public), string(Internal)})) 547 } 548 549 // SKU should be immutable. 550 if old != nil && old.SKU != "" && old.SKU != lb.SKU { 551 allErrs = append(allErrs, field.Forbidden(apiServerLBPath.Child("sku"), "API Server load balancer SKU should not be modified after AzureCluster creation.")) 552 } 553 554 // Type should be immutable. 555 if old != nil && old.Type != "" && old.Type != lb.Type { 556 allErrs = append(allErrs, field.Forbidden(apiServerLBPath.Child("type"), "API Server load balancer type should not be modified after AzureCluster creation.")) 557 } 558 559 // IdletimeoutInMinutes should be immutable. 560 if old != nil && old.IdleTimeoutInMinutes != nil && !ptr.Equal(old.IdleTimeoutInMinutes, lb.IdleTimeoutInMinutes) { 561 allErrs = append(allErrs, field.Forbidden(apiServerLBPath.Child("idleTimeoutInMinutes"), "API Server load balancer idle timeout cannot be modified after AzureCluster creation.")) 562 } 563 564 if lb.IdleTimeoutInMinutes != nil && (*lb.IdleTimeoutInMinutes < MinLBIdleTimeoutInMinutes || *lb.IdleTimeoutInMinutes > MaxLBIdleTimeoutInMinutes) { 565 allErrs = append(allErrs, field.Invalid(apiServerLBPath.Child("idleTimeoutInMinutes"), *lb.IdleTimeoutInMinutes, 566 fmt.Sprintf("Node outbound idle timeout should be between %d and %d minutes", MinLBIdleTimeoutInMinutes, MaxLoadBalancerOutboundIPs))) 567 } 568 569 return allErrs 570 } 571 572 func validateClassSpecForNodeOutboundLB(lb *LoadBalancerClassSpec, old *LoadBalancerClassSpec, apiserverLB LoadBalancerClassSpec, fldPath *field.Path) field.ErrorList { 573 var allErrs field.ErrorList 574 575 // LB can be nil when disabled for private clusters. 576 if lb == nil && apiserverLB.Type == Internal { 577 return allErrs 578 } 579 580 if lb == nil { 581 allErrs = append(allErrs, field.Required(fldPath, "Node outbound load balancer cannot be nil for public clusters.")) 582 return allErrs 583 } 584 585 if old != nil && old.SKU != lb.SKU { 586 allErrs = append(allErrs, field.Forbidden(fldPath.Child("sku"), "Node outbound load balancer SKU should not be modified after AzureCluster creation.")) 587 } 588 589 if old != nil && old.Type != lb.Type { 590 allErrs = append(allErrs, field.Forbidden(fldPath.Child("type"), "Node outbound load balancer Type cannot be modified after AzureCluster creation.")) 591 } 592 593 if old != nil && !ptr.Equal(old.IdleTimeoutInMinutes, lb.IdleTimeoutInMinutes) { 594 allErrs = append(allErrs, field.Forbidden(fldPath.Child("idleTimeoutInMinutes"), "Node outbound load balancer idle timeout cannot be modified after AzureCluster creation.")) 595 } 596 597 if lb.IdleTimeoutInMinutes != nil && (*lb.IdleTimeoutInMinutes < MinLBIdleTimeoutInMinutes || *lb.IdleTimeoutInMinutes > MaxLBIdleTimeoutInMinutes) { 598 allErrs = append(allErrs, field.Invalid(fldPath.Child("idleTimeoutInMinutes"), *lb.IdleTimeoutInMinutes, 599 fmt.Sprintf("Node outbound idle timeout should be between %d and %d minutes", MinLBIdleTimeoutInMinutes, MaxLoadBalancerOutboundIPs))) 600 } 601 602 return allErrs 603 } 604 605 func validateClassSpecForControlPlaneOutboundLB(lb *LoadBalancerClassSpec, apiserverLB LoadBalancerClassSpec, fldPath *field.Path) field.ErrorList { 606 var allErrs field.ErrorList 607 608 switch apiserverLB.Type { 609 case Public: 610 if lb != nil { 611 allErrs = append(allErrs, field.Forbidden(fldPath, "Control plane outbound load balancer cannot be set for public clusters.")) 612 } 613 case Internal: 614 // Control plane outbound lb can be nil when it's disabled for private clusters. 615 if lb == nil { 616 return nil 617 } 618 619 if lb.IdleTimeoutInMinutes != nil && (*lb.IdleTimeoutInMinutes < MinLBIdleTimeoutInMinutes || *lb.IdleTimeoutInMinutes > MaxLBIdleTimeoutInMinutes) { 620 allErrs = append(allErrs, field.Invalid(fldPath.Child("idleTimeoutInMinutes"), *lb.IdleTimeoutInMinutes, 621 fmt.Sprintf("Control plane outbound idle timeout should be between %d and %d minutes", MinLBIdleTimeoutInMinutes, MaxLoadBalancerOutboundIPs))) 622 } 623 } 624 625 return allErrs 626 } 627 628 func validateServiceEndpoints(serviceEndpoints []ServiceEndpointSpec, fldPath *field.Path) field.ErrorList { 629 var allErrs field.ErrorList 630 631 serviceEndpointsServices := make(map[string]bool, len(serviceEndpoints)) 632 for i, se := range serviceEndpoints { 633 if se.Service == "" { 634 allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("service"), "service is required for all service endpoints")) 635 } else { 636 if err := validateServiceEndpointServiceName(se.Service, fldPath.Index(i).Child("service")); err != nil { 637 allErrs = append(allErrs, err) 638 } 639 if _, ok := serviceEndpointsServices[se.Service]; ok { 640 allErrs = append(allErrs, field.Duplicate(fldPath.Index(i).Child("service"), se.Service)) 641 } 642 serviceEndpointsServices[se.Service] = true 643 } 644 645 if len(se.Locations) == 0 { 646 allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("locations"), "locations are required for all service endpoints")) 647 } else { 648 serviceEndpointsLocations := make(map[string]bool, len(se.Locations)) 649 for j, locationName := range se.Locations { 650 if err := validateServiceEndpointLocationName(locationName, fldPath.Index(i).Child("locations").Index(j)); err != nil { 651 allErrs = append(allErrs, err) 652 } 653 if _, ok := serviceEndpointsLocations[locationName]; ok { 654 allErrs = append(allErrs, field.Duplicate(fldPath.Index(i).Child("locations").Index(j), locationName)) 655 } 656 serviceEndpointsLocations[locationName] = true 657 } 658 } 659 } 660 661 return allErrs 662 } 663 664 func validateServiceEndpointServiceName(serviceName string, fldPath *field.Path) *field.Error { 665 if success := serviceEndpointServiceRegex.MatchString(serviceName); !success { 666 return field.Invalid(fldPath, serviceName, fmt.Sprintf("service name of endpoint service doesn't match regex %s", serviceEndpointServiceRegexPattern)) 667 } 668 return nil 669 } 670 671 func validateServiceEndpointLocationName(location string, fldPath *field.Path) *field.Error { 672 if success := serviceEndpointLocationRegex.MatchString(location); !success { 673 return field.Invalid(fldPath, location, fmt.Sprintf("location doesn't match regex %s", serviceEndpointLocationRegexPattern)) 674 } 675 return nil 676 } 677 678 func validatePrivateEndpoints(privateEndpointSpecs []PrivateEndpointSpec, subnetCIDRs []string, fldPath *field.Path) field.ErrorList { 679 var allErrs field.ErrorList 680 681 for i, pe := range privateEndpointSpecs { 682 if err := validatePrivateEndpointName(pe.Name, fldPath.Index(i).Child("name")); err != nil { 683 allErrs = append(allErrs, err) 684 } 685 686 if len(pe.PrivateLinkServiceConnections) == 0 { 687 allErrs = append(allErrs, field.Invalid(fldPath.Index(i), pe.PrivateLinkServiceConnections, "privateLinkServiceConnections cannot be empty")) 688 } 689 690 for j, privateLinkServiceConnection := range pe.PrivateLinkServiceConnections { 691 if privateLinkServiceConnection.PrivateLinkServiceID == "" { 692 allErrs = append(allErrs, field.Required(fldPath.Index(i).Child("privateLinkServiceConnections").Index(j), "privateLinkServiceID is required for all privateLinkServiceConnections in private endpoints")) 693 } else { 694 if err := validatePrivateEndpointPrivateLinkServiceConnection(privateLinkServiceConnection, fldPath.Index(i).Child("privateLinkServiceConnections").Index(j)); err != nil { 695 allErrs = append(allErrs, err) 696 } 697 } 698 } 699 700 for _, privateIP := range pe.PrivateIPAddresses { 701 if err := validatePrivateEndpointIPAddress(privateIP, subnetCIDRs, fldPath.Index(i).Child("privateIPAddresses")); err != nil { 702 allErrs = append(allErrs, err) 703 } 704 } 705 } 706 707 return allErrs 708 } 709 710 // validatePrivateEndpointName validates the Name of a Private Endpoint. 711 func validatePrivateEndpointName(name string, fldPath *field.Path) *field.Error { 712 if name == "" { 713 return field.Invalid(fldPath, name, "name of private endpoint cannot be empty") 714 } 715 716 if success, _ := regexp.MatchString(privateEndpointRegex, name); !success { 717 return field.Invalid(fldPath, name, 718 fmt.Sprintf("name of private endpoint doesn't match regex %s", privateEndpointRegex)) 719 } 720 return nil 721 } 722 723 // validatePrivateEndpointServiceID validates the service ID of a Private Endpoint. 724 func validatePrivateEndpointPrivateLinkServiceConnection(privateLinkServiceConnection PrivateLinkServiceConnection, fldPath *field.Path) *field.Error { 725 if success, _ := regexp.MatchString(resourceIDPattern, privateLinkServiceConnection.PrivateLinkServiceID); !success { 726 return field.Invalid(fldPath, privateLinkServiceConnection.PrivateLinkServiceID, 727 fmt.Sprintf("private endpoint privateLinkServiceConnection service ID doesn't match regex %s", resourceIDPattern)) 728 } 729 if privateLinkServiceConnection.Name != "" { 730 if success, _ := regexp.MatchString(privateEndpointRegex, privateLinkServiceConnection.Name); !success { 731 return field.Invalid(fldPath, privateLinkServiceConnection.Name, 732 fmt.Sprintf("private endpoint privateLinkServiceConnection name doesn't match regex %s", privateEndpointRegex)) 733 } 734 } 735 return nil 736 } 737 738 // validatePrivateEndpointIPAddress validates a Private Endpoint IP Address. 739 func validatePrivateEndpointIPAddress(address string, cidrs []string, fldPath *field.Path) *field.Error { 740 ip := net.ParseIP(address) 741 if ip == nil { 742 return field.Invalid(fldPath, address, 743 "Private Endpoint IP address isn't a valid IPv4 or IPv6 address") 744 } 745 746 for _, cidr := range cidrs { 747 _, subnet, _ := net.ParseCIDR(cidr) 748 if subnet != nil && subnet.Contains(ip) { 749 return nil 750 } 751 } 752 753 return field.Invalid(fldPath, address, 754 fmt.Sprintf("Private Endpoint IP address needs to be in subnet range (%s)", cidrs)) 755 }