sigs.k8s.io/cluster-api-provider-azure@v1.17.0/api/v1beta1/azuremanagedcontrolplane_webhook.go (about)

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