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