Skip to content

Commit 2dd765e

Browse files
rvanderp3jimzim
andcommitted
WIP: Allocate dedicated host
Co-authored-by: Jim Zimmerman <[email protected]>
1 parent 8ab8731 commit 2dd765e

21 files changed

+1398
-9
lines changed

api/v1beta1/conversion.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,8 @@ func Convert_v1beta2_S3Bucket_To_v1beta1_S3Bucket(in *v1beta2.S3Bucket, out *S3B
103103
func Convert_v1beta2_Ignition_To_v1beta1_Ignition(in *v1beta2.Ignition, out *Ignition, s conversion.Scope) error {
104104
return autoConvert_v1beta2_Ignition_To_v1beta1_Ignition(in, out, s)
105105
}
106+
107+
func Convert_v1beta2_AWSMachineStatus_To_v1beta1_AWSMachineStatus(in *v1beta2.AWSMachineStatus, out *AWSMachineStatus, s conversion.Scope) error {
108+
// Note: AllocatedHostID is not present in v1beta1, so it will be dropped during conversion
109+
return autoConvert_v1beta2_AWSMachineStatus_To_v1beta1_AWSMachineStatus(in, out, s)
110+
}

api/v1beta1/zz_generated.conversion.go

Lines changed: 3 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1beta2/awsmachine_types.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,11 @@ type AWSMachineSpec struct {
246246
// +kubebuilder:validation:Enum:=default;host
247247
HostAffinity *string `json:"hostAffinity,omitempty"`
248248

249+
// DynamicHostAllocation enables automatic allocation of dedicated hosts.
250+
// This field is mutually exclusive with HostID.
251+
// +optional
252+
DynamicHostAllocation *DynamicHostAllocationSpec `json:"dynamicHostAllocation,omitempty"`
253+
249254
// CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include:
250255
// "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType
251256
// "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads
@@ -255,6 +260,40 @@ type AWSMachineSpec struct {
255260
CapacityReservationPreference CapacityReservationPreference `json:"capacityReservationPreference,omitempty"`
256261
}
257262

263+
// DynamicHostAllocationSpec defines the configuration for dynamic dedicated host allocation.
264+
type DynamicHostAllocationSpec struct {
265+
// InstanceFamily specifies the EC2 instance family (e.g., "m5", "c5", "r5").
266+
// +kubebuilder:validation:Required
267+
InstanceFamily string `json:"instanceFamily"`
268+
269+
// AvailabilityZone specifies the target availability zone for allocation.
270+
// If not specified, uses the same AZ as the instance.
271+
// +optional
272+
AvailabilityZone *string `json:"availabilityZone,omitempty"`
273+
274+
// InstanceType specifies the specific instance type for the dedicated host.
275+
// If not specified, derives from InstanceFamily.
276+
// +optional
277+
InstanceType *string `json:"instanceType,omitempty"`
278+
279+
// Quantity specifies the number of dedicated hosts to allocate.
280+
// +kubebuilder:validation:Minimum=1
281+
// +kubebuilder:validation:Maximum=10
282+
// +kubebuilder:default=1
283+
// +optional
284+
Quantity *int32 `json:"quantity,omitempty"`
285+
286+
// AutoRelease determines whether to automatically release the dedicated host
287+
// when the machine is deleted.
288+
// +kubebuilder:default=true
289+
// +optional
290+
AutoRelease *bool `json:"autoRelease,omitempty"`
291+
292+
// Tags to apply to the allocated dedicated host.
293+
// +optional
294+
Tags map[string]string `json:"tags,omitempty"`
295+
}
296+
258297
// CloudInit defines options related to the bootstrapping systems where
259298
// CloudInit is used.
260299
type CloudInit struct {
@@ -432,6 +471,11 @@ type AWSMachineStatus struct {
432471
// Conditions defines current service state of the AWSMachine.
433472
// +optional
434473
Conditions clusterv1.Conditions `json:"conditions,omitempty"`
474+
475+
// AllocatedHostID tracks the dynamically allocated dedicated host ID.
476+
// This field is populated when DynamicHostAllocation is used.
477+
// +optional
478+
AllocatedHostID *string `json:"allocatedHostID,omitempty"`
435479
}
436480

437481
// +kubebuilder:object:root=true

api/v1beta2/awsmachine_webhook.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,14 +469,161 @@ func (r *AWSMachine) validateAdditionalSecurityGroups() field.ErrorList {
469469
func (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

480538
func (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+
}

api/v1beta2/types.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,11 @@ type Instance struct {
286286
// +optional
287287
HostID *string `json:"hostID,omitempty"`
288288

289+
// DynamicHostAllocation enables automatic allocation of dedicated hosts.
290+
// This field is mutually exclusive with HostID.
291+
// +optional
292+
DynamicHostAllocation *DynamicHostAllocationSpec `json:"dynamicHostAllocation,omitempty"`
293+
289294
// CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include:
290295
// "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType
291296
// "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads
@@ -311,6 +316,33 @@ const (
311316
CapacityReservationPreferenceOpen CapacityReservationPreference = "Open"
312317
)
313318

319+
// DedicatedHostInfo contains information about a dedicated host.
320+
type DedicatedHostInfo struct {
321+
// HostID is the ID of the dedicated host.
322+
HostID string `json:"hostID"`
323+
324+
// InstanceFamily is the instance family supported by the host.
325+
InstanceFamily string `json:"instanceFamily"`
326+
327+
// InstanceType is the instance type supported by the host.
328+
InstanceType string `json:"instanceType"`
329+
330+
// AvailabilityZone is the AZ where the host is located.
331+
AvailabilityZone string `json:"availabilityZone"`
332+
333+
// State is the current state of the dedicated host.
334+
State string `json:"state"`
335+
336+
// TotalCapacity is the total number of instances that can be launched on the host.
337+
TotalCapacity int32 `json:"totalCapacity"`
338+
339+
// AvailableCapacity is the number of instances that can still be launched on the host.
340+
AvailableCapacity int32 `json:"availableCapacity"`
341+
342+
// Tags associated with the dedicated host.
343+
Tags map[string]string `json:"tags,omitempty"`
344+
}
345+
314346
// MarketType describes the market type of an Instance
315347
// +kubebuilder:validation:Enum:=OnDemand;Spot;CapacityBlock
316348
type MarketType string

0 commit comments

Comments
 (0)