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 }