diff --git a/api/v1beta1/awscluster_conversion.go b/api/v1beta1/awscluster_conversion.go index 1ebb979848..33aff027e5 100644 --- a/api/v1beta1/awscluster_conversion.go +++ b/api/v1beta1/awscluster_conversion.go @@ -63,6 +63,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 5f44ccbecd..4cd5a66850 100644 --- a/api/v1beta1/awsmachine_conversion.go +++ b/api/v1beta1/awsmachine_conversion.go @@ -44,6 +44,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 { @@ -108,6 +110,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 8e445247da..ad9cc57bea 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -1449,6 +1449,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 } @@ -2057,6 +2059,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 43ac3da2d1..93c019b1b6 100644 --- a/api/v1beta2/awsmachine_types.go +++ b/api/v1beta2/awsmachine_types.go @@ -233,6 +233,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 5a119de8db..af6d68eba4 100644 --- a/api/v1beta2/awsmachine_webhook.go +++ b/api/v1beta2/awsmachine_webhook.go @@ -75,6 +75,7 @@ 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()...) @@ -107,6 +108,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()...) newAWSMachineSpec := newAWSMachine["spec"].(map[string]interface{}) oldAWSMachineSpec := oldAWSMachine["spec"].(map[string]interface{}) @@ -455,6 +457,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 8dc7ce96e1..3eb5b6931a 100644 --- a/api/v1beta2/awsmachine_webhook_test.go +++ b/api/v1beta2/awsmachine_webhook_test.go @@ -412,6 +412,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 9c3c2771c9..143a806861 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 29cc567267..e690bb9335 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -1222,6 +1222,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. @@ -3403,6 +3417,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 7ff40608ac..534a4ebcd0 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml @@ -2205,6 +2205,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 d128539e11..4a0b10b5a3 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml @@ -703,6 +703,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 c95fbb3674..fdac94f540 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml @@ -622,6 +622,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 8245f9a61b..05175d9908 100644 --- a/pkg/cloud/services/ec2/instances.go +++ b/pkg/cloud/services/ec2/instances.go @@ -258,6 +258,10 @@ func (s *Service) CreateInstance(ctx context.Context, scope *scope.MachineScope, 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) @@ -677,6 +681,30 @@ func (s *Service) runInstance(role string, i *infrav1.Instance) (*infrav1.Instan } } + if i.HostID != nil { + if i.HostAffinity == nil { + i.HostAffinity = aws.String("default") + } + if len(i.Tenancy) == 0 { + i.Tenancy = "host" + } + s.scope.Debug("Running instance with dedicated host placement", + "hostId", i.HostID, + "affinity", i.HostAffinity) + if input.Placement != nil { + s.scope.Warn("Placement already set for instance, overwriting with dedicated host placement", + "hostId", i.HostID, + "affinity", i.HostAffinity, + "placement", input.Placement) + } + + input.Placement = &types.Placement{ + Tenancy: types.Tenancy(i.Tenancy), + Affinity: i.HostAffinity, + HostId: i.HostID, + } + } + out, err := s.EC2Client.RunInstances(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 d6e3838b2d..4374126a9b 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..e313550161 --- /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: external 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 7e7d9131b5..b4dd695ff3 100644 --- a/test/e2e/shared/aws.go +++ b/test/e2e/shared/aws.go @@ -2441,3 +2441,63 @@ func GetMountTargetState(e2eCtx *E2EContext, mountTargetID string) (*string, err } return result.LifeCycleState, nil } + +func getAvailabilityZone(e2eCtx *E2EContext) string { + az := e2eCtx.E2EConfig.MustGetVariable(AwsAvailabilityZone1) + return az +} + +func getInstanceFamily(e2eCtx *E2EContext) string { + machineType := e2eCtx.E2EConfig.MustGetVariable(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(ctx context.Context, e2eCtx *E2EContext) (string, error) { + ec2Svc := ec2.NewFromConfig(*e2eCtx.AWSSessionV2) + input := &ec2.AllocateHostsInput{ + AvailabilityZone: aws.String(getAvailabilityZone(e2eCtx)), + InstanceFamily: aws.String(getInstanceFamily(e2eCtx)), + Quantity: aws.Int32(1), + } + output, err := ec2Svc.AllocateHosts(ctx, 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(ctx context.Context, e2eCtx *E2EContext, hostID string) { + ec2Svc := ec2.NewFromConfig(*e2eCtx.AWSSessionV2) + + input := &ec2.ReleaseHostsInput{ + HostIds: []string{hostID}, + } + + _, err := ec2Svc.ReleaseHosts(ctx, input) + Expect(err).ToNot(HaveOccurred(), "Failed to release host %s", hostID) + fmt.Println("Released Host ID: ", hostID) +} + +func GetHostID(ctx context.Context, e2eCtx *E2EContext, instanceID string) string { + ec2Svc := ec2.NewFromConfig(*e2eCtx.AWSSessionV2) + + input := &ec2.DescribeInstancesInput{ + InstanceIds: []string{instanceID}, + } + + result, err := ec2Svc.DescribeInstances(ctx, 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 fa9b00bebf..deac101fa5 100644 --- a/test/e2e/shared/defaults.go +++ b/test/e2e/shared/defaults.go @@ -73,6 +73,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 77de9a9dda..9ac1d76b55 100644 --- a/test/e2e/suites/unmanaged/unmanaged_functional_test.go +++ b/test/e2e/suites/unmanaged/unmanaged_functional_test.go @@ -973,4 +973,74 @@ var _ = ginkgo.Context("[unmanaged] [functional]", func() { "Eventually failed waiting for AWSCluster to show VPC endpoint as deleted in conditions") }) }) + + // NOTE: disabled whilst discussions happen on the costs. + ginkgo.PDescribe("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) + + ginkgo.By("Allocating a dedicated host") + hostID, err := shared.AllocateHost(ctx, 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(ctx, 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.MustGetVariable(shared.KubernetesVersion), + ControlPlaneMachineCount: ptr.To[int64](1), + WorkerMachineCount: ptr.To[int64](0), + ClusterctlVariables: map[string]string{ + "HOST_ID": hostID, + "HOST_AFFINITY": "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(ctx, 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!") + }) + }) })