Skip to content

Commit 0ec1390

Browse files
committed
add launchtemplate updates and tests
1 parent 592e975 commit 0ec1390

File tree

4 files changed

+262
-44
lines changed

4 files changed

+262
-44
lines changed

pkg/cloud/services/ec2/instances.go

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,14 +1156,7 @@ func getCapacityReservationSpecification(capacityReservationID *string, capacity
11561156
CapacityReservationId: capacityReservationID,
11571157
}
11581158
}
1159-
switch capacityReservationPreference {
1160-
case infrav1.CapacityReservationPreferenceNone:
1161-
spec.CapacityReservationPreference = types.CapacityReservationPreferenceNone
1162-
case infrav1.CapacityReservationPreferenceOnly:
1163-
spec.CapacityReservationPreference = types.CapacityReservationPreferenceCapacityReservationsOnly
1164-
case infrav1.CapacityReservationPreferenceOpen:
1165-
spec.CapacityReservationPreference = types.CapacityReservationPreferenceOpen
1166-
}
1159+
spec.CapacityReservationPreference = CapacityReservationPreferenceToSDK(capacityReservationPreference)
11671160
return &spec
11681161
}
11691162

pkg/cloud/services/ec2/instances_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5653,6 +5653,130 @@ func TestCreateInstance(t *testing.T) {
56535653
}
56545654
},
56555655
},
5656+
{
5657+
name: "Simple, setting CapacityReservationID and CapacityReservationPreference",
5658+
machine: &clusterv1.Machine{
5659+
ObjectMeta: metav1.ObjectMeta{
5660+
Labels: map[string]string{"set": "node"},
5661+
},
5662+
Spec: clusterv1.MachineSpec{
5663+
Bootstrap: clusterv1.Bootstrap{
5664+
DataSecretName: ptr.To[string]("bootstrap-data"),
5665+
},
5666+
},
5667+
},
5668+
machineConfig: &infrav1.AWSMachineSpec{
5669+
AMI: infrav1.AMIReference{
5670+
ID: aws.String("abc"),
5671+
},
5672+
InstanceType: "m5.large",
5673+
CapacityReservationID: aws.String("cr-12345678901234567"),
5674+
CapacityReservationPreference: infrav1.CapacityReservationPreferenceOnly,
5675+
},
5676+
awsCluster: &infrav1.AWSCluster{
5677+
ObjectMeta: metav1.ObjectMeta{Name: "test"},
5678+
Spec: infrav1.AWSClusterSpec{
5679+
5680+
NetworkSpec: infrav1.NetworkSpec{
5681+
Subnets: infrav1.Subnets{
5682+
infrav1.SubnetSpec{
5683+
ID: "subnet-1",
5684+
IsPublic: false,
5685+
},
5686+
infrav1.SubnetSpec{
5687+
IsPublic: false,
5688+
},
5689+
},
5690+
VPC: infrav1.VPCSpec{
5691+
ID: "vpc-test",
5692+
},
5693+
},
5694+
},
5695+
Status: infrav1.AWSClusterStatus{
5696+
Network: infrav1.NetworkStatus{
5697+
SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{
5698+
infrav1.SecurityGroupControlPlane: {
5699+
ID: "1",
5700+
},
5701+
infrav1.SecurityGroupNode: {
5702+
ID: "2",
5703+
},
5704+
infrav1.SecurityGroupLB: {
5705+
ID: "3",
5706+
},
5707+
},
5708+
APIServerELB: infrav1.LoadBalancer{
5709+
DNSName: "test-apiserver.us-east-1.aws",
5710+
},
5711+
},
5712+
},
5713+
},
5714+
expect: func(m *mocks.MockEC2APIMockRecorder) {
5715+
m.
5716+
DescribeInstanceTypes(context.TODO(), gomock.Eq(&ec2.DescribeInstanceTypesInput{
5717+
InstanceTypes: []types.InstanceType{
5718+
types.InstanceTypeM5Large,
5719+
},
5720+
})).
5721+
Return(&ec2.DescribeInstanceTypesOutput{
5722+
InstanceTypes: []types.InstanceTypeInfo{
5723+
{
5724+
ProcessorInfo: &types.ProcessorInfo{
5725+
SupportedArchitectures: []types.ArchitectureType{
5726+
types.ArchitectureTypeX8664,
5727+
},
5728+
},
5729+
},
5730+
},
5731+
}, nil)
5732+
m. // TODO: Restore these parameters, but with the tags as well
5733+
RunInstances(context.TODO(), gomock.Any()).
5734+
Return(&ec2.RunInstancesOutput{
5735+
Instances: []types.Instance{
5736+
{
5737+
State: &types.InstanceState{
5738+
Name: types.InstanceStateNamePending,
5739+
},
5740+
IamInstanceProfile: &types.IamInstanceProfile{
5741+
Arn: aws.String("arn:aws:iam::123456789012:instance-profile/foo"),
5742+
},
5743+
InstanceId: aws.String("two"),
5744+
InstanceType: types.InstanceTypeM5Large,
5745+
SubnetId: aws.String("subnet-1"),
5746+
ImageId: aws.String("ami-1"),
5747+
RootDeviceName: aws.String("device-1"),
5748+
BlockDeviceMappings: []types.InstanceBlockDeviceMapping{
5749+
{
5750+
DeviceName: aws.String("device-1"),
5751+
Ebs: &types.EbsInstanceBlockDevice{
5752+
VolumeId: aws.String("volume-1"),
5753+
},
5754+
},
5755+
},
5756+
Placement: &types.Placement{
5757+
AvailabilityZone: &az,
5758+
},
5759+
CapacityReservationId: aws.String("cr-12345678901234567"),
5760+
CapacityReservationSpecification: &types.CapacityReservationSpecificationResponse{
5761+
CapacityReservationPreference: types.CapacityReservationPreferenceCapacityReservationsOnly,
5762+
},
5763+
InstanceLifecycle: types.InstanceLifecycleTypeScheduled,
5764+
},
5765+
},
5766+
}, nil)
5767+
m.
5768+
DescribeNetworkInterfaces(context.TODO(), gomock.Any()).
5769+
Return(&ec2.DescribeNetworkInterfacesOutput{
5770+
NetworkInterfaces: []types.NetworkInterface{},
5771+
NextToken: nil,
5772+
}, nil)
5773+
},
5774+
check: func(instance *infrav1.Instance, err error) {
5775+
if err != nil {
5776+
t.Fatalf("did not expect error: %v", err)
5777+
}
5778+
},
5779+
},
56565780
}
56575781
for _, tc := range testcases {
56585782
t.Run(tc.name, func(t *testing.T) {

pkg/cloud/services/ec2/launchtemplate.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,7 @@ func (s *Service) createLaunchTemplateData(scope scope.LaunchTemplateScope, imag
650650
}
651651
data.InstanceMarketOptions = instanceMarketOptions
652652
data.PrivateDnsNameOptions = getLaunchTemplatePrivateDNSNameOptionsRequest(scope.GetLaunchTemplate().PrivateDNSName)
653+
data.CapacityReservationSpecification = getLaunchTemplateCapacityReservationSpecification(scope.GetLaunchTemplate())
653654

654655
blockDeviceMappings := []types.LaunchTemplateBlockDeviceMappingRequest{}
655656

@@ -682,6 +683,24 @@ func (s *Service) createLaunchTemplateData(scope scope.LaunchTemplateScope, imag
682683
return data, nil
683684
}
684685

686+
func getLaunchTemplateCapacityReservationSpecification(awsLaunchTemplate *expinfrav1.AWSLaunchTemplate) *types.LaunchTemplateCapacityReservationSpecificationRequest {
687+
if awsLaunchTemplate == nil {
688+
return nil
689+
}
690+
if awsLaunchTemplate.CapacityReservationID == nil && awsLaunchTemplate.CapacityReservationPreference == "" {
691+
return nil
692+
}
693+
spec := &types.LaunchTemplateCapacityReservationSpecificationRequest{
694+
CapacityReservationPreference: CapacityReservationPreferenceToSDK(awsLaunchTemplate.CapacityReservationPreference),
695+
}
696+
if awsLaunchTemplate.CapacityReservationID != nil {
697+
spec.CapacityReservationTarget = &types.CapacityReservationTarget{
698+
CapacityReservationId: awsLaunchTemplate.CapacityReservationID,
699+
}
700+
}
701+
return spec
702+
}
703+
685704
func volumeToLaunchTemplateBlockDeviceMappingRequest(v *infrav1.Volume) *types.LaunchTemplateBlockDeviceMappingRequest {
686705
ltEbsDevice := &types.LaunchTemplateEbsBlockDeviceRequest{
687706
DeleteOnTermination: aws.Bool(true),
@@ -829,6 +848,32 @@ func SDKToSpotMarketOptions(instanceMarketOptions *types.LaunchTemplateInstanceM
829848
return result
830849
}
831850

851+
func SDKToCapacityReservationPreference(preference types.CapacityReservationPreference) infrav1.CapacityReservationPreference {
852+
switch preference {
853+
case types.CapacityReservationPreferenceCapacityReservationsOnly:
854+
return infrav1.CapacityReservationPreferenceOnly
855+
case types.CapacityReservationPreferenceNone:
856+
return infrav1.CapacityReservationPreferenceNone
857+
case types.CapacityReservationPreferenceOpen:
858+
return infrav1.CapacityReservationPreferenceOpen
859+
default:
860+
return ""
861+
}
862+
}
863+
864+
func CapacityReservationPreferenceToSDK(preference infrav1.CapacityReservationPreference) types.CapacityReservationPreference {
865+
switch preference {
866+
case infrav1.CapacityReservationPreferenceNone:
867+
return types.CapacityReservationPreferenceNone
868+
case infrav1.CapacityReservationPreferenceOnly:
869+
return types.CapacityReservationPreferenceCapacityReservationsOnly
870+
case infrav1.CapacityReservationPreferenceOpen:
871+
return types.CapacityReservationPreferenceOpen
872+
default:
873+
return ""
874+
}
875+
}
876+
832877
// SDKToLaunchTemplate converts an AWS EC2 SDK instance to the CAPA instance type.
833878
func (s *Service) SDKToLaunchTemplate(d types.LaunchTemplateVersion) (*expinfrav1.AWSLaunchTemplate, string, *apimachinerytypes.NamespacedName, *string, error) {
834879
v := d.LaunchTemplateData

pkg/cloud/services/ec2/launchtemplate_test.go

Lines changed: 92 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,18 @@ func TestLaunchTemplateDataCreation(t *testing.T) {
14391439
})
14401440
}
14411441

1442+
var LaunchTemplateVersionIgnoreUnexported = cmpopts.IgnoreUnexported(
1443+
ec2types.CapacityReservationTarget{},
1444+
ec2types.LaunchTemplateCapacityReservationSpecificationRequest{},
1445+
ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{},
1446+
ec2types.LaunchTemplateSpotMarketOptionsRequest{},
1447+
ec2types.LaunchTemplateInstanceMarketOptionsRequest{},
1448+
ec2types.Tag{},
1449+
ec2types.LaunchTemplateTagSpecificationRequest{},
1450+
ec2types.RequestLaunchTemplateData{},
1451+
ec2.CreateLaunchTemplateVersionInput{},
1452+
)
1453+
14421454
func TestCreateLaunchTemplateVersion(t *testing.T) {
14431455
mockCtrl := gomock.NewController(t)
14441456
defer mockCtrl.Finish()
@@ -1459,6 +1471,7 @@ func TestCreateLaunchTemplateVersion(t *testing.T) {
14591471
awsResourceReference []infrav1.AWSResourceReference
14601472
expect func(m *mocks.MockEC2APIMockRecorder)
14611473
wantErr bool
1474+
mpScopeUpdater func(*scope.MachinePoolScope)
14621475
marketType ec2types.MarketType
14631476
}{
14641477
{
@@ -1506,24 +1519,22 @@ func TestCreateLaunchTemplateVersion(t *testing.T) {
15061519
func(ctx context.Context, arg *ec2.CreateLaunchTemplateVersionInput, requestOptions ...request.Option) {
15071520
// formatting added to match tags slice during cmp.Equal()
15081521
formatTagsInput(arg)
1509-
if !cmp.Equal(expectedInput, arg, cmpopts.IgnoreUnexported(
1510-
ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{},
1511-
ec2types.LaunchTemplateSpotMarketOptionsRequest{},
1512-
ec2types.LaunchTemplateInstanceMarketOptionsRequest{},
1513-
ec2types.Tag{},
1514-
ec2types.LaunchTemplateTagSpecificationRequest{},
1515-
ec2types.RequestLaunchTemplateData{},
1516-
ec2.CreateLaunchTemplateVersionInput{},
1517-
)) {
1518-
t.Fatalf("mismatch in input expected: %+v, but got %+v, diff: %s", expectedInput, arg, cmp.Diff(expectedInput, arg))
1522+
if !cmp.Equal(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported) {
1523+
t.Fatalf("mismatch in input expected: %+v, but got %+v, diff: %s", expectedInput, arg, cmp.Diff(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported))
15191524
}
15201525
})
15211526
},
15221527
},
15231528
{
15241529
name: "Should successfully create launch template version with capacity-block",
15251530
awsResourceReference: []infrav1.AWSResourceReference{{ID: aws.String("1")}},
1526-
marketType: ec2types.MarketTypeCapacityBlock,
1531+
mpScopeUpdater: func(mps *scope.MachinePoolScope) {
1532+
spec := mps.AWSMachinePool.Spec
1533+
spec.AWSLaunchTemplate.CapacityReservationID = aws.String("cr-12345678901234567")
1534+
spec.AWSLaunchTemplate.MarketType = infrav1.MarketTypeCapacityBlock
1535+
spec.AWSLaunchTemplate.SpotMarketOptions = nil
1536+
mps.AWSMachinePool.Spec = spec
1537+
},
15271538
expect: func(m *mocks.MockEC2APIMockRecorder) {
15281539
sgMap := make(map[infrav1.SecurityGroupRole]infrav1.SecurityGroup)
15291540
sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"}
@@ -1542,6 +1553,11 @@ func TestCreateLaunchTemplateVersion(t *testing.T) {
15421553
InstanceMarketOptions: &ec2types.LaunchTemplateInstanceMarketOptionsRequest{
15431554
MarketType: ec2types.MarketTypeCapacityBlock,
15441555
},
1556+
CapacityReservationSpecification: &ec2types.LaunchTemplateCapacityReservationSpecificationRequest{
1557+
CapacityReservationTarget: &ec2types.CapacityReservationTarget{
1558+
CapacityReservationId: aws.String("cr-12345678901234567"),
1559+
},
1560+
},
15451561
TagSpecifications: []ec2types.LaunchTemplateTagSpecificationRequest{
15461562
{
15471563
ResourceType: ec2types.ResourceTypeInstance,
@@ -1563,16 +1579,66 @@ func TestCreateLaunchTemplateVersion(t *testing.T) {
15631579
func(ctx context.Context, arg *ec2.CreateLaunchTemplateVersionInput, requestOptions ...request.Option) {
15641580
// formatting added to match tags slice during cmp.Equal()
15651581
formatTagsInput(arg)
1566-
if !cmp.Equal(expectedInput, arg, cmpopts.IgnoreUnexported(
1567-
ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{},
1568-
ec2types.LaunchTemplateSpotMarketOptionsRequest{},
1569-
ec2types.LaunchTemplateInstanceMarketOptionsRequest{},
1570-
ec2types.Tag{},
1571-
ec2types.LaunchTemplateTagSpecificationRequest{},
1572-
ec2types.RequestLaunchTemplateData{},
1573-
ec2.CreateLaunchTemplateVersionInput{},
1574-
)) {
1575-
t.Fatalf("mismatch in input expected: %+v, but got %+v, diff: %s", expectedInput, arg, cmp.Diff(expectedInput, arg))
1582+
if !cmp.Equal(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported) {
1583+
t.Fatalf("mismatch in input expected: %+v, but got %+v, diff: %s", expectedInput, arg, cmp.Diff(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported))
1584+
}
1585+
})
1586+
},
1587+
},
1588+
{
1589+
name: "Should successfully create launch template version with capacity reservation ID and preference",
1590+
awsResourceReference: []infrav1.AWSResourceReference{{ID: aws.String("1")}},
1591+
mpScopeUpdater: func(mps *scope.MachinePoolScope) {
1592+
spec := mps.AWSMachinePool.Spec
1593+
spec.AWSLaunchTemplate.CapacityReservationID = aws.String("cr-12345678901234567")
1594+
spec.AWSLaunchTemplate.CapacityReservationPreference = infrav1.CapacityReservationPreferenceOnly
1595+
spec.AWSLaunchTemplate.SpotMarketOptions = nil
1596+
mps.AWSMachinePool.Spec = spec
1597+
},
1598+
expect: func(m *mocks.MockEC2APIMockRecorder) {
1599+
sgMap := make(map[infrav1.SecurityGroupRole]infrav1.SecurityGroup)
1600+
sgMap[infrav1.SecurityGroupNode] = infrav1.SecurityGroup{ID: "1"}
1601+
sgMap[infrav1.SecurityGroupLB] = infrav1.SecurityGroup{ID: "2"}
1602+
1603+
expectedInput := &ec2.CreateLaunchTemplateVersionInput{
1604+
LaunchTemplateData: &ec2types.RequestLaunchTemplateData{
1605+
InstanceType: ec2types.InstanceTypeT3Large,
1606+
IamInstanceProfile: &ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{
1607+
Name: aws.String("instance-profile"),
1608+
},
1609+
KeyName: aws.String("default"),
1610+
UserData: ptr.To[string](base64.StdEncoding.EncodeToString(userData)),
1611+
SecurityGroupIds: []string{"nodeSG", "lbSG", "1"},
1612+
ImageId: aws.String("imageID"),
1613+
CapacityReservationSpecification: &ec2types.LaunchTemplateCapacityReservationSpecificationRequest{
1614+
CapacityReservationTarget: &ec2types.CapacityReservationTarget{
1615+
CapacityReservationId: aws.String("cr-12345678901234567"),
1616+
},
1617+
CapacityReservationPreference: ec2types.CapacityReservationPreferenceCapacityReservationsOnly,
1618+
},
1619+
TagSpecifications: []ec2types.LaunchTemplateTagSpecificationRequest{
1620+
{
1621+
ResourceType: ec2types.ResourceTypeInstance,
1622+
Tags: defaultEC2AndDataTags("aws-mp-name", "cluster-name", userDataSecretKey, testBootstrapDataHash),
1623+
},
1624+
{
1625+
ResourceType: ec2types.ResourceTypeVolume,
1626+
Tags: defaultEC2Tags("aws-mp-name", "cluster-name"),
1627+
},
1628+
},
1629+
},
1630+
LaunchTemplateId: aws.String("launch-template-id"),
1631+
}
1632+
m.CreateLaunchTemplateVersion(context.TODO(), gomock.AssignableToTypeOf(expectedInput)).Return(&ec2.CreateLaunchTemplateVersionOutput{
1633+
LaunchTemplateVersion: &ec2types.LaunchTemplateVersion{
1634+
LaunchTemplateId: aws.String("launch-template-id"),
1635+
},
1636+
}, nil).Do(
1637+
func(ctx context.Context, arg *ec2.CreateLaunchTemplateVersionInput, requestOptions ...request.Option) {
1638+
// formatting added to match tags slice during cmp.Equal()
1639+
formatTagsInput(arg)
1640+
if !cmp.Equal(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported) {
1641+
t.Fatalf("mismatch in input expected: %+v, but got %+v, diff: %s", expectedInput, arg, cmp.Diff(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported))
15761642
}
15771643
})
15781644
},
@@ -1619,15 +1685,7 @@ func TestCreateLaunchTemplateVersion(t *testing.T) {
16191685
func(ctx context.Context, arg *ec2.CreateLaunchTemplateVersionInput, requestOptions ...request.Option) {
16201686
// formatting added to match tags slice during cmp.Equal()
16211687
formatTagsInput(arg)
1622-
if !cmp.Equal(expectedInput, arg, cmpopts.IgnoreUnexported(
1623-
ec2types.LaunchTemplateIamInstanceProfileSpecificationRequest{},
1624-
ec2types.LaunchTemplateSpotMarketOptionsRequest{},
1625-
ec2types.LaunchTemplateInstanceMarketOptionsRequest{},
1626-
ec2types.Tag{},
1627-
ec2types.LaunchTemplateTagSpecificationRequest{},
1628-
ec2types.RequestLaunchTemplateData{},
1629-
ec2.CreateLaunchTemplateVersionInput{},
1630-
)) {
1688+
if !cmp.Equal(expectedInput, arg, LaunchTemplateVersionIgnoreUnexported) {
16311689
t.Fatalf("mismatch in input expected: %+v, got: %+v", expectedInput, arg)
16321690
}
16331691
})
@@ -1645,13 +1703,11 @@ func TestCreateLaunchTemplateVersion(t *testing.T) {
16451703
cs, err := setupClusterScope(client)
16461704
g.Expect(err).NotTo(HaveOccurred())
16471705

1648-
var ms *scope.MachinePoolScope
1649-
if tc.marketType == ec2types.MarketTypeCapacityBlock {
1650-
ms, err = setupCapacityBlocksMachinePoolScope(client, cs)
1651-
} else {
1652-
ms, err = setupMachinePoolScope(client, cs)
1653-
}
1706+
ms, err := setupMachinePoolScope(client, cs)
16541707
g.Expect(err).NotTo(HaveOccurred())
1708+
if updateScope := tc.mpScopeUpdater; updateScope != nil {
1709+
updateScope(ms)
1710+
}
16551711

16561712
ms.AWSMachinePool.Spec.AWSLaunchTemplate.AdditionalSecurityGroups = tc.awsResourceReference
16571713

0 commit comments

Comments
 (0)