diff --git a/api/v1beta1/awscluster_conversion.go b/api/v1beta1/awscluster_conversion.go index 9bdaf07261..20e92ce49e 100644 --- a/api/v1beta1/awscluster_conversion.go +++ b/api/v1beta1/awscluster_conversion.go @@ -62,6 +62,8 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error { dst.Status.Bastion.NetworkInterfaceType = restored.Status.Bastion.NetworkInterfaceType dst.Status.Bastion.CapacityReservationID = restored.Status.Bastion.CapacityReservationID 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.Spec.Partition = restored.Spec.Partition diff --git a/api/v1beta1/awsmachine_conversion.go b/api/v1beta1/awsmachine_conversion.go index c5ac50ade1..d489038343 100644 --- a/api/v1beta1/awsmachine_conversion.go +++ b/api/v1beta1/awsmachine_conversion.go @@ -43,6 +43,8 @@ func (src *AWSMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.SecurityGroupOverrides = restored.Spec.SecurityGroupOverrides dst.Spec.CapacityReservationID = restored.Spec.CapacityReservationID dst.Spec.MarketType = restored.Spec.MarketType + dst.Spec.HostID = restored.Spec.HostID + dst.Spec.HostAffinity = restored.Spec.HostAffinity dst.Spec.NetworkInterfaceType = restored.Spec.NetworkInterfaceType if restored.Spec.ElasticIPPool != nil { if dst.Spec.ElasticIPPool == nil { @@ -107,6 +109,8 @@ func (r *AWSMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.SecurityGroupOverrides = restored.Spec.Template.Spec.SecurityGroupOverrides 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.HostAffinity = restored.Spec.Template.Spec.HostAffinity dst.Spec.Template.Spec.NetworkInterfaceType = restored.Spec.Template.Spec.NetworkInterfaceType if restored.Spec.Template.Spec.ElasticIPPool != nil { if dst.Spec.Template.Spec.ElasticIPPool == nil { diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index d933c71a48..4f8f11535f 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -1435,6 +1435,8 @@ func autoConvert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(in *v1beta2.AW // WARNING: in.PrivateDNSName requires manual conversion: does not exist in peer-type // 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.HostAffinity requires manual conversion: does not exist in peer-type return nil } @@ -2039,6 +2041,8 @@ func autoConvert_v1beta2_Instance_To_v1beta1_Instance(in *v1beta2.Instance, out // WARNING: in.PublicIPOnLaunch requires manual conversion: does not exist in peer-type // 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.HostAffinity requires manual conversion: does not exist in peer-type + // WARNING: in.HostID requires manual conversion: does not exist in peer-type return nil } diff --git a/api/v1beta2/awsmachine_types.go b/api/v1beta2/awsmachine_types.go index 191e46bddf..96eb547fe6 100644 --- a/api/v1beta2/awsmachine_types.go +++ b/api/v1beta2/awsmachine_types.go @@ -223,6 +223,18 @@ type AWSMachineSpec struct { // If marketType is not specified and spotMarketOptions is provided, the marketType defaults to "Spot". // +optional MarketType MarketType `json:"marketType,omitempty"` + + // HostID specifies the Dedicated Host on which the instance must be started. + // +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 defined, HostID is required. + // +optional + // +kubebuilder:validation:Enum:=default;host + HostAffinity *string `json:"hostAffinity,omitempty"` } // CloudInit defines options related to the bootstrapping systems where diff --git a/api/v1beta2/awsmachine_webhook.go b/api/v1beta2/awsmachine_webhook.go index 1937c6ff21..0223b7a577 100644 --- a/api/v1beta2/awsmachine_webhook.go +++ b/api/v1beta2/awsmachine_webhook.go @@ -64,6 +64,7 @@ func (r *AWSMachine) ValidateCreate() (admission.Warnings, error) { 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()...) @@ -91,6 +92,8 @@ func (r *AWSMachine) ValidateUpdate(old runtime.Object) (admission.Warnings, err allErrs = append(allErrs, r.validateCloudInitSecret()...) allErrs = append(allErrs, r.validateAdditionalSecurityGroups()...) allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...) + allErrs = append(allErrs, r.validateHostAffinity()...) + newAWSMachineSpec := newAWSMachine["spec"].(map[string]interface{}) oldAWSMachineSpec := oldAWSMachine["spec"].(map[string]interface{}) @@ -433,6 +436,17 @@ func (r *AWSMachine) validateAdditionalSecurityGroups() field.ErrorList { return allErrs } +func (r *AWSMachine) validateHostAffinity() 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")) + } + } + return allErrs +} + func (r *AWSMachine) validateSSHKeyName() field.ErrorList { return validateSSHKeyName(r.Spec.SSHKeyName) } diff --git a/api/v1beta2/awsmachine_webhook_test.go b/api/v1beta2/awsmachine_webhook_test.go index 8045ddc79a..29e0dfb9cd 100644 --- a/api/v1beta2/awsmachine_webhook_test.go +++ b/api/v1beta2/awsmachine_webhook_test.go @@ -411,6 +411,37 @@ func TestAWSMachineCreate(t *testing.T) { }, wantErr: true, }, + { + name: "configure host affinity with Host ID", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + HostAffinity: ptr.To("default"), + HostID: ptr.To("h-09dcf61cb388b0149"), + }, + }, + wantErr: false, + }, + { + name: "configure host affinity with invalid affinity", + machine: &AWSMachine{ + Spec: AWSMachineSpec{ + InstanceType: "test", + HostAffinity: ptr.To("invalid"), + }, + }, + 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{ diff --git a/api/v1beta2/types.go b/api/v1beta2/types.go index bee54a9f0b..fa7fd1f0ae 100644 --- a/api/v1beta2/types.go +++ b/api/v1beta2/types.go @@ -273,6 +273,18 @@ type Instance struct { // If marketType is not specified and spotMarketOptions is provided, the marketType defaults to "Spot". // +optional MarketType MarketType `json:"marketType,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 defined, HostID is required. + // +optional + // +kubebuilder:validation:Enum:=default;host + HostAffinity *string `json:"hostAffinity,omitempty"` + + // HostID specifies the dedicated host on which the instance should be started + // +optional + HostID *string `json:"hostID,omitempty"` } // MarketType describes the market type of an Instance diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index 7d39649cfa..820ff9cd21 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -771,6 +771,16 @@ func (in *AWSMachineSpec) DeepCopyInto(out *AWSMachineSpec) { *out = new(string) **out = **in } + if in.HostID != nil { + in, out := &in.HostID, &out.HostID + *out = new(string) + **out = **in + } + if in.HostAffinity != nil { + in, out := &in.HostAffinity, &out.HostAffinity + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSMachineSpec. @@ -1610,6 +1620,16 @@ func (in *Instance) DeepCopyInto(out *Instance) { *out = new(string) **out = **in } + if in.HostAffinity != nil { + in, out := &in.HostAffinity, &out.HostAffinity + *out = new(string) + **out = **in + } + if in.HostID != nil { + in, out := &in.HostID, &out.HostID + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Instance. 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 afa19c657c..bdee116782 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -1213,6 +1213,20 @@ spec: description: Specifies whether enhanced networking with ENA is enabled. type: boolean + hostAffinity: + description: |- + 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 defined, HostID is required. + enum: + - default + - host + type: string + hostID: + description: HostID specifies the dedicated host on which the + instance should be started + type: string iamProfile: description: The name of the IAM instance profile associated with the instance, if applicable. @@ -3378,6 +3392,20 @@ spec: description: Specifies whether enhanced networking with ENA is enabled. type: boolean + hostAffinity: + description: |- + 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 defined, HostID is required. + enum: + - default + - host + type: string + hostID: + description: HostID specifies the dedicated host 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 3cdc2fd19c..b7fd06970a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml @@ -2180,6 +2180,20 @@ spec: description: Specifies whether enhanced networking with ENA is enabled. type: boolean + hostAffinity: + description: |- + 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 defined, HostID is required. + enum: + - default + - host + type: string + hostID: + description: HostID specifies the dedicated host 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 5baacc3e2f..f021e9fc50 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml @@ -686,6 +686,20 @@ spec: - message: allowed values are 'none' and 'amazon-pool' rule: self in ['none','amazon-pool'] type: object + hostAffinity: + description: |- + 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 defined, HostID is required. + enum: + - default + - host + type: string + hostID: + description: HostID specifies the Dedicated Host on which the instance + must be started. + 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 40cf10944a..4ad1a00737 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml @@ -620,6 +620,20 @@ spec: - message: allowed values are 'none' and 'amazon-pool' rule: self in ['none','amazon-pool'] type: object + hostAffinity: + description: |- + 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 defined, HostID is required. + enum: + - default + - host + type: string + hostID: + description: HostID specifies the Dedicated Host on which + the instance must be started. + 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 2c1756a931..e2be64d993 100644 --- a/pkg/cloud/services/ec2/instances.go +++ b/pkg/cloud/services/ec2/instances.go @@ -255,6 +255,10 @@ func (s *Service) CreateInstance(scope *scope.MachineScope, userData []byte, use input.MarketType = scope.AWSMachine.Spec.MarketType + input.HostID = scope.AWSMachine.Spec.HostID + + input.HostAffinity = scope.AWSMachine.Spec.HostAffinity + s.scope.Debug("Running instance", "machine-role", scope.Role()) s.scope.Debug("Running instance with instance metadata options", "metadata options", input.InstanceMetadataOptions) out, err := s.runInstance(scope.Role(), input) @@ -674,6 +678,27 @@ func (s *Service) runInstance(role string, i *infrav1.Instance) (*infrav1.Instan } } + if i.HostID != nil { + if i.HostAffinity == nil { + i.HostAffinity = aws.String("Default") + } + s.scope.Debug("Running instance with dedicated host placement", + "hostId", i.HostID, + "hostAffinity", i.HostAffinity) + if input.Placement != nil { + s.scope.Warn("Placement already set for instance, overwriting with dedicated host placement", + "hostId", i.HostID, + "hostAffinity", i.HostAffinity, + "placement", input.Placement.GoString()) + } + + input.Placement = &ec2.Placement{ + Tenancy: aws.String("host"), + Affinity: i.HostAffinity, + HostId: i.HostID, + } + } + out, err := s.EC2Client.RunInstancesWithContext(context.TODO(), input) if err != nil { return nil, errors.Wrap(err, "failed to run instance") diff --git a/test/e2e/data/e2e_conf.yaml b/test/e2e/data/e2e_conf.yaml index 241b922ab7..2af6669283 100644 --- a/test/e2e/data/e2e_conf.yaml +++ b/test/e2e/data/e2e_conf.yaml @@ -168,6 +168,7 @@ providers: - sourcePath: "./shared/v1beta2_provider/metadata.yaml" - sourcePath: "./infrastructure-aws/withoutclusterclass/generated/cluster-template-ignition.yaml" - sourcePath: "./infrastructure-aws/withoutclusterclass/generated/cluster-template-upgrade-to-external-cloud-provider.yaml" + - sourcePath: "./infrastructure-aws/withoutclusterclass/generated/cluster-template-dedicated-host.yaml" replacements: # To allow bugs to be catched. - old: "failureThreshold: 3" diff --git a/test/e2e/data/infrastructure-aws/withoutclusterclass/kustomize_sources/dedicated-host/dedicated-host-resource-set.yaml b/test/e2e/data/infrastructure-aws/withoutclusterclass/kustomize_sources/dedicated-host/dedicated-host-resource-set.yaml new file mode 100644 index 0000000000..a202d61697 --- /dev/null +++ b/test/e2e/data/infrastructure-aws/withoutclusterclass/kustomize_sources/dedicated-host/dedicated-host-resource-set.yaml @@ -0,0 +1,49 @@ +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: MachineDeployment +metadata: + name: "${CLUSTER_NAME}-md-dh" +spec: + clusterName: "${CLUSTER_NAME}" + replicas: 1 + selector: + matchLabels: + template: + spec: + clusterName: "${CLUSTER_NAME}" + version: "${KUBERNETES_VERSION}" + bootstrap: + configRef: + name: "${CLUSTER_NAME}-md-dh" + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: KubeadmConfigTemplate + infrastructureRef: + name: "${CLUSTER_NAME}-md-dh" + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: AWSMachineTemplate +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: AWSMachineTemplate +metadata: + name: "${CLUSTER_NAME}-md-dh" +spec: + template: + spec: + instanceType: "${AWS_NODE_MACHINE_TYPE}" + iamInstanceProfile: "nodes.cluster-api-provider-aws.sigs.k8s.io" + sshKeyName: "${AWS_SSH_KEY_NAME}" + hostID: "${HOST_ID}" + hostAffinity: "${HOST_AFFINITY}" +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: "${CLUSTER_NAME}-md-dh" +spec: + template: + spec: + joinConfiguration: + nodeRegistration: + name: '{{ ds.meta_data.local_hostname }}' + kubeletExtraArgs: + cloud-provider: aws diff --git a/test/e2e/data/infrastructure-aws/withoutclusterclass/kustomize_sources/dedicated-host/kustomization.yaml b/test/e2e/data/infrastructure-aws/withoutclusterclass/kustomize_sources/dedicated-host/kustomization.yaml new file mode 100644 index 0000000000..191bad9a25 --- /dev/null +++ b/test/e2e/data/infrastructure-aws/withoutclusterclass/kustomize_sources/dedicated-host/kustomization.yaml @@ -0,0 +1,10 @@ +resources: + - ../default + - dedicated-host-resource-set.yaml + +generatorOptions: + disableNameSuffixHash: true + labels: + type: generated + annotations: + note: generated diff --git a/test/e2e/shared/aws.go b/test/e2e/shared/aws.go index e5f9f9a0a4..9a9b5af78d 100644 --- a/test/e2e/shared/aws.go +++ b/test/e2e/shared/aws.go @@ -2366,3 +2366,63 @@ func GetMountTargetState(e2eCtx *E2EContext, mountTargetID string) (*string, err } return result.LifeCycleState, nil } + +func getAvailabilityZone(e2eCtx *E2EContext) string { + az := e2eCtx.E2EConfig.GetVariable(AwsAvailabilityZone1) + return az +} + +func getInstanceFamily(e2eCtx *E2EContext) string { + machineType := e2eCtx.E2EConfig.GetVariable(AwsNodeMachineType) + // from instance type get instace family behind the dot + // for example: t3a.medium -> t3 + machineTypeSplit := strings.Split(machineType, ".") + if len(machineTypeSplit) > 0 { + return machineTypeSplit[0] + } + return "t3" +} + +func AllocateHost(e2eCtx *E2EContext) (string, error) { + ec2Svc := ec2.New(e2eCtx.AWSSession) + input := &ec2.AllocateHostsInput{ + AvailabilityZone: aws.String(getAvailabilityZone(e2eCtx)), + InstanceFamily: aws.String(getInstanceFamily(e2eCtx)), + Quantity: aws.Int64(1), + } + output, err := ec2Svc.AllocateHosts(input) + Expect(err).ToNot(HaveOccurred(), "Failed to allocate host") + Expect(len(output.HostIds)).To(BeNumerically(">", 0), "No dedicated host ID returned") + fmt.Println("Allocated Host ID: ", *output.HostIds[0]) + hostID := *output.HostIds[0] + return hostID, nil +} + +func ReleaseHost(e2eCtx *E2EContext, hostID string) { + ec2Svc := ec2.New(e2eCtx.AWSSession) + + input := &ec2.ReleaseHostsInput{ + HostIds: []*string{aws.String(hostID)}, + } + + _, err := ec2Svc.ReleaseHosts(input) + Expect(err).ToNot(HaveOccurred(), "Failed to release host %s", hostID) + fmt.Println("Released Host ID: ", hostID) +} + +func GetHostID(e2eCtx *E2EContext, instanceID string) string { + ec2Svc := ec2.New(e2eCtx.AWSSession) + + input := &ec2.DescribeInstancesInput{ + InstanceIds: []*string{aws.String(instanceID)}, + } + + result, err := ec2Svc.DescribeInstances(input) + Expect(err).ToNot(HaveOccurred(), "Failed to get host ID for instance %s", instanceID) + Expect(len(result.Reservations)).To(BeNumerically(">", 0), "No reservation returned") + Expect(len(result.Reservations[0].Instances)).To(BeNumerically(">", 0), "No instance returned") + placement := *result.Reservations[0].Instances[0].Placement + hostID := *placement.HostId + fmt.Println("Host ID: ", hostID) + return hostID +} diff --git a/test/e2e/shared/defaults.go b/test/e2e/shared/defaults.go index 8e514bf913..2b1e8eb912 100644 --- a/test/e2e/shared/defaults.go +++ b/test/e2e/shared/defaults.go @@ -72,6 +72,8 @@ const ( ClassicElbTestKubernetesFrom = "CLASSICELB_TEST_KUBERNETES_VERSION_FROM" ClassicElbTestKubernetesTo = "CLASSICELB_TEST_KUBERNETES_VERSION_TO" + + DedicatedHostFlavor = "dedicated-host" ) // ResourceQuotaFilePath is the path to the file that contains the resource usage. diff --git a/test/e2e/suites/unmanaged/unmanaged_functional_test.go b/test/e2e/suites/unmanaged/unmanaged_functional_test.go index e7d68f7bca..6b70a346d4 100644 --- a/test/e2e/suites/unmanaged/unmanaged_functional_test.go +++ b/test/e2e/suites/unmanaged/unmanaged_functional_test.go @@ -957,4 +957,74 @@ var _ = ginkgo.Context("[unmanaged] [functional]", func() { "Eventually failed waiting for AWSCluster to show VPC endpoint as deleted in conditions") }) }) + + ginkgo.Describe("Dedicated hosts cluster test", func() { + ginkgo.It("should create cluster with dedicated hosts", func() { + specName := "dedicated-host" + if !e2eCtx.Settings.SkipQuotas { + requiredResources = &shared.TestResource{EC2Normal: 1 * e2eCtx.Settings.InstanceVCPU, IGW: 1, NGW: 1, VPC: 1, ClassicLB: 1, EIP: 1, EventBridgeRules: 50} + requiredResources.WriteRequestedResources(e2eCtx, specName) + Expect(shared.AcquireResources(requiredResources, ginkgo.GinkgoParallelProcess(), flock.New(shared.ResourceQuotaFilePath))).To(Succeed()) + defer shared.ReleaseResources(requiredResources, ginkgo.GinkgoParallelProcess(), flock.New(shared.ResourceQuotaFilePath)) + } + namespace := shared.SetupSpecNamespace(ctx, specName, e2eCtx) + defer shared.DumpSpecResourcesAndCleanup(ctx, specName, namespace, e2eCtx) + + // Allocate a dedicated host and ensure it is released after the test + ginkgo.By("Allocating a dedicated host") + hostID, err := shared.AllocateHost(e2eCtx) + Expect(err).To(BeNil()) + Expect(hostID).NotTo(BeEmpty()) + ginkgo.By(fmt.Sprintf("Allocated dedicated host: %s", hostID)) + defer func() { + ginkgo.By(fmt.Sprintf("Releasing the dedicated host: %s", hostID)) + shared.ReleaseHost(e2eCtx, hostID) + }() + + ginkgo.By("Creating cluster") + clusterName := fmt.Sprintf("%s-%s", specName, util.RandomString(6)) + + // Create a cluster with a dedicated host + clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{ + ClusterProxy: e2eCtx.Environment.BootstrapClusterProxy, + ConfigCluster: clusterctl.ConfigClusterInput{ + LogFolder: filepath.Join(e2eCtx.Settings.ArtifactFolder, "clusters", e2eCtx.Environment.BootstrapClusterProxy.GetName()), + ClusterctlConfigPath: e2eCtx.Environment.ClusterctlConfigPath, + KubeconfigPath: e2eCtx.Environment.BootstrapClusterProxy.GetKubeconfigPath(), + InfrastructureProvider: clusterctl.DefaultInfrastructureProvider, + Flavor: shared.DedicatedHostFlavor, + Namespace: namespace.Name, + ClusterName: clusterName, + KubernetesVersion: e2eCtx.E2EConfig.GetVariable(shared.KubernetesVersion), + ControlPlaneMachineCount: ptr.To[int64](1), + WorkerMachineCount: ptr.To[int64](0), + ClusterctlVariables: map[string]string{ + "HOST_ID": hostID, + "HOST_AFFINITY": "host", + "TENANCY": "host", + }, + }, + WaitForClusterIntervals: e2eCtx.E2EConfig.GetIntervals(specName, "wait-cluster"), + WaitForControlPlaneIntervals: e2eCtx.E2EConfig.GetIntervals(specName, "wait-control-plane"), + }, result) + workerMachines := result.MachineDeployments + mdName := fmt.Sprintf("%s-md-dh", clusterName) + var found *clusterv1.MachineDeployment + for _, md := range workerMachines { + if md.Name == mdName { + found = md + } + } + Expect(found).NotTo(BeNil(), fmt.Sprintf("Expected MachineDeployment %s to be found", mdName)) + machineList := getAWSMachinesForDeployment(namespace.Name, *found) + Expect(len(machineList.Items)).To(Equal(1), fmt.Sprintf("Expected one machine in MachineDeployment %s, but got %d", mdName, len(machineList.Items))) + machine := machineList.Items[0] + instanceID := *(machine.Spec.InstanceID) + ginkgo.By(fmt.Sprintf("Worker instance ID: %s", instanceID)) + instanceHostID := shared.GetHostID(e2eCtx, instanceID) + ginkgo.By(fmt.Sprintf("Worker instance host ID: %s", instanceHostID)) + Expect(instanceHostID).To(Equal(hostID), fmt.Sprintf("Expected instance to be on host %s, but got %s", hostID, instanceHostID)) + ginkgo.By("PASSED!") + }) + }) })