Skip to content

Commit 034254d

Browse files
committed
Add PlacementGroupName and PlacementGroupPartition to AWSLaunchTemplate
This change adds support for specifying placement group configuration in AWSMachinePool launch templates. New fields added to AWSLaunchTemplate: - PlacementGroupName: specifies the name of the placement group in which to launch instances - PlacementGroupPartition: specifies the partition number within a partition placement group (valid values: 1-7) This enables users to target specific partitions when using partition placement groups with MachinePool resources, providing higher levels of availability by ensuring instances in different partitions do not share underlying hardware. Relates-to: #4870
1 parent 04a4f62 commit 034254d

File tree

3 files changed

+202
-0
lines changed

3 files changed

+202
-0
lines changed

exp/api/v1beta2/types.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ type AWSLaunchTemplate struct {
134134
// +optional
135135
PrivateDNSName *infrav1.PrivateDNSName `json:"privateDnsName,omitempty"`
136136

137+
// PlacementGroupName specifies the name of the placement group in which to launch the instance.
138+
// +optional
139+
PlacementGroupName string `json:"placementGroupName,omitempty"`
140+
141+
// PlacementGroupPartition is the partition number within the placement group in which to launch the instance.
142+
// This value is only valid if the placement group, referred in `PlacementGroupName`, was created with
143+
// strategy set to partition.
144+
// +kubebuilder:validation:Minimum:=1
145+
// +kubebuilder:validation:Maximum:=7
146+
// +optional
147+
PlacementGroupPartition int64 `json:"placementGroupPartition,omitempty"`
148+
137149
// CapacityReservationID specifies the target Capacity Reservation into which the instance should be launched.
138150
// +optional
139151
CapacityReservationID *string `json:"capacityReservationId,omitempty"`

pkg/cloud/services/ec2/launchtemplate.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,7 @@ func (s *Service) createLaunchTemplateData(scope scope.LaunchTemplateScope, imag
674674
data.InstanceMarketOptions = instanceMarketOptions
675675
data.PrivateDnsNameOptions = getLaunchTemplatePrivateDNSNameOptionsRequest(scope.GetLaunchTemplate().PrivateDNSName)
676676
data.CapacityReservationSpecification = getLaunchTemplateCapacityReservationSpecification(scope.GetLaunchTemplate())
677+
data.Placement = getLaunchTemplatePlacementRequest(scope.GetLaunchTemplate())
677678

678679
blockDeviceMappings := []types.LaunchTemplateBlockDeviceMappingRequest{}
679680

@@ -724,6 +725,30 @@ func getLaunchTemplateCapacityReservationSpecification(awsLaunchTemplate *expinf
724725
return spec
725726
}
726727

728+
func getLaunchTemplatePlacementRequest(awsLaunchTemplate *expinfrav1.AWSLaunchTemplate) *types.LaunchTemplatePlacementRequest {
729+
if awsLaunchTemplate == nil {
730+
return nil
731+
}
732+
if awsLaunchTemplate.PlacementGroupName == "" && awsLaunchTemplate.PlacementGroupPartition == 0 {
733+
return nil
734+
}
735+
736+
// PlacementGroupPartition without PlacementGroupName is invalid
737+
if awsLaunchTemplate.PlacementGroupName == "" && awsLaunchTemplate.PlacementGroupPartition != 0 {
738+
return nil
739+
}
740+
741+
placement := &types.LaunchTemplatePlacementRequest{
742+
GroupName: aws.String(awsLaunchTemplate.PlacementGroupName),
743+
}
744+
745+
if awsLaunchTemplate.PlacementGroupPartition != 0 {
746+
placement.PartitionNumber = utils.ToInt32Pointer(&awsLaunchTemplate.PlacementGroupPartition)
747+
}
748+
749+
return placement
750+
}
751+
727752
func volumeToLaunchTemplateBlockDeviceMappingRequest(v *infrav1.Volume) *types.LaunchTemplateBlockDeviceMappingRequest {
728753
ltEbsDevice := &types.LaunchTemplateEbsBlockDeviceRequest{
729754
DeleteOnTermination: aws.Bool(true),

pkg/cloud/services/ec2/launchtemplate_test.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,6 +1444,7 @@ var LaunchTemplateVersionIgnoreUnexported = cmpopts.IgnoreUnexported(
14441444
ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{},
14451445
ec2types.LaunchTemplateSpotMarketOptionsRequest{},
14461446
ec2types.LaunchTemplateInstanceMarketOptionsRequest{},
1447+
ec2types.LaunchTemplatePlacementRequest{},
14471448
ec2types.Tag{},
14481449
ec2types.LaunchTemplateTagSpecificationRequest{},
14491450
ec2types.RequestLaunchTemplateData{},
@@ -1642,6 +1643,116 @@ func TestCreateLaunchTemplateVersion(t *testing.T) {
16421643
})
16431644
},
16441645
},
1646+
{
1647+
name: "Should successfully create launch template version with placement group name",
1648+
awsResourceReference: []infrav1.AWSResourceReference{{ID: aws.String("1")}},
1649+
mpScopeUpdater: func(mps *scope.MachinePoolScope) {
1650+
spec := mps.AWSMachinePool.Spec
1651+
spec.AWSLaunchTemplate.PlacementGroupName = "my-placement-group"
1652+
spec.AWSLaunchTemplate.SpotMarketOptions = nil
1653+
mps.AWSMachinePool.Spec = spec
1654+
},
1655+
expect: func(m *mocks.MockEC2APIMockRecorder) {
1656+
sgMap := make(map[infrav1.SecurityGroupRole]infrav1.SecurityGroup)
1657+
sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"}
1658+
sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"}
1659+
1660+
expectedInput := &ec2.CreateLaunchTemplateVersionInput{
1661+
LaunchTemplateData: &ec2types.RequestLaunchTemplateData{
1662+
InstanceType: ec2types.InstanceTypeT3Large,
1663+
IamInstanceProfile: &ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{
1664+
Name: aws.String("instance-profile"),
1665+
},
1666+
KeyName: aws.String("default"),
1667+
UserData: ptr.To[string](base64.StdEncoding.EncodeToString(userData)),
1668+
SecurityGroupIds: []string{"nodeSG", "lbSG", "1"},
1669+
ImageId: aws.String("imageID"),
1670+
Placement: &ec2types.LaunchTemplatePlacementRequest{
1671+
GroupName: aws.String("my-placement-group"),
1672+
},
1673+
TagSpecifications: []ec2types.LaunchTemplateTagSpecificationRequest{
1674+
{
1675+
ResourceType: ec2types.ResourceTypeInstance,
1676+
Tags: defaultEC2AndDataTags("aws-mp-name", "cluster-name", userDataSecretKey, testBootstrapDataHash),
1677+
},
1678+
{
1679+
ResourceType: ec2types.ResourceTypeVolume,
1680+
Tags: defaultEC2Tags("aws-mp-name", "cluster-name"),
1681+
},
1682+
},
1683+
},
1684+
LaunchTemplateId: aws.String("launch-template-id"),
1685+
}
1686+
m.CreateLaunchTemplateVersion(context.TODO(), gomock.AssignableToTypeOf(expectedInput)).Return(&ec2.CreateLaunchTemplateVersionOutput{
1687+
LaunchTemplateVersion: &ec2types.LaunchTemplateVersion{
1688+
LaunchTemplateId: aws.String("launch-template-id"),
1689+
},
1690+
}, nil).Do(
1691+
func(ctx context.Context, arg *ec2.CreateLaunchTemplateVersionInput, requestOptions ...ec2.Options) {
1692+
// formatting added to match tags slice during cmp.Equal()
1693+
formatTagsInput(arg)
1694+
if !cmp.Equal(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported) {
1695+
t.Fatalf("mismatch in input expected: %+v, but got %+v, diff: %s", expectedInput, arg, cmp.Diff(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported))
1696+
}
1697+
})
1698+
},
1699+
},
1700+
{
1701+
name: "Should successfully create launch template version with placement group name and partition",
1702+
awsResourceReference: []infrav1.AWSResourceReference{{ID: aws.String("1")}},
1703+
mpScopeUpdater: func(mps *scope.MachinePoolScope) {
1704+
spec := mps.AWSMachinePool.Spec
1705+
spec.AWSLaunchTemplate.PlacementGroupName = "my-partition-placement-group"
1706+
spec.AWSLaunchTemplate.PlacementGroupPartition = 3
1707+
spec.AWSLaunchTemplate.SpotMarketOptions = nil
1708+
mps.AWSMachinePool.Spec = spec
1709+
},
1710+
expect: func(m *mocks.MockEC2APIMockRecorder) {
1711+
sgMap := make(map[infrav1.SecurityGroupRole]infrav1.SecurityGroup)
1712+
sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"}
1713+
sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"}
1714+
1715+
expectedInput := &ec2.CreateLaunchTemplateVersionInput{
1716+
LaunchTemplateData: &ec2types.RequestLaunchTemplateData{
1717+
InstanceType: ec2types.InstanceTypeT3Large,
1718+
IamInstanceProfile: &ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{
1719+
Name: aws.String("instance-profile"),
1720+
},
1721+
KeyName: aws.String("default"),
1722+
UserData: ptr.To[string](base64.StdEncoding.EncodeToString(userData)),
1723+
SecurityGroupIds: []string{"nodeSG", "lbSG", "1"},
1724+
ImageId: aws.String("imageID"),
1725+
Placement: &ec2types.LaunchTemplatePlacementRequest{
1726+
GroupName: aws.String("my-partition-placement-group"),
1727+
PartitionNumber: aws.Int32(3),
1728+
},
1729+
TagSpecifications: []ec2types.LaunchTemplateTagSpecificationRequest{
1730+
{
1731+
ResourceType: ec2types.ResourceTypeInstance,
1732+
Tags: defaultEC2AndDataTags("aws-mp-name", "cluster-name", userDataSecretKey, testBootstrapDataHash),
1733+
},
1734+
{
1735+
ResourceType: ec2types.ResourceTypeVolume,
1736+
Tags: defaultEC2Tags("aws-mp-name", "cluster-name"),
1737+
},
1738+
},
1739+
},
1740+
LaunchTemplateId: aws.String("launch-template-id"),
1741+
}
1742+
m.CreateLaunchTemplateVersion(context.TODO(), gomock.AssignableToTypeOf(expectedInput)).Return(&ec2.CreateLaunchTemplateVersionOutput{
1743+
LaunchTemplateVersion: &ec2types.LaunchTemplateVersion{
1744+
LaunchTemplateId: aws.String("launch-template-id"),
1745+
},
1746+
}, nil).Do(
1747+
func(ctx context.Context, arg *ec2.CreateLaunchTemplateVersionInput, requestOptions ...ec2.Options) {
1748+
// formatting added to match tags slice during cmp.Equal()
1749+
formatTagsInput(arg)
1750+
if !cmp.Equal(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported) {
1751+
t.Fatalf("mismatch in input expected: %+v, but got %+v, diff: %s", expectedInput, arg, cmp.Diff(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported))
1752+
}
1753+
})
1754+
},
1755+
},
16451756
{
16461757
name: "Should return error if AWS failed during launch template version creation",
16471758
awsResourceReference: []infrav1.AWSResourceReference{{ID: aws.String("1")}},
@@ -2192,3 +2303,57 @@ func TestDeleteLaunchTemplateVersion(t *testing.T) {
21922303
})
21932304
}
21942305
}
2306+
2307+
func TestGetLaunchTemplatePlacementRequest(t *testing.T) {
2308+
testCases := []struct {
2309+
name string
2310+
input *expinfrav1.AWSLaunchTemplate
2311+
expected *ec2types.LaunchTemplatePlacementRequest
2312+
}{
2313+
{
2314+
name: "nil launch template returns nil",
2315+
input: nil,
2316+
expected: nil,
2317+
},
2318+
{
2319+
name: "empty placement group name and partition returns nil",
2320+
input: &expinfrav1.AWSLaunchTemplate{},
2321+
expected: nil,
2322+
},
2323+
{
2324+
name: "placement group partition without name returns nil",
2325+
input: &expinfrav1.AWSLaunchTemplate{
2326+
PlacementGroupPartition: 3,
2327+
},
2328+
expected: nil,
2329+
},
2330+
{
2331+
name: "placement group name only returns group name",
2332+
input: &expinfrav1.AWSLaunchTemplate{
2333+
PlacementGroupName: "my-placement-group",
2334+
},
2335+
expected: &ec2types.LaunchTemplatePlacementRequest{
2336+
GroupName: aws.String("my-placement-group"),
2337+
},
2338+
},
2339+
{
2340+
name: "placement group name with partition returns both",
2341+
input: &expinfrav1.AWSLaunchTemplate{
2342+
PlacementGroupName: "my-partition-pg",
2343+
PlacementGroupPartition: 5,
2344+
},
2345+
expected: &ec2types.LaunchTemplatePlacementRequest{
2346+
GroupName: aws.String("my-partition-pg"),
2347+
PartitionNumber: aws.Int32(5),
2348+
},
2349+
},
2350+
}
2351+
2352+
for _, tc := range testCases {
2353+
t.Run(tc.name, func(t *testing.T) {
2354+
g := NewWithT(t)
2355+
result := getLaunchTemplatePlacementRequest(tc.input)
2356+
g.Expect(result).To(Equal(tc.expected))
2357+
})
2358+
}
2359+
}

0 commit comments

Comments
 (0)