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

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