@@ -460,14 +460,164 @@ func (r *AWSMachine) validateAdditionalSecurityGroups() field.ErrorList {
460460func (r * AWSMachine ) validateHostAffinity () field.ErrorList {
461461 var allErrs field.ErrorList
462462
463+ // Validate static host allocation
463464 if r .Spec .HostAffinity != nil {
464465 if r .Spec .HostID == nil || len (* r .Spec .HostID ) == 0 {
465466 allErrs = append (allErrs , field .Required (field .NewPath ("spec.hostID" ), "hostID must be set when hostAffinity is configured" ))
466467 }
467468 }
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+
468529 return allErrs
469530}
470531
471532func (r * AWSMachine ) validateSSHKeyName () field.ErrorList {
472533 return validateSSHKeyName (r .Spec .SSHKeyName )
473534}
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