@@ -460,14 +460,164 @@ func (r *AWSMachine) validateAdditionalSecurityGroups() field.ErrorList {
460
460
func (r * AWSMachine ) validateHostAffinity () field.ErrorList {
461
461
var allErrs field.ErrorList
462
462
463
+ // Validate static host allocation
463
464
if r .Spec .HostAffinity != nil {
464
465
if r .Spec .HostID == nil || len (* r .Spec .HostID ) == 0 {
465
466
allErrs = append (allErrs , field .Required (field .NewPath ("spec.hostID" ), "hostID must be set when hostAffinity is configured" ))
466
467
}
467
468
}
469
+
470
+ // Validate dynamic host allocation
471
+ if r .Spec .DynamicHostAllocation != nil {
472
+ // Mutual exclusivity check
473
+ if r .Spec .HostID != nil {
474
+ allErrs = append (allErrs , field .Forbidden (field .NewPath ("spec.hostID" ), "cannot specify both hostID and dynamicHostAllocation" ))
475
+ }
476
+ if r .Spec .HostAffinity != nil {
477
+ allErrs = append (allErrs , field .Forbidden (field .NewPath ("spec.hostAffinity" ), "cannot specify both hostAffinity and dynamicHostAllocation" ))
478
+ }
479
+
480
+ // Validate dynamic allocation spec
481
+ allErrs = append (allErrs , r .validateDynamicHostAllocation ()... )
482
+ }
483
+
484
+ return allErrs
485
+ }
486
+
487
+ func (r * AWSMachine ) validateDynamicHostAllocation () field.ErrorList {
488
+ var allErrs field.ErrorList
489
+ spec := r .Spec .DynamicHostAllocation
490
+
491
+ // Validate instance family is required
492
+ if spec .InstanceFamily == "" {
493
+ allErrs = append (allErrs , field .Required (field .NewPath ("spec.dynamicHostAllocation.instanceFamily" ), "instanceFamily is required" ))
494
+ } else {
495
+ // Validate instance family format
496
+ if ! isValidInstanceFamily (spec .InstanceFamily ) {
497
+ allErrs = append (allErrs , field .Invalid (field .NewPath ("spec.dynamicHostAllocation.instanceFamily" ), spec .InstanceFamily , "invalid instance family format" ))
498
+ }
499
+ }
500
+
501
+ // Validate quantity if specified
502
+ if spec .Quantity != nil {
503
+ if * spec .Quantity < 1 || * spec .Quantity > 10 {
504
+ allErrs = append (allErrs , field .Invalid (field .NewPath ("spec.dynamicHostAllocation.quantity" ), * spec .Quantity , "quantity must be between 1 and 10" ))
505
+ }
506
+ }
507
+
508
+ // Validate instance type format if specified
509
+ if spec .InstanceType != nil && * spec .InstanceType != "" {
510
+ if ! isValidInstanceType (* spec .InstanceType ) {
511
+ allErrs = append (allErrs , field .Invalid (field .NewPath ("spec.dynamicHostAllocation.instanceType" ), * spec .InstanceType , "invalid instance type format" ))
512
+ }
513
+
514
+ // Check consistency between instance family and instance type
515
+ expectedFamily := extractInstanceFamily (* spec .InstanceType )
516
+ if expectedFamily != spec .InstanceFamily {
517
+ allErrs = append (allErrs , field .Invalid (field .NewPath ("spec.dynamicHostAllocation.instanceType" ), * spec .InstanceType ,
518
+ fmt .Sprintf ("instance type %s does not match specified instance family %s" , * spec .InstanceType , spec .InstanceFamily )))
519
+ }
520
+ }
521
+
522
+ // Validate availability zone format if specified
523
+ if spec .AvailabilityZone != nil && * spec .AvailabilityZone != "" {
524
+ if ! isValidAvailabilityZone (* spec .AvailabilityZone ) {
525
+ allErrs = append (allErrs , field .Invalid (field .NewPath ("spec.dynamicHostAllocation.availabilityZone" ), * spec .AvailabilityZone , "invalid availability zone format" ))
526
+ }
527
+ }
528
+
468
529
return allErrs
469
530
}
470
531
471
532
func (r * AWSMachine ) validateSSHKeyName () field.ErrorList {
472
533
return validateSSHKeyName (r .Spec .SSHKeyName )
473
534
}
535
+
536
+ // isValidInstanceFamily validates the format of an EC2 instance family.
537
+ func isValidInstanceFamily (family string ) bool {
538
+ // Instance families typically follow patterns like: m5, c5, r5, t3, etc.
539
+ // Allow alphanumeric characters, must start with a letter
540
+ if len (family ) < 2 || len (family ) > 10 {
541
+ return false
542
+ }
543
+
544
+ for i , char := range family {
545
+ if i == 0 {
546
+ // First character must be a letter
547
+ if ! ((char >= 'a' && char <= 'z' ) || (char >= 'A' && char <= 'Z' )) {
548
+ return false
549
+ }
550
+ } else {
551
+ // Subsequent characters can be letters or numbers
552
+ if ! ((char >= 'a' && char <= 'z' ) || (char >= 'A' && char <= 'Z' ) || (char >= '0' && char <= '9' )) {
553
+ return false
554
+ }
555
+ }
556
+ }
557
+ return true
558
+ }
559
+
560
+ // isValidInstanceType validates the format of an EC2 instance type.
561
+ func isValidInstanceType (instanceType string ) bool {
562
+ // Instance types follow the pattern: family.size (e.g., m5.large, c5.xlarge)
563
+ parts := strings .Split (instanceType , "." )
564
+ if len (parts ) != 2 {
565
+ return false
566
+ }
567
+
568
+ family , size := parts [0 ], parts [1 ]
569
+
570
+ // Validate family part
571
+ if ! isValidInstanceFamily (family ) {
572
+ return false
573
+ }
574
+
575
+ // Validate size part - common sizes include: nano, micro, small, medium, large, xlarge, 2xlarge, etc.
576
+ validSizes := map [string ]bool {
577
+ "nano" : true , "micro" : true , "small" : true , "medium" : true , "large" : true ,
578
+ "xlarge" : true , "2xlarge" : true , "3xlarge" : true , "4xlarge" : true , "6xlarge" : true ,
579
+ "8xlarge" : true , "9xlarge" : true , "10xlarge" : true , "12xlarge" : true , "16xlarge" : true ,
580
+ "18xlarge" : true , "24xlarge" : true , "32xlarge" : true , "48xlarge" : true , "56xlarge" : true ,
581
+ "112xlarge" : true , "224xlarge" : true , "metal" : true ,
582
+ }
583
+
584
+ return validSizes [size ]
585
+ }
586
+
587
+ // isValidAvailabilityZone validates the format of an AWS availability zone.
588
+ func isValidAvailabilityZone (az string ) bool {
589
+ // AZ format: region + zone letter (e.g., us-west-2a, eu-central-1b)
590
+ if len (az ) < 4 {
591
+ return false
592
+ }
593
+
594
+ // Should end with a single letter
595
+ lastChar := az [len (az )- 1 ]
596
+ if ! ((lastChar >= 'a' && lastChar <= 'z' ) || (lastChar >= 'A' && lastChar <= 'Z' )) {
597
+ return false
598
+ }
599
+
600
+ // The rest should be a valid region format (contains dashes and alphanumeric)
601
+ region := az [:len (az )- 1 ]
602
+ if len (region ) < 3 {
603
+ return false
604
+ }
605
+
606
+ // Basic validation for region format
607
+ for _ , char := range region {
608
+ if ! ((char >= 'a' && char <= 'z' ) || (char >= 'A' && char <= 'Z' ) || (char >= '0' && char <= '9' ) || char == '-' ) {
609
+ return false
610
+ }
611
+ }
612
+
613
+ return true
614
+ }
615
+
616
+ // extractInstanceFamily extracts the instance family from an instance type.
617
+ func extractInstanceFamily (instanceType string ) string {
618
+ parts := strings .Split (instanceType , "." )
619
+ if len (parts ) < 2 {
620
+ return instanceType
621
+ }
622
+ return parts [0 ]
623
+ }
0 commit comments