sigs.k8s.io/cluster-api-provider-azure@v1.14.3/api/v1beta1/azuremanagedmachinepool_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  	"regexp"
    23  	"strconv"
    24  	"strings"
    25  	"unicode"
    26  
    27  	"github.com/pkg/errors"
    28  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    31  	"k8s.io/apimachinery/pkg/util/validation/field"
    32  	"k8s.io/utils/ptr"
    33  	"sigs.k8s.io/cluster-api-provider-azure/feature"
    34  	azureutil "sigs.k8s.io/cluster-api-provider-azure/util/azure"
    35  	webhookutils "sigs.k8s.io/cluster-api-provider-azure/util/webhook"
    36  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    37  	clusterctlv1alpha3 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
    38  	capifeature "sigs.k8s.io/cluster-api/feature"
    39  	ctrl "sigs.k8s.io/controller-runtime"
    40  	"sigs.k8s.io/controller-runtime/pkg/client"
    41  	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
    42  )
    43  
    44  var validNodePublicPrefixID = regexp.MustCompile(`(?i)^/?subscriptions/[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}/resourcegroups/[^/]+/providers/microsoft\.network/publicipprefixes/[^/]+$`)
    45  
    46  // SetupAzureManagedMachinePoolWebhookWithManager sets up and registers the webhook with the manager.
    47  func SetupAzureManagedMachinePoolWebhookWithManager(mgr ctrl.Manager) error {
    48  	mw := &azureManagedMachinePoolWebhook{Client: mgr.GetClient()}
    49  	return ctrl.NewWebhookManagedBy(mgr).
    50  		For(&AzureManagedMachinePool{}).
    51  		WithDefaulter(mw).
    52  		WithValidator(mw).
    53  		Complete()
    54  }
    55  
    56  //+kubebuilder:webhook:path=/mutate-infrastructure-cluster-x-k8s-io-v1beta1-azuremanagedmachinepool,mutating=true,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=azuremanagedmachinepools,verbs=create;update,versions=v1beta1,name=default.azuremanagedmachinepools.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    57  
    58  // azureManagedMachinePoolWebhook implements a validating and defaulting webhook for AzureManagedMachinePool.
    59  type azureManagedMachinePoolWebhook struct {
    60  	Client client.Client
    61  }
    62  
    63  // Default implements webhook.Defaulter so a webhook will be registered for the type.
    64  func (mw *azureManagedMachinePoolWebhook) Default(ctx context.Context, obj runtime.Object) error {
    65  	m, ok := obj.(*AzureManagedMachinePool)
    66  	if !ok {
    67  		return apierrors.NewBadRequest("expected an AzureManagedMachinePool")
    68  	}
    69  	if m.Labels == nil {
    70  		m.Labels = make(map[string]string)
    71  	}
    72  	m.Labels[LabelAgentPoolMode] = m.Spec.Mode
    73  
    74  	if m.Spec.Name == nil || *m.Spec.Name == "" {
    75  		m.Spec.Name = &m.Name
    76  	}
    77  
    78  	if m.Spec.OSType == nil {
    79  		m.Spec.OSType = ptr.To(DefaultOSType)
    80  	}
    81  
    82  	return nil
    83  }
    84  
    85  //+kubebuilder:webhook:verbs=create;update;delete,path=/validate-infrastructure-cluster-x-k8s-io-v1beta1-azuremanagedmachinepool,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=infrastructure.cluster.x-k8s.io,resources=azuremanagedmachinepools,versions=v1beta1,name=validation.azuremanagedmachinepools.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1;v1beta1
    86  
    87  // ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
    88  func (mw *azureManagedMachinePoolWebhook) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
    89  	m, ok := obj.(*AzureManagedMachinePool)
    90  	if !ok {
    91  		return nil, apierrors.NewBadRequest("expected an AzureManagedMachinePool")
    92  	}
    93  	// NOTE: AzureManagedMachinePool relies upon MachinePools, which is behind a feature gate flag.
    94  	// The webhook must prevent creating new objects in case the feature flag is disabled.
    95  	if !feature.Gates.Enabled(capifeature.MachinePool) {
    96  		return nil, field.Forbidden(
    97  			field.NewPath("spec"),
    98  			"can be set only if the Cluster API 'MachinePool' feature flag is enabled",
    99  		)
   100  	}
   101  
   102  	var errs []error
   103  
   104  	errs = append(errs, validateMaxPods(
   105  		m.Spec.MaxPods,
   106  		field.NewPath("Spec", "MaxPods")))
   107  
   108  	errs = append(errs, validateOSType(
   109  		m.Spec.Mode,
   110  		m.Spec.OSType,
   111  		field.NewPath("Spec", "OSType")))
   112  
   113  	errs = append(errs, validateMPName(
   114  		m.Name,
   115  		m.Spec.Name,
   116  		m.Spec.OSType,
   117  		field.NewPath("Spec", "Name")))
   118  
   119  	errs = append(errs, validateNodeLabels(
   120  		m.Spec.NodeLabels,
   121  		field.NewPath("Spec", "NodeLabels")))
   122  
   123  	errs = append(errs, validateNodePublicIPPrefixID(
   124  		m.Spec.NodePublicIPPrefixID,
   125  		field.NewPath("Spec", "NodePublicIPPrefixID")))
   126  
   127  	errs = append(errs, validateEnableNodePublicIP(
   128  		m.Spec.EnableNodePublicIP,
   129  		m.Spec.NodePublicIPPrefixID,
   130  		field.NewPath("Spec", "EnableNodePublicIP")))
   131  
   132  	errs = append(errs, validateKubeletConfig(
   133  		m.Spec.KubeletConfig,
   134  		field.NewPath("Spec", "KubeletConfig")))
   135  
   136  	errs = append(errs, validateLinuxOSConfig(
   137  		m.Spec.LinuxOSConfig,
   138  		m.Spec.KubeletConfig,
   139  		field.NewPath("Spec", "LinuxOSConfig")))
   140  
   141  	errs = append(errs, validateMPSubnetName(
   142  		m.Spec.SubnetName,
   143  		field.NewPath("Spec", "SubnetName")))
   144  
   145  	return nil, kerrors.NewAggregate(errs)
   146  }
   147  
   148  // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
   149  func (mw *azureManagedMachinePoolWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
   150  	old, ok := oldObj.(*AzureManagedMachinePool)
   151  	if !ok {
   152  		return nil, apierrors.NewBadRequest("expected an AzureManagedMachinePool")
   153  	}
   154  	m, ok := newObj.(*AzureManagedMachinePool)
   155  	if !ok {
   156  		return nil, apierrors.NewBadRequest("expected an AzureManagedMachinePool")
   157  	}
   158  	var allErrs field.ErrorList
   159  
   160  	if err := webhookutils.ValidateImmutable(
   161  		field.NewPath("Spec", "Name"),
   162  		old.Spec.Name,
   163  		m.Spec.Name); err != nil {
   164  		allErrs = append(allErrs, err)
   165  	}
   166  
   167  	if err := validateNodeLabels(m.Spec.NodeLabels, field.NewPath("Spec", "NodeLabels")); err != nil {
   168  		allErrs = append(allErrs,
   169  			field.Invalid(
   170  				field.NewPath("Spec", "NodeLabels"),
   171  				m.Spec.NodeLabels,
   172  				err.Error()))
   173  	}
   174  
   175  	if err := webhookutils.ValidateImmutable(
   176  		field.NewPath("Spec", "OSType"),
   177  		old.Spec.OSType,
   178  		m.Spec.OSType); err != nil {
   179  		allErrs = append(allErrs, err)
   180  	}
   181  
   182  	if err := webhookutils.ValidateImmutable(
   183  		field.NewPath("Spec", "SKU"),
   184  		old.Spec.SKU,
   185  		m.Spec.SKU); err != nil {
   186  		allErrs = append(allErrs, err)
   187  	}
   188  
   189  	if err := webhookutils.ValidateImmutable(
   190  		field.NewPath("Spec", "OSDiskSizeGB"),
   191  		old.Spec.OSDiskSizeGB,
   192  		m.Spec.OSDiskSizeGB); err != nil {
   193  		allErrs = append(allErrs, err)
   194  	}
   195  
   196  	if err := webhookutils.ValidateImmutable(
   197  		field.NewPath("Spec", "SubnetName"),
   198  		old.Spec.SubnetName,
   199  		m.Spec.SubnetName); err != nil && old.Spec.SubnetName != nil {
   200  		allErrs = append(allErrs, err)
   201  	}
   202  
   203  	if err := webhookutils.ValidateImmutable(
   204  		field.NewPath("Spec", "EnableFIPS"),
   205  		old.Spec.EnableFIPS,
   206  		m.Spec.EnableFIPS); err != nil && old.Spec.EnableFIPS != nil {
   207  		allErrs = append(allErrs, err)
   208  	}
   209  
   210  	if err := webhookutils.ValidateImmutable(
   211  		field.NewPath("Spec", "EnableEncryptionAtHost"),
   212  		old.Spec.EnableEncryptionAtHost,
   213  		m.Spec.EnableEncryptionAtHost); err != nil && old.Spec.EnableEncryptionAtHost != nil {
   214  		allErrs = append(allErrs, err)
   215  	}
   216  
   217  	if !webhookutils.EnsureStringSlicesAreEquivalent(m.Spec.AvailabilityZones, old.Spec.AvailabilityZones) {
   218  		allErrs = append(allErrs,
   219  			field.Invalid(
   220  				field.NewPath("Spec", "AvailabilityZones"),
   221  				m.Spec.AvailabilityZones,
   222  				"field is immutable"))
   223  	}
   224  
   225  	if m.Spec.Mode != string(NodePoolModeSystem) && old.Spec.Mode == string(NodePoolModeSystem) {
   226  		// validate for last system node pool
   227  		if err := validateLastSystemNodePool(mw.Client, m.Labels, m.Namespace, m.Annotations); err != nil {
   228  			allErrs = append(allErrs, field.Forbidden(
   229  				field.NewPath("Spec", "Mode"),
   230  				"Cannot change node pool mode to User, you must have at least one System node pool in your cluster"))
   231  		}
   232  	}
   233  
   234  	if err := webhookutils.ValidateImmutable(
   235  		field.NewPath("Spec", "MaxPods"),
   236  		old.Spec.MaxPods,
   237  		m.Spec.MaxPods); err != nil {
   238  		allErrs = append(allErrs, err)
   239  	}
   240  
   241  	if err := webhookutils.ValidateImmutable(
   242  		field.NewPath("Spec", "OsDiskType"),
   243  		old.Spec.OsDiskType,
   244  		m.Spec.OsDiskType); err != nil {
   245  		allErrs = append(allErrs, err)
   246  	}
   247  
   248  	if err := webhookutils.ValidateImmutable(
   249  		field.NewPath("Spec", "ScaleSetPriority"),
   250  		old.Spec.ScaleSetPriority,
   251  		m.Spec.ScaleSetPriority); err != nil {
   252  		allErrs = append(allErrs, err)
   253  	}
   254  
   255  	if err := webhookutils.ValidateImmutable(
   256  		field.NewPath("Spec", "EnableUltraSSD"),
   257  		old.Spec.EnableUltraSSD,
   258  		m.Spec.EnableUltraSSD); err != nil {
   259  		allErrs = append(allErrs, err)
   260  	}
   261  	if err := webhookutils.ValidateImmutable(
   262  		field.NewPath("Spec", "EnableNodePublicIP"),
   263  		old.Spec.EnableNodePublicIP,
   264  		m.Spec.EnableNodePublicIP); err != nil {
   265  		allErrs = append(allErrs, err)
   266  	}
   267  	if err := webhookutils.ValidateImmutable(
   268  		field.NewPath("Spec", "NodePublicIPPrefixID"),
   269  		old.Spec.NodePublicIPPrefixID,
   270  		m.Spec.NodePublicIPPrefixID); err != nil {
   271  		allErrs = append(allErrs, err)
   272  	}
   273  
   274  	if err := webhookutils.ValidateImmutable(
   275  		field.NewPath("Spec", "KubeletConfig"),
   276  		old.Spec.KubeletConfig,
   277  		m.Spec.KubeletConfig); err != nil {
   278  		allErrs = append(allErrs, err)
   279  	}
   280  
   281  	if err := webhookutils.ValidateImmutable(
   282  		field.NewPath("Spec", "KubeletDiskType"),
   283  		old.Spec.KubeletDiskType,
   284  		m.Spec.KubeletDiskType); err != nil {
   285  		allErrs = append(allErrs, err)
   286  	}
   287  
   288  	if err := webhookutils.ValidateImmutable(
   289  		field.NewPath("Spec", "LinuxOSConfig"),
   290  		old.Spec.LinuxOSConfig,
   291  		m.Spec.LinuxOSConfig); err != nil {
   292  		allErrs = append(allErrs, err)
   293  	}
   294  
   295  	if len(allErrs) != 0 {
   296  		return nil, apierrors.NewInvalid(GroupVersion.WithKind(AzureManagedMachinePoolKind).GroupKind(), m.Name, allErrs)
   297  	}
   298  
   299  	return nil, nil
   300  }
   301  
   302  // ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
   303  func (mw *azureManagedMachinePoolWebhook) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
   304  	m, ok := obj.(*AzureManagedMachinePool)
   305  	if !ok {
   306  		return nil, apierrors.NewBadRequest("expected an AzureManagedMachinePool")
   307  	}
   308  	if m.Spec.Mode != string(NodePoolModeSystem) {
   309  		return nil, nil
   310  	}
   311  
   312  	return nil, errors.Wrapf(validateLastSystemNodePool(mw.Client, m.Labels, m.Namespace, m.Annotations), "if the delete is triggered via owner MachinePool please refer to trouble shooting section in https://capz.sigs.k8s.io/topics/managedcluster.html")
   313  }
   314  
   315  // validateLastSystemNodePool is used to check if the existing system node pool is the last system node pool.
   316  // If it is a last system node pool it cannot be deleted or mutated to user node pool as AKS expects min 1 system node pool.
   317  func validateLastSystemNodePool(cli client.Client, labels map[string]string, namespace string, annotations map[string]string) error {
   318  	ctx := context.Background()
   319  
   320  	// Fetch the Cluster.
   321  	clusterName, ok := labels[clusterv1.ClusterNameLabel]
   322  	if !ok {
   323  		return nil
   324  	}
   325  
   326  	ownerCluster := &clusterv1.Cluster{}
   327  	key := client.ObjectKey{
   328  		Namespace: namespace,
   329  		Name:      clusterName,
   330  	}
   331  
   332  	if err := cli.Get(ctx, key, ownerCluster); err != nil {
   333  		return err
   334  	}
   335  
   336  	if !ownerCluster.DeletionTimestamp.IsZero() {
   337  		return nil
   338  	}
   339  
   340  	// checking if this AzureManagedMachinePool is going to be deleted for clusterctl move operation
   341  	if _, ok := annotations[clusterctlv1alpha3.DeleteForMoveAnnotation]; ok {
   342  		return nil
   343  	}
   344  
   345  	opt1 := client.InNamespace(namespace)
   346  	opt2 := client.MatchingLabels(map[string]string{
   347  		clusterv1.ClusterNameLabel: clusterName,
   348  		LabelAgentPoolMode:         string(NodePoolModeSystem),
   349  	})
   350  
   351  	ammpList := &AzureManagedMachinePoolList{}
   352  	if err := cli.List(ctx, ammpList, opt1, opt2); err != nil {
   353  		return err
   354  	}
   355  
   356  	if len(ammpList.Items) <= 1 {
   357  		return errors.New("AKS Cluster must have at least one system pool")
   358  	}
   359  	return nil
   360  }
   361  
   362  func validateMaxPods(maxPods *int, fldPath *field.Path) error {
   363  	if maxPods != nil {
   364  		if ptr.Deref(maxPods, 0) < 10 || ptr.Deref(maxPods, 0) > 250 {
   365  			return field.Invalid(
   366  				fldPath,
   367  				maxPods,
   368  				"MaxPods must be between 10 and 250")
   369  		}
   370  	}
   371  
   372  	return nil
   373  }
   374  
   375  func validateOSType(mode string, osType *string, fldPath *field.Path) error {
   376  	if mode == string(NodePoolModeSystem) {
   377  		if osType != nil && *osType != LinuxOS {
   378  			return field.Forbidden(
   379  				fldPath,
   380  				"System node pooll must have OSType 'Linux'")
   381  		}
   382  	}
   383  
   384  	return nil
   385  }
   386  
   387  func validateMPName(mpName string, specName *string, osType *string, fldPath *field.Path) error {
   388  	var name *string
   389  	var fieldNameMessage string
   390  	if specName == nil || *specName == "" {
   391  		name = &mpName
   392  		fieldNameMessage = "when spec.name is empty, metadata.name"
   393  	} else {
   394  		name = specName
   395  		fieldNameMessage = "spec.name"
   396  	}
   397  
   398  	if err := validateNameLength(osType, name, fieldNameMessage, fldPath); err != nil {
   399  		return err
   400  	}
   401  	return validateNamePattern(name, fieldNameMessage, fldPath)
   402  }
   403  
   404  func validateNameLength(osType *string, name *string, fieldNameMessage string, fldPath *field.Path) error {
   405  	if osType != nil && *osType == WindowsOS &&
   406  		name != nil && len(*name) > 6 {
   407  		return field.Invalid(
   408  			fldPath,
   409  			name,
   410  			fmt.Sprintf("For OSType Windows, %s can not be longer than 6 characters.", fieldNameMessage))
   411  	} else if (osType == nil || *osType == LinuxOS) &&
   412  		(name != nil && len(*name) > 12) {
   413  		return field.Invalid(
   414  			fldPath,
   415  			osType,
   416  			fmt.Sprintf("For OSType Linux, %s can not be longer than 12 characters.", fieldNameMessage))
   417  	}
   418  	return nil
   419  }
   420  
   421  func validateNamePattern(name *string, fieldNameMessage string, fldPath *field.Path) error {
   422  	if name == nil || *name == "" {
   423  		return nil
   424  	}
   425  
   426  	if !unicode.IsLower(rune((*name)[0])) {
   427  		return field.Invalid(
   428  			fldPath,
   429  			name,
   430  			fmt.Sprintf("%s must begin with a lowercase letter.", fieldNameMessage))
   431  	}
   432  
   433  	for _, char := range *name {
   434  		if !(unicode.IsLower(char) || unicode.IsNumber(char)) {
   435  			return field.Invalid(
   436  				fldPath,
   437  				name,
   438  				fmt.Sprintf("%s may only contain lowercase alphanumeric characters.", fieldNameMessage))
   439  		}
   440  	}
   441  	return nil
   442  }
   443  
   444  func validateNodeLabels(nodeLabels map[string]string, fldPath *field.Path) error {
   445  	for key := range nodeLabels {
   446  		if azureutil.IsAzureSystemNodeLabelKey(key) {
   447  			return field.Invalid(
   448  				fldPath,
   449  				key,
   450  				fmt.Sprintf("Node pool label key must not start with %s", azureutil.AzureSystemNodeLabelPrefix))
   451  		}
   452  	}
   453  
   454  	return nil
   455  }
   456  
   457  func validateNodePublicIPPrefixID(nodePublicIPPrefixID *string, fldPath *field.Path) error {
   458  	if nodePublicIPPrefixID != nil && !validNodePublicPrefixID.MatchString(*nodePublicIPPrefixID) {
   459  		return field.Invalid(
   460  			fldPath,
   461  			nodePublicIPPrefixID,
   462  			fmt.Sprintf("resource ID must match %q", validNodePublicPrefixID.String()))
   463  	}
   464  	return nil
   465  }
   466  
   467  func validateEnableNodePublicIP(enableNodePublicIP *bool, nodePublicIPPrefixID *string, fldPath *field.Path) error {
   468  	if (enableNodePublicIP == nil || !*enableNodePublicIP) &&
   469  		nodePublicIPPrefixID != nil {
   470  		return field.Invalid(
   471  			fldPath,
   472  			enableNodePublicIP,
   473  			"must be set to true when NodePublicIPPrefixID is set")
   474  	}
   475  	return nil
   476  }
   477  
   478  func validateMPSubnetName(subnetName *string, fldPath *field.Path) error {
   479  	if subnetName != nil {
   480  		subnetRegex := "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,78}[a-zA-Z0-9]$"
   481  		regex := regexp.MustCompile(subnetRegex)
   482  		if success := regex.MatchString(ptr.Deref(subnetName, "")); !success {
   483  			return field.Invalid(fldPath, subnetName,
   484  				fmt.Sprintf("name of subnet doesn't match regex %s", subnetRegex))
   485  		}
   486  	}
   487  	return nil
   488  }
   489  
   490  // validateKubeletConfig enforces the AKS API configuration for KubeletConfig.
   491  // See:  https://learn.microsoft.com/en-us/azure/aks/custom-node-configuration.
   492  func validateKubeletConfig(kubeletConfig *KubeletConfig, fldPath *field.Path) error {
   493  	var allowedUnsafeSysctlsPatterns = []string{
   494  		`^kernel\.shm.+$`,
   495  		`^kernel\.msg.+$`,
   496  		`^kernel\.sem$`,
   497  		`^fs\.mqueue\..+$`,
   498  		`^net\..+$`,
   499  	}
   500  	if kubeletConfig != nil {
   501  		if kubeletConfig.CPUCfsQuotaPeriod != nil {
   502  			if !strings.HasSuffix(ptr.Deref(kubeletConfig.CPUCfsQuotaPeriod, ""), "ms") {
   503  				return field.Invalid(
   504  					fldPath.Child("CPUfsQuotaPeriod"),
   505  					kubeletConfig.CPUCfsQuotaPeriod,
   506  					"must be a string value in milliseconds with a 'ms' suffix, e.g., '100ms'")
   507  			}
   508  		}
   509  		if kubeletConfig.ImageGcHighThreshold != nil && kubeletConfig.ImageGcLowThreshold != nil {
   510  			if ptr.Deref(kubeletConfig.ImageGcLowThreshold, 0) > ptr.Deref(kubeletConfig.ImageGcHighThreshold, 0) {
   511  				return field.Invalid(
   512  					fldPath.Child("ImageGcLowThreshold"),
   513  					kubeletConfig.ImageGcLowThreshold,
   514  					fmt.Sprintf("must not be greater than ImageGcHighThreshold, ImageGcLowThreshold=%d, ImageGcHighThreshold=%d",
   515  						ptr.Deref(kubeletConfig.ImageGcLowThreshold, 0), ptr.Deref(kubeletConfig.ImageGcHighThreshold, 0)))
   516  			}
   517  		}
   518  		for _, val := range kubeletConfig.AllowedUnsafeSysctls {
   519  			var hasMatch bool
   520  			for _, p := range allowedUnsafeSysctlsPatterns {
   521  				if m, _ := regexp.MatchString(p, val); m {
   522  					hasMatch = true
   523  					break
   524  				}
   525  			}
   526  			if !hasMatch {
   527  				return field.Invalid(
   528  					fldPath.Child("AllowedUnsafeSysctls"),
   529  					kubeletConfig.AllowedUnsafeSysctls,
   530  					fmt.Sprintf("%s is not a supported AllowedUnsafeSysctls configuration", val))
   531  			}
   532  		}
   533  	}
   534  	return nil
   535  }
   536  
   537  // validateLinuxOSConfig enforces AKS API configuration for Linux OS custom configuration
   538  // See: https://learn.microsoft.com/en-us/azure/aks/custom-node-configuration#linux-os-custom-configuration for detailed information.
   539  func validateLinuxOSConfig(linuxOSConfig *LinuxOSConfig, kubeletConfig *KubeletConfig, fldPath *field.Path) error {
   540  	var errs []error
   541  	if linuxOSConfig == nil {
   542  		return nil
   543  	}
   544  
   545  	if linuxOSConfig.SwapFileSizeMB != nil {
   546  		if kubeletConfig == nil || ptr.Deref(kubeletConfig.FailSwapOn, true) {
   547  			errs = append(errs, field.Invalid(
   548  				fldPath.Child("SwapFileSizeMB"),
   549  				linuxOSConfig.SwapFileSizeMB,
   550  				"KubeletConfig.FailSwapOn must be set to false to enable swap file on nodes"))
   551  		}
   552  	}
   553  
   554  	if linuxOSConfig.Sysctls != nil && linuxOSConfig.Sysctls.NetIpv4IPLocalPortRange != nil {
   555  		// match numbers separated by a space
   556  		portRangeRegex := `^[0-9]+ [0-9]+$`
   557  		portRange := *linuxOSConfig.Sysctls.NetIpv4IPLocalPortRange
   558  
   559  		match, matchErr := regexp.MatchString(portRangeRegex, portRange)
   560  		if matchErr != nil {
   561  			errs = append(errs, matchErr)
   562  		}
   563  		if !match {
   564  			errs = append(errs, field.Invalid(
   565  				fldPath.Child("NetIpv4IpLocalPortRange"),
   566  				linuxOSConfig.Sysctls.NetIpv4IPLocalPortRange,
   567  				"LinuxOSConfig.Sysctls.NetIpv4IpLocalPortRange must be of the format \"<int> <int>\""))
   568  		} else {
   569  			ports := strings.Split(portRange, " ")
   570  			firstPort, _ := strconv.Atoi(ports[0])
   571  			lastPort, _ := strconv.Atoi(ports[1])
   572  
   573  			if firstPort < 1024 || firstPort > 60999 {
   574  				errs = append(errs, field.Invalid(
   575  					fldPath.Child("NetIpv4IpLocalPortRange", "First"),
   576  					linuxOSConfig.Sysctls.NetIpv4IPLocalPortRange,
   577  					fmt.Sprintf("first port of NetIpv4IpLocalPortRange=%d must be in between [1024 - 60999]", firstPort)))
   578  			}
   579  
   580  			if lastPort < 32768 || lastPort > 65000 {
   581  				errs = append(errs, field.Invalid(
   582  					fldPath.Child("NetIpv4IpLocalPortRange", "Last"),
   583  					linuxOSConfig.Sysctls.NetIpv4IPLocalPortRange,
   584  					fmt.Sprintf("last port of NetIpv4IpLocalPortRange=%d must be in between [32768 -65000]", lastPort)))
   585  			}
   586  
   587  			if firstPort > lastPort {
   588  				errs = append(errs, field.Invalid(
   589  					fldPath.Child("NetIpv4IpLocalPortRange", "First"),
   590  					linuxOSConfig.Sysctls.NetIpv4IPLocalPortRange,
   591  					fmt.Sprintf("first port of NetIpv4IpLocalPortRange=%d cannot be greater than last port of NetIpv4IpLocalPortRange=%d", firstPort, lastPort)))
   592  			}
   593  		}
   594  	}
   595  	return kerrors.NewAggregate(errs)
   596  }