@@ -469,14 +469,161 @@ 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 if ! isValidInstanceFamily (spec .InstanceFamily ) {
504+ allErrs = append (allErrs , field .Invalid (field .NewPath ("spec.dynamicHostAllocation.instanceFamily" ), spec .InstanceFamily , "invalid instance family format" ))
505+ }
506+
507+ // Validate quantity if specified
508+ if spec .Quantity != nil {
509+ if * spec .Quantity < 1 || * spec .Quantity > 10 {
510+ allErrs = append (allErrs , field .Invalid (field .NewPath ("spec.dynamicHostAllocation.quantity" ), * spec .Quantity , "quantity must be between 1 and 10" ))
511+ }
512+ }
513+
514+ // Validate instance type format if specified
515+ if spec .InstanceType != nil && * spec .InstanceType != "" {
516+ if ! isValidInstanceType (* spec .InstanceType ) {
517+ allErrs = append (allErrs , field .Invalid (field .NewPath ("spec.dynamicHostAllocation.instanceType" ), * spec .InstanceType , "invalid instance type format" ))
518+ }
519+
520+ // Check consistency between instance family and instance type
521+ expectedFamily := extractInstanceFamily (* spec .InstanceType )
522+ if expectedFamily != spec .InstanceFamily {
523+ allErrs = append (allErrs , field .Invalid (field .NewPath ("spec.dynamicHostAllocation.instanceType" ), * spec .InstanceType ,
524+ fmt .Sprintf ("instance type %s does not match specified instance family %s" , * spec .InstanceType , spec .InstanceFamily )))
525+ }
526+ }
527+
528+ // Validate availability zone format if specified
529+ if spec .AvailabilityZone != nil && * spec .AvailabilityZone != "" {
530+ if ! isValidAvailabilityZone (* spec .AvailabilityZone ) {
531+ allErrs = append (allErrs , field .Invalid (field .NewPath ("spec.dynamicHostAllocation.availabilityZone" ), * spec .AvailabilityZone , "invalid availability zone format" ))
532+ }
533+ }
534+
477535 return allErrs
478536}
479537
480538func (r * AWSMachine ) validateSSHKeyName () field.ErrorList {
481539 return validateSSHKeyName (r .Spec .SSHKeyName )
482540}
541+
542+ // isValidInstanceFamily validates the format of an EC2 instance family.
543+ func isValidInstanceFamily (family string ) bool {
544+ // Instance families typically follow patterns like: m5, c5, r5, t3, etc.
545+ // Allow alphanumeric characters, must start with a letter
546+ if len (family ) < 2 || len (family ) > 10 {
547+ return false
548+ }
549+
550+ for i , char := range family {
551+ if i == 0 {
552+ // First character must be a letter
553+ if (char < 'a' || char > 'z' ) && (char < 'A' || char > 'Z' ) {
554+ return false
555+ }
556+ } else {
557+ // Subsequent characters can be letters or numbers
558+ if (char < 'a' || char > 'z' ) && (char < 'A' || char > 'Z' ) && (char < '0' || char > '9' ) {
559+ return false
560+ }
561+ }
562+ }
563+ return true
564+ }
565+
566+ // isValidInstanceType validates the format of an EC2 instance type.
567+ func isValidInstanceType (instanceType string ) bool {
568+ // Instance types follow the pattern: family.size (e.g., m5.large, c5.xlarge)
569+ parts := strings .Split (instanceType , "." )
570+ if len (parts ) != 2 {
571+ return false
572+ }
573+
574+ family , size := parts [0 ], parts [1 ]
575+
576+ // Validate family part
577+ if ! isValidInstanceFamily (family ) {
578+ return false
579+ }
580+
581+ // Validate size part - common sizes include: nano, micro, small, medium, large, xlarge, 2xlarge, etc.
582+ validSizes := map [string ]bool {
583+ "nano" : true , "micro" : true , "small" : true , "medium" : true , "large" : true ,
584+ "xlarge" : true , "2xlarge" : true , "3xlarge" : true , "4xlarge" : true , "6xlarge" : true ,
585+ "8xlarge" : true , "9xlarge" : true , "10xlarge" : true , "12xlarge" : true , "16xlarge" : true ,
586+ "18xlarge" : true , "24xlarge" : true , "32xlarge" : true , "48xlarge" : true , "56xlarge" : true ,
587+ "112xlarge" : true , "224xlarge" : true , "metal" : true ,
588+ }
589+
590+ return validSizes [size ]
591+ }
592+
593+ // isValidAvailabilityZone validates the format of an AWS availability zone.
594+ func isValidAvailabilityZone (az string ) bool {
595+ // AZ format: region + zone letter (e.g., us-west-2a, eu-central-1b)
596+ if len (az ) < 4 {
597+ return false
598+ }
599+
600+ // Should end with a single letter
601+ lastChar := az [len (az )- 1 ]
602+ if (lastChar < 'a' || lastChar > 'z' ) && (lastChar < 'A' || lastChar > 'Z' ) {
603+ return false
604+ }
605+
606+ // The rest should be a valid region format (contains dashes and alphanumeric)
607+ region := az [:len (az )- 1 ]
608+ if len (region ) < 3 {
609+ return false
610+ }
611+
612+ // Basic validation for region format
613+ for _ , char := range region {
614+ if (char < 'a' || char > 'z' ) && (char < 'A' || char > 'Z' ) && (char < '0' || char > '9' ) && char != '-' {
615+ return false
616+ }
617+ }
618+
619+ return true
620+ }
621+
622+ // extractInstanceFamily extracts the instance family from an instance type.
623+ func extractInstanceFamily (instanceType string ) string {
624+ parts := strings .Split (instanceType , "." )
625+ if len (parts ) < 2 {
626+ return instanceType
627+ }
628+ return parts [0 ]
629+ }
0 commit comments