From ded98cd9a95d0dce87695a9454a2be35a864942c Mon Sep 17 00:00:00 2001 From: Pankaj Walke Date: Wed, 12 Nov 2025 18:52:24 +0000 Subject: [PATCH 1/2] Initial changes for supporting HostResourceGroupArn parameter in AWSMachine.Spec Signed-off-by: Pankaj Walke --- api/v1beta1/awscluster_conversion.go | 1 + api/v1beta1/awsmachine_conversion.go | 2 + api/v1beta1/zz_generated.conversion.go | 2 + api/v1beta2/awsmachine_types.go | 8 +++ api/v1beta2/awsmachine_webhook.go | 18 +++++-- api/v1beta2/awsmachine_webhook_test.go | 52 +++++++++++++++++++ api/v1beta2/awsmachinetemplate_webhook.go | 18 +++++-- .../awsmachinetemplate_webhook_test.go | 51 ++++++++++++++++++ api/v1beta2/types.go | 5 ++ api/v1beta2/zz_generated.deepcopy.go | 10 ++++ ...ster.x-k8s.io_awsmanagedcontrolplanes.yaml | 8 +++ ...tructure.cluster.x-k8s.io_awsclusters.yaml | 4 ++ ...tructure.cluster.x-k8s.io_awsmachines.yaml | 6 +++ ....cluster.x-k8s.io_awsmachinetemplates.yaml | 6 +++ pkg/cloud/services/ec2/instances.go | 30 +++++++++++ 15 files changed, 215 insertions(+), 6 deletions(-) diff --git a/api/v1beta1/awscluster_conversion.go b/api/v1beta1/awscluster_conversion.go index 805d60856e..789d3b492b 100644 --- a/api/v1beta1/awscluster_conversion.go +++ b/api/v1beta1/awscluster_conversion.go @@ -65,6 +65,7 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error { dst.Status.Bastion.MarketType = restored.Status.Bastion.MarketType dst.Status.Bastion.HostAffinity = restored.Status.Bastion.HostAffinity dst.Status.Bastion.HostID = restored.Status.Bastion.HostID + dst.Status.Bastion.HostResourceGroupArn = restored.Status.Bastion.HostResourceGroupArn dst.Status.Bastion.CapacityReservationPreference = restored.Status.Bastion.CapacityReservationPreference dst.Status.Bastion.CPUOptions = restored.Status.Bastion.CPUOptions if restored.Status.Bastion.DynamicHostAllocation != nil { diff --git a/api/v1beta1/awsmachine_conversion.go b/api/v1beta1/awsmachine_conversion.go index 85f7a1c36b..a4dbc234bd 100644 --- a/api/v1beta1/awsmachine_conversion.go +++ b/api/v1beta1/awsmachine_conversion.go @@ -45,6 +45,7 @@ func (src *AWSMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.CapacityReservationID = restored.Spec.CapacityReservationID dst.Spec.MarketType = restored.Spec.MarketType dst.Spec.HostID = restored.Spec.HostID + dst.Spec.HostResourceGroupArn = restored.Spec.HostResourceGroupArn dst.Spec.HostAffinity = restored.Spec.HostAffinity dst.Spec.CapacityReservationPreference = restored.Spec.CapacityReservationPreference dst.Spec.NetworkInterfaceType = restored.Spec.NetworkInterfaceType @@ -117,6 +118,7 @@ func (r *AWSMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.CapacityReservationID = restored.Spec.Template.Spec.CapacityReservationID dst.Spec.Template.Spec.MarketType = restored.Spec.Template.Spec.MarketType dst.Spec.Template.Spec.HostID = restored.Spec.Template.Spec.HostID + dst.Spec.Template.Spec.HostResourceGroupArn = restored.Spec.Template.Spec.HostResourceGroupArn dst.Spec.Template.Spec.HostAffinity = restored.Spec.Template.Spec.HostAffinity dst.Spec.Template.Spec.CapacityReservationPreference = restored.Spec.Template.Spec.CapacityReservationPreference dst.Spec.Template.Spec.NetworkInterfaceType = restored.Spec.Template.Spec.NetworkInterfaceType diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index b8e74d2ab6..91021f7161 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -1437,6 +1437,7 @@ func autoConvert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(in *v1beta2.AW // WARNING: in.CapacityReservationID requires manual conversion: does not exist in peer-type // WARNING: in.MarketType requires manual conversion: does not exist in peer-type // WARNING: in.HostID requires manual conversion: does not exist in peer-type + // WARNING: in.HostResourceGroupArn requires manual conversion: does not exist in peer-type // WARNING: in.HostAffinity requires manual conversion: does not exist in peer-type // WARNING: in.DynamicHostAllocation requires manual conversion: does not exist in peer-type // WARNING: in.CapacityReservationPreference requires manual conversion: does not exist in peer-type @@ -2042,6 +2043,7 @@ func autoConvert_v1beta2_Instance_To_v1beta1_Instance(in *v1beta2.Instance, out // WARNING: in.MarketType requires manual conversion: does not exist in peer-type // WARNING: in.HostAffinity requires manual conversion: does not exist in peer-type // WARNING: in.HostID requires manual conversion: does not exist in peer-type + // WARNING: in.HostResourceGroupArn requires manual conversion: does not exist in peer-type // WARNING: in.DynamicHostAllocation requires manual conversion: does not exist in peer-type // WARNING: in.CapacityReservationPreference requires manual conversion: does not exist in peer-type // WARNING: in.CPUOptions requires manual conversion: does not exist in peer-type diff --git a/api/v1beta2/awsmachine_types.go b/api/v1beta2/awsmachine_types.go index ef966ceb12..c603c7702a 100644 --- a/api/v1beta2/awsmachine_types.go +++ b/api/v1beta2/awsmachine_types.go @@ -250,6 +250,14 @@ type AWSMachineSpec struct { // +optional HostID *string `json:"hostID,omitempty"` + // HostResourceGroupArn specifies the Dedicated Host Resource Group ARN on which the instance must be started. + // This field is mutually exclusive with DynamicHostAllocation and HostID. + // Note: The instance's AMI licenses must match the licenses associated with the host resource group. + // If the host resource group has no associated licenses, ensure the AMI also has no special licensing requirements. + // +kubebuilder:validation:Pattern=`^arn:aws[a-z\-]*:resource-groups:[a-z0-9\-]+:[0-9]{12}:group/[a-zA-Z0-9\-_]+$` + // +optional + HostResourceGroupArn *string `json:"hostResourceGroupArn,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. diff --git a/api/v1beta2/awsmachine_webhook.go b/api/v1beta2/awsmachine_webhook.go index 9c271c6939..cd9a499ac6 100644 --- a/api/v1beta2/awsmachine_webhook.go +++ b/api/v1beta2/awsmachine_webhook.go @@ -477,12 +477,24 @@ func (r *AWSMachine) validateAdditionalSecurityGroups() field.ErrorList { func (r *AWSMachine) validateHostAllocation() field.ErrorList { var allErrs field.ErrorList - // Check if both hostID and dynamicHostAllocation are specified + // Check if multiple host allocation options are specified hasHostID := r.Spec.HostID != nil && len(*r.Spec.HostID) > 0 + hasHostResourceGroupArn := r.Spec.HostResourceGroupArn != nil && len(*r.Spec.HostResourceGroupArn) > 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")) + count := 0 + if hasHostID { + count++ + } + if hasHostResourceGroupArn { + count++ + } + if hasDynamicHostAllocation { + count++ + } + + if count > 1 { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), "hostID, hostResourceGroupArn, and dynamicHostAllocation are mutually exclusive")) } return allErrs diff --git a/api/v1beta2/awsmachine_webhook_test.go b/api/v1beta2/awsmachine_webhook_test.go index d233cde8d5..a4ace180b9 100644 --- a/api/v1beta2/awsmachine_webhook_test.go +++ b/api/v1beta2/awsmachine_webhook_test.go @@ -596,6 +596,58 @@ func TestAWSMachineCreate(t *testing.T) { }, wantErr: false, }, + { + name: "hostResourceGroupArn alone is valid", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + HostResourceGroupArn: aws.String("arn:aws:resource-groups:us-west-2:123456789012:group/test-group"), + }, + }, + wantErr: false, + }, + { + name: "hostID and hostResourceGroupArn are mutually exclusive", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + HostID: aws.String("h-1234567890abcdef0"), + HostResourceGroupArn: aws.String("arn:aws:resource-groups:us-west-2:123456789012:group/test-group"), + }, + }, + wantErr: true, + }, + { + name: "hostResourceGroupArn and dynamicHostAllocation are mutually exclusive", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + HostResourceGroupArn: aws.String("arn:aws:resource-groups:us-west-2:123456789012:group/test-group"), + DynamicHostAllocation: &DynamicHostAllocationSpec{ + Tags: map[string]string{ + "Environment": "test", + }, + }, + }, + }, + wantErr: true, + }, + { + name: "all three host allocation options are mutually exclusive", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + HostID: aws.String("h-1234567890abcdef0"), + HostResourceGroupArn: aws.String("arn:aws:resource-groups:us-west-2:123456789012:group/test-group"), + DynamicHostAllocation: &DynamicHostAllocationSpec{ + Tags: map[string]string{ + "Environment": "test", + }, + }, + }, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/api/v1beta2/awsmachinetemplate_webhook.go b/api/v1beta2/awsmachinetemplate_webhook.go index aedb28d38f..26ecb42700 100644 --- a/api/v1beta2/awsmachinetemplate_webhook.go +++ b/api/v1beta2/awsmachinetemplate_webhook.go @@ -177,12 +177,24 @@ func (r *AWSMachineTemplate) validateHostAllocation() field.ErrorList { spec := r.Spec.Template.Spec - // Check if both hostID and dynamicHostAllocation are specified + // Check if multiple host allocation options are specified hasHostID := spec.HostID != nil && len(*spec.HostID) > 0 + hasHostResourceGroupArn := spec.HostResourceGroupArn != nil && len(*spec.HostResourceGroupArn) > 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")) + count := 0 + if hasHostID { + count++ + } + if hasHostResourceGroupArn { + count++ + } + if hasDynamicHostAllocation { + count++ + } + + if count > 1 { + allErrs = append(allErrs, field.Forbidden(field.NewPath("spec.template.spec"), "hostID, hostResourceGroupArn, and dynamicHostAllocation are mutually exclusive")) } return allErrs diff --git a/api/v1beta2/awsmachinetemplate_webhook_test.go b/api/v1beta2/awsmachinetemplate_webhook_test.go index 1aefb0d260..0b4fc9cf97 100644 --- a/api/v1beta2/awsmachinetemplate_webhook_test.go +++ b/api/v1beta2/awsmachinetemplate_webhook_test.go @@ -100,6 +100,57 @@ func TestAWSMachineTemplateValidateCreate(t *testing.T) { }, wantError: true, }, + { + name: "hostResourceGroupArn alone is valid", + inputTemplate: &AWSMachineTemplate{ + ObjectMeta: metav1.ObjectMeta{}, + Spec: AWSMachineTemplateSpec{ + Template: AWSMachineTemplateResource{ + Spec: AWSMachineSpec{ + InstanceType: "test", + HostResourceGroupArn: aws.String("arn:aws:resource-groups:us-west-2:123456789012:group/test-group"), + }, + }, + }, + }, + wantError: false, + }, + { + name: "hostID and hostResourceGroupArn are mutually exclusive", + inputTemplate: &AWSMachineTemplate{ + ObjectMeta: metav1.ObjectMeta{}, + Spec: AWSMachineTemplateSpec{ + Template: AWSMachineTemplateResource{ + Spec: AWSMachineSpec{ + InstanceType: "test", + HostID: aws.String("h-1234567890abcdef0"), + HostResourceGroupArn: aws.String("arn:aws:resource-groups:us-west-2:123456789012:group/test-group"), + }, + }, + }, + }, + wantError: true, + }, + { + name: "hostResourceGroupArn and dynamicHostAllocation are mutually exclusive", + inputTemplate: &AWSMachineTemplate{ + ObjectMeta: metav1.ObjectMeta{}, + Spec: AWSMachineTemplateSpec{ + Template: AWSMachineTemplateResource{ + Spec: AWSMachineSpec{ + InstanceType: "test", + HostResourceGroupArn: aws.String("arn:aws:resource-groups:us-west-2:123456789012:group/test-group"), + DynamicHostAllocation: &DynamicHostAllocationSpec{ + Tags: map[string]string{ + "Environment": "test", + }, + }, + }, + }, + }, + }, + wantError: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/api/v1beta2/types.go b/api/v1beta2/types.go index 6f6702a011..62af5a3d7d 100644 --- a/api/v1beta2/types.go +++ b/api/v1beta2/types.go @@ -286,6 +286,11 @@ type Instance struct { // +optional HostID *string `json:"hostID,omitempty"` + // HostResourceGroupArn specifies the Dedicated Host Resource Group ARN on which the instance should be started. + // Note: The instance's AMI licenses must match the licenses associated with the host resource group. + // +optional + HostResourceGroupArn *string `json:"hostResourceGroupArn,omitempty"` + // DynamicHostAllocation enables automatic allocation of dedicated hosts. // This field is mutually exclusive with HostID. // +optional diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 1bd4313128..6b3f09737b 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -777,6 +777,11 @@ func (in *AWSMachineSpec) DeepCopyInto(out *AWSMachineSpec) { *out = new(string) **out = **in } + if in.HostResourceGroupArn != nil { + in, out := &in.HostResourceGroupArn, &out.HostResourceGroupArn + *out = new(string) + **out = **in + } if in.HostAffinity != nil { in, out := &in.HostAffinity, &out.HostAffinity *out = new(string) @@ -1810,6 +1815,11 @@ func (in *Instance) DeepCopyInto(out *Instance) { *out = new(string) **out = **in } + if in.HostResourceGroupArn != nil { + in, out := &in.HostResourceGroupArn, &out.HostResourceGroupArn + *out = new(string) + **out = **in + } if in.DynamicHostAllocation != nil { in, out := &in.DynamicHostAllocation, &out.DynamicHostAllocation *out = new(DynamicHostAllocationSpec) diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml index 662022c257..963af84b25 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -1295,6 +1295,10 @@ spec: description: HostID specifies the dedicated host on which the instance should be started. type: string + hostResourceGroupArn: + description: HostResourceGroupArn specifies the Dedicated Host + Resource Group ARN on which the instance should be started. + type: string iamProfile: description: The name of the IAM instance profile associated with the instance, if applicable. @@ -3583,6 +3587,10 @@ spec: description: HostID specifies the dedicated host on which the instance should be started. type: string + hostResourceGroupArn: + description: HostResourceGroupArn specifies the Dedicated Host + Resource Group ARN on which the instance should be started. + type: string iamProfile: description: The name of the IAM instance profile associated with the instance, if applicable. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml index 869454a917..f3d42ae2ff 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml @@ -2273,6 +2273,10 @@ spec: description: HostID specifies the dedicated host on which the instance should be started. type: string + hostResourceGroupArn: + description: HostResourceGroupArn specifies the Dedicated Host + Resource Group ARN on which the instance should be started. + type: string iamProfile: description: The name of the IAM instance profile associated with the instance, if applicable. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml index 4f871ecdc2..8e4bef3079 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml @@ -777,6 +777,12 @@ spec: maxLength: 19 pattern: ^h-[0-9a-f]{17}$ type: string + hostResourceGroupArn: + description: |- + HostResourceGroupArn specifies the Dedicated Host Resource Group ARN on which the instance must be started. + This field is mutually exclusive with DynamicHostAllocation and HostID. + pattern: ^arn:aws[a-z\-]*:resource-groups:[a-z0-9\-]+:[0-9]{12}:group/[a-zA-Z0-9\-_]+$ + type: string iamInstanceProfile: description: IAMInstanceProfile is a name of an IAM instance profile to assign to the instance diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml index 1d3f40efed..5d4b97d4c2 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml @@ -697,6 +697,12 @@ spec: maxLength: 19 pattern: ^h-[0-9a-f]{17}$ type: string + hostResourceGroupArn: + description: |- + HostResourceGroupArn specifies the Dedicated Host Resource Group ARN on which the instance must be started. + This field is mutually exclusive with DynamicHostAllocation and HostID. + pattern: ^arn:aws[a-z\-]*:resource-groups:[a-z0-9\-]+:[0-9]{12}:group/[a-zA-Z0-9\-_]+$ + type: string iamInstanceProfile: description: IAMInstanceProfile is a name of an IAM instance profile to assign to the instance diff --git a/pkg/cloud/services/ec2/instances.go b/pkg/cloud/services/ec2/instances.go index 1b6f1c2714..895067e8dd 100644 --- a/pkg/cloud/services/ec2/instances.go +++ b/pkg/cloud/services/ec2/instances.go @@ -275,6 +275,7 @@ func (s *Service) CreateInstance(ctx context.Context, scope *scope.MachineScope, } else { // Use static host allocation if specified input.HostID = scope.AWSMachine.Spec.HostID + input.HostResourceGroupArn = scope.AWSMachine.Spec.HostResourceGroupArn input.HostAffinity = scope.AWSMachine.Spec.HostAffinity } @@ -729,10 +730,39 @@ func (s *Service) runInstance(role string, i *infrav1.Instance) (*infrav1.Instan Affinity: i.HostAffinity, HostId: i.HostID, } + } else if i.HostResourceGroupArn != nil { + if i.HostAffinity == nil { + i.HostAffinity = aws.String(string(types.AffinityHost)) + } + if len(i.Tenancy) == 0 { + i.Tenancy = string(types.TenancyHost) + } + s.scope.Debug("Running instance with host resource group placement", + "hostResourceGroupArn", i.HostResourceGroupArn, + "affinity", i.HostAffinity) + if input.Placement != nil { + s.scope.Warn("Placement already set for instance, overwriting with host resource group placement", + "hostResourceGroupArn", i.HostResourceGroupArn, + "affinity", i.HostAffinity, + "placement", input.Placement) + } + + input.Placement = &types.Placement{ + Tenancy: types.Tenancy(i.Tenancy), + Affinity: i.HostAffinity, + HostResourceGroupArn: i.HostResourceGroupArn, + } + // input.LicenseSpecifications = &types.LicenseConfiguration{ + // LicenseConfigurationArn: , + // } } out, err := s.EC2Client.RunInstances(context.TODO(), input) if err != nil { + // Provide more helpful error message for host resource group licensing issues + if strings.Contains(err.Error(), "host resource group") && strings.Contains(err.Error(), "licenses") { + return nil, errors.Wrap(err, "failed to run instance: AMI licenses must match the licenses associated with the host resource group. Ensure the AMI and host resource group have compatible licensing") + } return nil, errors.Wrap(err, "failed to run instance") } From 78c25e0699620bee829c0bde993d600b1cea8f45 Mon Sep 17 00:00:00 2001 From: Pankaj Walke Date: Sat, 15 Nov 2025 06:11:20 +0000 Subject: [PATCH 2/2] Add licenseConfigurationArns field in AWSMAchineSpec --- api/v1beta1/awsmachine_conversion.go | 2 ++ api/v1beta1/zz_generated.conversion.go | 2 ++ api/v1beta2/awsmachine_types.go | 6 ++++ api/v1beta2/awsmachine_webhook.go | 7 +++++ api/v1beta2/awsmachine_webhook_test.go | 21 +++++++++++++ api/v1beta2/awsmachinetemplate_webhook.go | 7 +++++ .../awsmachinetemplate_webhook_test.go | 31 +++++++++++++++++++ api/v1beta2/types.go | 5 +++ api/v1beta2/zz_generated.deepcopy.go | 10 ++++++ ...ster.x-k8s.io_awsmanagedcontrolplanes.yaml | 24 +++++++++++--- ...tructure.cluster.x-k8s.io_awsclusters.yaml | 12 +++++-- ...tructure.cluster.x-k8s.io_awsmachines.yaml | 10 ++++++ ....cluster.x-k8s.io_awsmachinetemplates.yaml | 10 ++++++ pkg/cloud/services/ec2/instances.go | 28 +++++++++++++++-- 14 files changed, 166 insertions(+), 9 deletions(-) diff --git a/api/v1beta1/awsmachine_conversion.go b/api/v1beta1/awsmachine_conversion.go index a4dbc234bd..89c6b9881e 100644 --- a/api/v1beta1/awsmachine_conversion.go +++ b/api/v1beta1/awsmachine_conversion.go @@ -46,6 +46,7 @@ func (src *AWSMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.MarketType = restored.Spec.MarketType dst.Spec.HostID = restored.Spec.HostID dst.Spec.HostResourceGroupArn = restored.Spec.HostResourceGroupArn + dst.Spec.LicenseConfigurationArns = restored.Spec.LicenseConfigurationArns dst.Spec.HostAffinity = restored.Spec.HostAffinity dst.Spec.CapacityReservationPreference = restored.Spec.CapacityReservationPreference dst.Spec.NetworkInterfaceType = restored.Spec.NetworkInterfaceType @@ -119,6 +120,7 @@ func (r *AWSMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.MarketType = restored.Spec.Template.Spec.MarketType dst.Spec.Template.Spec.HostID = restored.Spec.Template.Spec.HostID dst.Spec.Template.Spec.HostResourceGroupArn = restored.Spec.Template.Spec.HostResourceGroupArn + dst.Spec.Template.Spec.LicenseConfigurationArns = restored.Spec.Template.Spec.LicenseConfigurationArns dst.Spec.Template.Spec.HostAffinity = restored.Spec.Template.Spec.HostAffinity dst.Spec.Template.Spec.CapacityReservationPreference = restored.Spec.Template.Spec.CapacityReservationPreference dst.Spec.Template.Spec.NetworkInterfaceType = restored.Spec.Template.Spec.NetworkInterfaceType diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index 91021f7161..8ae690c3fd 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -1438,6 +1438,7 @@ func autoConvert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(in *v1beta2.AW // WARNING: in.MarketType requires manual conversion: does not exist in peer-type // WARNING: in.HostID requires manual conversion: does not exist in peer-type // WARNING: in.HostResourceGroupArn requires manual conversion: does not exist in peer-type + // WARNING: in.LicenseConfigurationArns requires manual conversion: does not exist in peer-type // WARNING: in.HostAffinity requires manual conversion: does not exist in peer-type // WARNING: in.DynamicHostAllocation requires manual conversion: does not exist in peer-type // WARNING: in.CapacityReservationPreference requires manual conversion: does not exist in peer-type @@ -2044,6 +2045,7 @@ func autoConvert_v1beta2_Instance_To_v1beta1_Instance(in *v1beta2.Instance, out // WARNING: in.HostAffinity requires manual conversion: does not exist in peer-type // WARNING: in.HostID requires manual conversion: does not exist in peer-type // WARNING: in.HostResourceGroupArn requires manual conversion: does not exist in peer-type + // WARNING: in.LicenseConfigurationArns requires manual conversion: does not exist in peer-type // WARNING: in.DynamicHostAllocation requires manual conversion: does not exist in peer-type // WARNING: in.CapacityReservationPreference requires manual conversion: does not exist in peer-type // WARNING: in.CPUOptions requires manual conversion: does not exist in peer-type diff --git a/api/v1beta2/awsmachine_types.go b/api/v1beta2/awsmachine_types.go index c603c7702a..f72b7be7ce 100644 --- a/api/v1beta2/awsmachine_types.go +++ b/api/v1beta2/awsmachine_types.go @@ -258,6 +258,12 @@ type AWSMachineSpec struct { // +optional HostResourceGroupArn *string `json:"hostResourceGroupArn,omitempty"` + // LicenseConfigurationArns specifies the License Configuration ARNs to associate with the instance. + // This field is required when HostResourceGroupArn is specified to ensure proper license compliance. + // +kubebuilder:validation:MaxItems=10 + // +optional + LicenseConfigurationArns []string `json:"licenseConfigurationArns,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. diff --git a/api/v1beta2/awsmachine_webhook.go b/api/v1beta2/awsmachine_webhook.go index cd9a499ac6..3d0c187f2e 100644 --- a/api/v1beta2/awsmachine_webhook.go +++ b/api/v1beta2/awsmachine_webhook.go @@ -497,6 +497,13 @@ func (r *AWSMachine) validateHostAllocation() field.ErrorList { allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), "hostID, hostResourceGroupArn, and dynamicHostAllocation are mutually exclusive")) } + // Validate licenseConfigurationArns is required when hostResourceGroupArn is specified + if hasHostResourceGroupArn { + if len(r.Spec.LicenseConfigurationArns) == 0 { + allErrs = append(allErrs, field.Required(field.NewPath("spec", "licenseConfigurationArns"), "licenseConfigurationArns is required when hostResourceGroupArn is specified")) + } + } + return allErrs } diff --git a/api/v1beta2/awsmachine_webhook_test.go b/api/v1beta2/awsmachine_webhook_test.go index a4ace180b9..ba48ee52a7 100644 --- a/api/v1beta2/awsmachine_webhook_test.go +++ b/api/v1beta2/awsmachine_webhook_test.go @@ -648,6 +648,27 @@ func TestAWSMachineCreate(t *testing.T) { }, wantErr: true, }, + { + name: "hostResourceGroupArn without licenseConfigurationArns should fail", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + HostResourceGroupArn: aws.String("arn:aws:resource-groups:us-west-2:123456789012:group/test-group"), + }, + }, + wantErr: true, + }, + { + name: "hostResourceGroupArn with licenseConfigurationArns should succeed", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + HostResourceGroupArn: aws.String("arn:aws:resource-groups:us-west-2:123456789012:group/test-group"), + LicenseConfigurationArns: []string{"arn:aws:license-manager:us-west-2:259732043995:license-configuration:lic-4acd3f7c117b9e314cce36e46084d071"}, + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/api/v1beta2/awsmachinetemplate_webhook.go b/api/v1beta2/awsmachinetemplate_webhook.go index 26ecb42700..4dd479eab9 100644 --- a/api/v1beta2/awsmachinetemplate_webhook.go +++ b/api/v1beta2/awsmachinetemplate_webhook.go @@ -197,6 +197,13 @@ func (r *AWSMachineTemplate) validateHostAllocation() field.ErrorList { allErrs = append(allErrs, field.Forbidden(field.NewPath("spec.template.spec"), "hostID, hostResourceGroupArn, and dynamicHostAllocation are mutually exclusive")) } + // Validate licenseConfigurationArns is required when hostResourceGroupArn is specified + if hasHostResourceGroupArn { + if len(spec.LicenseConfigurationArns) == 0 { + allErrs = append(allErrs, field.Required(field.NewPath("spec", "template", "spec", "licenseConfigurationArns"), "licenseConfigurationArns is required when hostResourceGroupArn is specified")) + } + } + return allErrs } diff --git a/api/v1beta2/awsmachinetemplate_webhook_test.go b/api/v1beta2/awsmachinetemplate_webhook_test.go index 0b4fc9cf97..7fa4b4d4da 100644 --- a/api/v1beta2/awsmachinetemplate_webhook_test.go +++ b/api/v1beta2/awsmachinetemplate_webhook_test.go @@ -151,6 +151,37 @@ func TestAWSMachineTemplateValidateCreate(t *testing.T) { }, wantError: true, }, + { + name: "hostResourceGroupArn without licenseConfigurationArns should fail", + inputTemplate: &AWSMachineTemplate{ + ObjectMeta: metav1.ObjectMeta{}, + Spec: AWSMachineTemplateSpec{ + Template: AWSMachineTemplateResource{ + Spec: AWSMachineSpec{ + InstanceType: "test", + HostResourceGroupArn: aws.String("arn:aws:resource-groups:us-west-2:123456789012:group/test-group"), + }, + }, + }, + }, + wantError: true, + }, + { + name: "hostResourceGroupArn with licenseConfigurationArns should succeed", + inputTemplate: &AWSMachineTemplate{ + ObjectMeta: metav1.ObjectMeta{}, + Spec: AWSMachineTemplateSpec{ + Template: AWSMachineTemplateResource{ + Spec: AWSMachineSpec{ + InstanceType: "test", + HostResourceGroupArn: aws.String("arn:aws:resource-groups:us-west-2:123456789012:group/test-group"), + LicenseConfigurationArns: []string{"arn:aws:license-manager:us-west-2:259732043995:license-configuration:lic-4acd3f7c117b9e314cce36e46084d071"}, + }, + }, + }, + }, + wantError: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/api/v1beta2/types.go b/api/v1beta2/types.go index 62af5a3d7d..35cced8b97 100644 --- a/api/v1beta2/types.go +++ b/api/v1beta2/types.go @@ -291,6 +291,11 @@ type Instance struct { // +optional HostResourceGroupArn *string `json:"hostResourceGroupArn,omitempty"` + // LicenseConfigurationArns specifies the License Configuration ARNs to associate with the instance. + // This field is required when HostResourceGroupArn is specified to ensure proper license compliance. + // +optional + LicenseConfigurationArns []string `json:"licenseConfigurationArns,omitempty"` + // DynamicHostAllocation enables automatic allocation of dedicated hosts. // This field is mutually exclusive with HostID. // +optional diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 6b3f09737b..4bbddc4394 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -782,6 +782,11 @@ func (in *AWSMachineSpec) DeepCopyInto(out *AWSMachineSpec) { *out = new(string) **out = **in } + if in.LicenseConfigurationArns != nil { + in, out := &in.LicenseConfigurationArns, &out.LicenseConfigurationArns + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.HostAffinity != nil { in, out := &in.HostAffinity, &out.HostAffinity *out = new(string) @@ -1820,6 +1825,11 @@ func (in *Instance) DeepCopyInto(out *Instance) { *out = new(string) **out = **in } + if in.LicenseConfigurationArns != nil { + in, out := &in.LicenseConfigurationArns, &out.LicenseConfigurationArns + *out = make([]string, len(*in)) + copy(*out, *in) + } if in.DynamicHostAllocation != nil { in, out := &in.DynamicHostAllocation, &out.DynamicHostAllocation *out = new(DynamicHostAllocationSpec) diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml index 963af84b25..10661a460b 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -1296,8 +1296,9 @@ spec: instance should be started. type: string hostResourceGroupArn: - description: HostResourceGroupArn specifies the Dedicated Host - Resource Group ARN on which the instance should be started. + description: |- + HostResourceGroupArn specifies the Dedicated Host Resource Group ARN on which the instance should be started. + Note: The instance's AMI licenses must match the licenses associated with the host resource group. type: string iamProfile: description: The name of the IAM instance profile associated with @@ -1373,6 +1374,13 @@ spec: instanceState: description: The current state of the instance. type: string + licenseConfigurationArns: + description: |- + LicenseConfigurationArns specifies the License Configuration ARNs to associate with the instance. + This field is required when HostResourceGroupArn is specified to ensure proper license compliance. + items: + type: string + type: array marketType: description: |- MarketType specifies the type of market for the EC2 instance. Valid values include: @@ -3588,8 +3596,9 @@ spec: instance should be started. type: string hostResourceGroupArn: - description: HostResourceGroupArn specifies the Dedicated Host - Resource Group ARN on which the instance should be started. + description: |- + HostResourceGroupArn specifies the Dedicated Host Resource Group ARN on which the instance should be started. + Note: The instance's AMI licenses must match the licenses associated with the host resource group. type: string iamProfile: description: The name of the IAM instance profile associated with @@ -3665,6 +3674,13 @@ spec: instanceState: description: The current state of the instance. type: string + licenseConfigurationArns: + description: |- + LicenseConfigurationArns specifies the License Configuration ARNs to associate with the instance. + This field is required when HostResourceGroupArn is specified to ensure proper license compliance. + items: + type: string + type: array marketType: description: |- MarketType specifies the type of market for the EC2 instance. Valid values include: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml index f3d42ae2ff..5ac6c22327 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml @@ -2274,8 +2274,9 @@ spec: instance should be started. type: string hostResourceGroupArn: - description: HostResourceGroupArn specifies the Dedicated Host - Resource Group ARN on which the instance should be started. + description: |- + HostResourceGroupArn specifies the Dedicated Host Resource Group ARN on which the instance should be started. + Note: The instance's AMI licenses must match the licenses associated with the host resource group. type: string iamProfile: description: The name of the IAM instance profile associated with @@ -2351,6 +2352,13 @@ spec: instanceState: description: The current state of the instance. type: string + licenseConfigurationArns: + description: |- + LicenseConfigurationArns specifies the License Configuration ARNs to associate with the instance. + This field is required when HostResourceGroupArn is specified to ensure proper license compliance. + items: + type: string + type: array marketType: description: |- MarketType specifies the type of market for the EC2 instance. Valid values include: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml index 8e4bef3079..d3aa53a866 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml @@ -781,6 +781,8 @@ spec: description: |- HostResourceGroupArn specifies the Dedicated Host Resource Group ARN on which the instance must be started. This field is mutually exclusive with DynamicHostAllocation and HostID. + Note: The instance's AMI licenses must match the licenses associated with the host resource group. + If the host resource group has no associated licenses, ensure the AMI also has no special licensing requirements. pattern: ^arn:aws[a-z\-]*:resource-groups:[a-z0-9\-]+:[0-9]{12}:group/[a-zA-Z0-9\-_]+$ type: string iamInstanceProfile: @@ -976,6 +978,14 @@ spec: m4.xlarge' minLength: 2 type: string + licenseConfigurationArns: + description: |- + LicenseConfigurationArns specifies the License Configuration ARNs to associate with the instance. + This field is required when HostResourceGroupArn is specified to ensure proper license compliance. + items: + type: string + maxItems: 10 + type: array marketType: description: |- MarketType specifies the type of market for the EC2 instance. Valid values include: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml index 5d4b97d4c2..b71ea15489 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml @@ -701,6 +701,8 @@ spec: description: |- HostResourceGroupArn specifies the Dedicated Host Resource Group ARN on which the instance must be started. This field is mutually exclusive with DynamicHostAllocation and HostID. + Note: The instance's AMI licenses must match the licenses associated with the host resource group. + If the host resource group has no associated licenses, ensure the AMI also has no special licensing requirements. pattern: ^arn:aws[a-z\-]*:resource-groups:[a-z0-9\-]+:[0-9]{12}:group/[a-zA-Z0-9\-_]+$ type: string iamInstanceProfile: @@ -896,6 +898,14 @@ spec: Example: m4.xlarge' minLength: 2 type: string + licenseConfigurationArns: + description: |- + LicenseConfigurationArns specifies the License Configuration ARNs to associate with the instance. + This field is required when HostResourceGroupArn is specified to ensure proper license compliance. + items: + type: string + maxItems: 10 + type: array marketType: description: |- MarketType specifies the type of market for the EC2 instance. Valid values include: diff --git a/pkg/cloud/services/ec2/instances.go b/pkg/cloud/services/ec2/instances.go index 895067e8dd..eb4ac8b8b5 100644 --- a/pkg/cloud/services/ec2/instances.go +++ b/pkg/cloud/services/ec2/instances.go @@ -276,6 +276,7 @@ func (s *Service) CreateInstance(ctx context.Context, scope *scope.MachineScope, // Use static host allocation if specified input.HostID = scope.AWSMachine.Spec.HostID input.HostResourceGroupArn = scope.AWSMachine.Spec.HostResourceGroupArn + input.LicenseConfigurationArns = scope.AWSMachine.Spec.LicenseConfigurationArns input.HostAffinity = scope.AWSMachine.Spec.HostAffinity } @@ -752,9 +753,15 @@ func (s *Service) runInstance(role string, i *infrav1.Instance) (*infrav1.Instan Affinity: i.HostAffinity, HostResourceGroupArn: i.HostResourceGroupArn, } - // input.LicenseSpecifications = &types.LicenseConfiguration{ - // LicenseConfigurationArn: , - // } + if len(i.LicenseConfigurationArns) > 0 { + licenseSpecs := make([]types.LicenseConfigurationRequest, len(i.LicenseConfigurationArns)) + for idx, arn := range i.LicenseConfigurationArns { + licenseSpecs[idx] = types.LicenseConfigurationRequest{ + LicenseConfigurationArn: aws.String(arn), + } + } + input.LicenseSpecifications = licenseSpecs + } } out, err := s.EC2Client.RunInstances(context.TODO(), input) @@ -1019,6 +1026,21 @@ func (s *Service) SDKToInstance(v types.Instance) (*infrav1.Instance, error) { } } + // Extract host allocation information from placement + if v.Placement != nil { + i.HostID = v.Placement.HostId + i.HostResourceGroupArn = v.Placement.HostResourceGroupArn + i.HostAffinity = v.Placement.Affinity + } + + // Extract license configuration ARNs from license specifications + if len(v.Licenses) > 0 { + i.LicenseConfigurationArns = make([]string, len(v.Licenses)) + for idx, license := range v.Licenses { + i.LicenseConfigurationArns[idx] = aws.ToString(license.LicenseConfigurationArn) + } + } + return i, nil }