Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions api/v1beta1/awscluster_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error {
dst.Status.Bastion.HostID = restored.Status.Bastion.HostID
dst.Status.Bastion.CapacityReservationPreference = restored.Status.Bastion.CapacityReservationPreference
dst.Status.Bastion.CPUOptions = restored.Status.Bastion.CPUOptions
if restored.Status.Bastion.DynamicHostAllocation != nil {
dst.Status.Bastion.DynamicHostAllocation = restored.Status.Bastion.DynamicHostAllocation
}
}
dst.Spec.Partition = restored.Spec.Partition

Expand Down
11 changes: 11 additions & 0 deletions api/v1beta1/awsmachine_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ func (src *AWSMachine) ConvertTo(dstRaw conversion.Hub) error {
dst.Spec.CapacityReservationPreference = restored.Spec.CapacityReservationPreference
dst.Spec.NetworkInterfaceType = restored.Spec.NetworkInterfaceType
dst.Spec.CPUOptions = restored.Spec.CPUOptions
if restored.Spec.DynamicHostAllocation != nil {
dst.Spec.DynamicHostAllocation = restored.Spec.DynamicHostAllocation
}
if restored.Spec.ElasticIPPool != nil {
if dst.Spec.ElasticIPPool == nil {
dst.Spec.ElasticIPPool = &infrav1.ElasticIPPool{}
Expand All @@ -61,6 +64,11 @@ func (src *AWSMachine) ConvertTo(dstRaw conversion.Hub) error {
}
}

dst.Status.DedicatedHost = restored.Status.DedicatedHost
dst.Status.HostReleaseAttempts = restored.Status.HostReleaseAttempts
dst.Status.LastHostReleaseAttempt = restored.Status.LastHostReleaseAttempt
dst.Status.HostReleaseFailedReason = restored.Status.HostReleaseFailedReason

return nil
}

Expand Down Expand Up @@ -117,6 +125,9 @@ func (r *AWSMachineTemplate) ConvertTo(dstRaw conversion.Hub) error {
dst.Spec.Template.Spec.CapacityReservationPreference = restored.Spec.Template.Spec.CapacityReservationPreference
dst.Spec.Template.Spec.NetworkInterfaceType = restored.Spec.Template.Spec.NetworkInterfaceType
dst.Spec.Template.Spec.CPUOptions = restored.Spec.Template.Spec.CPUOptions
if restored.Spec.Template.Spec.DynamicHostAllocation != nil {
dst.Spec.Template.Spec.DynamicHostAllocation = restored.Spec.Template.Spec.DynamicHostAllocation
}
if restored.Spec.Template.Spec.ElasticIPPool != nil {
if dst.Spec.Template.Spec.ElasticIPPool == nil {
dst.Spec.Template.Spec.ElasticIPPool = &infrav1.ElasticIPPool{}
Expand Down
5 changes: 5 additions & 0 deletions api/v1beta1/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,8 @@ func Convert_v1beta2_S3Bucket_To_v1beta1_S3Bucket(in *v1beta2.S3Bucket, out *S3B
func Convert_v1beta2_Ignition_To_v1beta1_Ignition(in *v1beta2.Ignition, out *Ignition, s conversion.Scope) error {
return autoConvert_v1beta2_Ignition_To_v1beta1_Ignition(in, out, s)
}

func Convert_v1beta2_AWSMachineStatus_To_v1beta1_AWSMachineStatus(in *v1beta2.AWSMachineStatus, out *AWSMachineStatus, s conversion.Scope) error {
// Note: DedicatedHostID is not present in v1beta1, so it will be dropped during conversion
return autoConvert_v1beta2_AWSMachineStatus_To_v1beta1_AWSMachineStatus(in, out, s)
}
11 changes: 6 additions & 5 deletions api/v1beta1/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 56 additions & 2 deletions api/v1beta2/awsmachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ type AWSMachineSpec struct {
PlacementGroupPartition int64 `json:"placementGroupPartition,omitempty"`

// Tenancy indicates if instance should run on shared or single-tenant hardware.
// When Tenancy=host, AWS will attempt to find a suitable host from:
// - Preexisting allocated hosts that have auto-placement enabled
// - A specific host ID, if configured
// - Allocating a new dedicated host if DynamicHostAllocation is configured
// +optional
// +kubebuilder:validation:Enum:=default;dedicated;host
Tenancy string `json:"tenancy,omitempty"`
Expand All @@ -240,17 +244,28 @@ type AWSMachineSpec struct {
MarketType MarketType `json:"marketType,omitempty"`

// HostID specifies the Dedicated Host on which the instance must be started.
// This field is mutually exclusive with DynamicHostAllocation.
// +kubebuilder:validation:Pattern=`^h-[0-9a-f]{17}$`
// +kubebuilder:validation:MaxLength=19
// +optional
HostID *string `json:"hostID,omitempty"`

// HostAffinity specifies the dedicated host affinity setting for the instance.
// When hostAffinity is set to host, an instance started onto a specific host always restarts on the same host if stopped.
// When hostAffinity is set to default, and you stop and restart the instance, it can be restarted on any available host.
// When HostAffinity is set to host, an instance started onto a specific host always restarts on the same host if stopped.
// When HostAffinity is set to default, and you stop and restart the instance, it can be restarted on any available host.
// When HostAffinity is defined, HostID is required.
// +optional
// +kubebuilder:validation:Enum:=default;host
// +kubebuilder:default=host
HostAffinity *string `json:"hostAffinity,omitempty"`

// DynamicHostAllocation enables automatic allocation of a single dedicated host.
// This field is mutually exclusive with HostID and always allocates exactly one host.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe include some more context for consumers here e.g:
"depending on the instance type used, the host allocation for that single instance might be less or more cost effective. This is usually a good fit for metal instance types..."
@punkwalker is it fair to say that the .metal size instance types would always use all the host allocation sockets/cpus for that same type? as in the example here https://aws.amazon.com/blogs/compute/understanding-the-lifecycle-of-amazon-ec2-dedicated-hosts

it would be also nice to document better the .Tenancy field, e.g.
"when .tenancy=host is set, you can either let aws find a suitable host for you from preexisting allocations that have auto-placement enabled, choose an specific .hostID, or use dynamicHostAllocation to create a dedicated host allocation for the instance.

Copy link
Contributor

@punkwalker punkwalker Sep 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it fair to say that the .metal size instance types would always use all the host allocation sockets/cpus for that same type?

It seems that this is mostly true for all instance families but there can be some exceptions as well. However, as we are going with 1:1 mapping, I don't think so we need to consider this as of now.

"when .tenancy=host is set, you can either let aws find a suitable host for you from preexisting allocations that have auto-placement enabled, choose an specific .hostID, or use dynamicHostAllocation to create a dedicated host allocation for the instance.

Similarly for this, if we do 1:1 mapping then we should not use auto-placement as it will be redundant when we place only single instance on the host.

// Cost effectiveness of allocating a single instance on a dedicated host may vary
// depending on the instance type and the region.
// +optional
DynamicHostAllocation *DynamicHostAllocationSpec `json:"dynamicHostAllocation,omitempty"`

// CapacityReservationPreference specifies the preference for use of Capacity Reservations by the instance. Valid values include:
// "Open": The instance may make use of open Capacity Reservations that match its AZ and InstanceType
// "None": The instance may not make use of any Capacity Reservations. This is to conserve open reservations for desired workloads
Expand All @@ -260,6 +275,14 @@ type AWSMachineSpec struct {
CapacityReservationPreference CapacityReservationPreference `json:"capacityReservationPreference,omitempty"`
}

// DynamicHostAllocationSpec defines the configuration for dynamic dedicated host allocation.
// This specification always allocates exactly one dedicated host per machine.
type DynamicHostAllocationSpec struct {
// Tags to apply to the allocated dedicated host.
// +optional
Tags map[string]string `json:"tags,omitempty"`
}
Comment on lines +281 to +284
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use the same tags that are present on Machine under spec.additionalTags and default tags as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could for sure. will make that change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@punkwalker Ive made the change to include tags specified in spec.additionalTags. I looked at including the default tags as well but it seemed odd to apply some of them to the dedicated host. For example, the role tag that would be attached to the machine is node and that was be propagated to the dedicate host. I assumed default tags meant those that would be applied to the machine but I may have misunderstood. What did you mean by "default tags"?


// CloudInit defines options related to the bootstrapping systems where
// CloudInit is used.
type CloudInit struct {
Expand Down Expand Up @@ -438,6 +461,37 @@ type AWSMachineStatus struct {
// Conditions defines current service state of the AWSMachine.
// +optional
Conditions clusterv1.Conditions `json:"conditions,omitempty"`

// DedicatedHost tracks the dynamically allocated dedicated host.
// This field is populated when DynamicHostAllocation is used.
// +optional
DedicatedHost *DedicatedHostStatus `json:"dedicatedHost,omitempty"`

// HostReleaseAttempts tracks the number of attempts to release the dedicated host.
// +optional
HostReleaseAttempts *int32 `json:"hostReleaseAttempts,omitempty"`

// LastHostReleaseAttempt tracks the timestamp of the last attempt to release the dedicated host.
// +optional
LastHostReleaseAttempt *metav1.Time `json:"lastHostReleaseAttempt,omitempty"`

// HostReleaseFailedReason tracks the reason for the last host release failure.
// +optional
HostReleaseFailedReason *string `json:"hostReleaseFailedReason,omitempty"`
}

// DedicatedHostStatus defines the observed state of a dynamically allocated dedicated host
// associated with an AWSMachine. This struct is used to track the ID of the dedicated host
// and any failure messages encountered during host release operations.
type DedicatedHostStatus struct {
// ID tracks the dynamically allocated dedicated host ID.
// This field is populated when DynamicHostAllocation is used.
// +optional
ID *string `json:"id,omitempty"`

// ReleaseFailureMessage tracks the last failure message for the release host attempt.
// +optional
ReleaseFailureMessage *string `json:"releaseFailureMessage,omitempty"`
}

// +kubebuilder:object:root=true
Expand Down
17 changes: 10 additions & 7 deletions api/v1beta2/awsmachine_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ func (*awsMachineWebhook) ValidateCreate(_ context.Context, obj runtime.Object)
allErrs = append(allErrs, r.validateNonRootVolumes()...)
allErrs = append(allErrs, r.validateSSHKeyName()...)
allErrs = append(allErrs, r.validateAdditionalSecurityGroups()...)
allErrs = append(allErrs, r.validateHostAffinity()...)
allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...)
allErrs = append(allErrs, r.validateNetworkElasticIPPool()...)
allErrs = append(allErrs, r.validateInstanceMarketType()...)
allErrs = append(allErrs, r.validateCapacityReservation()...)
allErrs = append(allErrs, r.validateHostAllocation()...)

return nil, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
}
Expand Down Expand Up @@ -109,7 +109,7 @@ func (*awsMachineWebhook) ValidateUpdate(ctx context.Context, oldObj, newObj run
allErrs = append(allErrs, r.validateCloudInitSecret()...)
allErrs = append(allErrs, r.validateAdditionalSecurityGroups()...)
allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...)
allErrs = append(allErrs, r.validateHostAffinity()...)
allErrs = append(allErrs, r.validateHostAllocation()...)

newAWSMachineSpec := newAWSMachine["spec"].(map[string]interface{})
oldAWSMachineSpec := oldAWSMachine["spec"].(map[string]interface{})
Expand Down Expand Up @@ -474,14 +474,17 @@ func (r *AWSMachine) validateAdditionalSecurityGroups() field.ErrorList {
return allErrs
}

func (r *AWSMachine) validateHostAffinity() field.ErrorList {
func (r *AWSMachine) validateHostAllocation() field.ErrorList {
var allErrs field.ErrorList

if r.Spec.HostAffinity != nil {
if r.Spec.HostID == nil || len(*r.Spec.HostID) == 0 {
allErrs = append(allErrs, field.Required(field.NewPath("spec.hostID"), "hostID must be set when hostAffinity is configured"))
}
// Check if both hostID and dynamicHostAllocation are specified
hasHostID := r.Spec.HostID != nil && len(*r.Spec.HostID) > 0
hasDynamicHostAllocation := r.Spec.DynamicHostAllocation != nil

if hasHostID && hasDynamicHostAllocation {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec.hostID"), "hostID and dynamicHostAllocation are mutually exclusive"), field.Forbidden(field.NewPath("spec.dynamicHostAllocation"), "hostID and dynamicHostAllocation are mutually exclusive"))
}

return allErrs
}

Expand Down
49 changes: 39 additions & 10 deletions api/v1beta2/awsmachine_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,16 +489,6 @@ func TestAWSMachineCreate(t *testing.T) {
},
wantErr: true,
},
{
name: "configure host affinity without Host ID",
machine: &AWSMachine{
Spec: AWSMachineSpec{
InstanceType: "test",
HostAffinity: ptr.To("default"),
},
},
wantErr: true,
},
{
name: "create with valid BYOIPv4",
machine: &AWSMachine{
Expand Down Expand Up @@ -567,6 +557,45 @@ func TestAWSMachineCreate(t *testing.T) {
},
wantErr: true,
},
{
name: "hostID and dynamicHostAllocation are mutually exclusive",
machine: &AWSMachine{
Spec: AWSMachineSpec{
InstanceType: "test",
HostID: aws.String("h-1234567890abcdef0"),
DynamicHostAllocation: &DynamicHostAllocationSpec{
Tags: map[string]string{
"Environment": "test",
},
},
},
},
wantErr: true,
},
{
name: "hostID alone is valid",
machine: &AWSMachine{
Spec: AWSMachineSpec{
InstanceType: "test",
HostID: aws.String("h-1234567890abcdef0"),
},
},
wantErr: false,
},
{
name: "dynamicHostAllocation alone is valid",
machine: &AWSMachine{
Spec: AWSMachineSpec{
InstanceType: "test",
DynamicHostAllocation: &DynamicHostAllocationSpec{
Tags: map[string]string{
"Environment": "test",
},
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
17 changes: 17 additions & 0 deletions api/v1beta2/awsmachinetemplate_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,22 @@ func (r *AWSMachineTemplate) validateIgnitionAndCloudInit() field.ErrorList {

return allErrs
}
func (r *AWSMachineTemplate) validateHostAllocation() field.ErrorList {
var allErrs field.ErrorList

spec := r.Spec.Template.Spec

// Check if both hostID and dynamicHostAllocation are specified
hasHostID := spec.HostID != nil && len(*spec.HostID) > 0
hasDynamicHostAllocation := spec.DynamicHostAllocation != nil

if hasHostID && hasDynamicHostAllocation {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec.template.spec.hostID"), "hostID and dynamicHostAllocation are mutually exclusive"), field.Forbidden(field.NewPath("spec.template.spec.dynamicHostAllocation"), "hostID and dynamicHostAllocation are mutually exclusive"))
}

return allErrs
}

func (r *AWSMachineTemplate) validateSSHKeyName() field.ErrorList {
return validateSSHKeyName(r.Spec.Template.Spec.SSHKeyName)
}
Expand Down Expand Up @@ -205,6 +221,7 @@ func (r *AWSMachineTemplateWebhook) ValidateCreate(_ context.Context, raw runtim
allErrs = append(allErrs, obj.validateSSHKeyName()...)
allErrs = append(allErrs, obj.validateAdditionalSecurityGroups()...)
allErrs = append(allErrs, obj.Spec.Template.Spec.AdditionalTags.Validate()...)
allErrs = append(allErrs, obj.validateHostAllocation()...)

return nil, aggregateObjErrors(obj.GroupVersionKind().GroupKind(), obj.Name, allErrs)
}
Expand Down
20 changes: 20 additions & 0 deletions api/v1beta2/awsmachinetemplate_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,26 @@ func TestAWSMachineTemplateValidateCreate(t *testing.T) {
},
wantError: false,
},
{
name: "hostID and dynamicHostAllocation are mutually exclusive",
inputTemplate: &AWSMachineTemplate{
ObjectMeta: metav1.ObjectMeta{},
Spec: AWSMachineTemplateSpec{
Template: AWSMachineTemplateResource{
Spec: AWSMachineSpec{
InstanceType: "test",
HostID: aws.String("h-1234567890abcdef0"),
DynamicHostAllocation: &DynamicHostAllocationSpec{
Tags: map[string]string{
"Environment": "test",
},
},
},
},
},
},
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
13 changes: 13 additions & 0 deletions api/v1beta2/conditions_consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ const (
// InstanceReadyCondition reports on current status of the EC2 instance. Ready indicates the instance is in a Running state.
InstanceReadyCondition clusterv1.ConditionType = "InstanceReady"

// DedicatedHostReleaseCondition reports on the status of dedicated host release operations.
// This condition tracks whether the dedicated host has been successfully released or if there are failures.
DedicatedHostReleaseCondition clusterv1.ConditionType = "DedicatedHostRelease"

// InstanceNotFoundReason used when the instance couldn't be retrieved.
InstanceNotFoundReason = "InstanceNotFound"
// InstanceTerminatedReason instance is in a terminated state.
Expand Down Expand Up @@ -191,4 +195,13 @@ const (

// S3BucketFailedReason is used when any errors occur during reconciliation of an S3 bucket.
S3BucketFailedReason = "S3BucketCreationFailed"

// DedicatedHostReleaseSucceededReason used when the dedicated host is successfully released.
DedicatedHostReleaseSucceededReason = "DedicatedHostReleaseSucceeded"

// DedicatedHostReleaseFailedReason used when the dedicated host release fails.
DedicatedHostReleaseFailedReason = "DedicatedHostReleaseFailed"

// DedicatedHostReleaseRetryingReason used when the dedicated host release is being retried.
DedicatedHostReleaseRetryingReason = "DedicatedHostReleaseRetrying"
)
Loading