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  }