Skip to content

Commit ababcb8

Browse files
committed
WIP: Allocate dedicated host
1 parent 8ab8731 commit ababcb8

19 files changed

+1390
-7
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: 45 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,41 @@ type AWSMachineSpec struct {
255260
CapacityReservationPreference CapacityReservationPreference `json:"capacityReservationPreference,omitempty"`
256261
}
257262

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

437482
// +kubebuilder:object:root=true

api/v1beta2/awsmachine_webhook.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,14 +469,164 @@ 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 {
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

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

api/v1beta2/types.go

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

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

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

0 commit comments

Comments
 (0)