Skip to content

Commit 58fd1c2

Browse files
feat: support dynamic EC2 placement and block device overrides (github-aws-runners#5178)
## Description Add missing dynamic EC2 override labels for placement and block device mappings, and align `parseEc2OverrideConfig` conditional ordering with the AWS SDK request property order. This adds parser support for: - `ghr-ec2-placement-group-name` - `ghr-ec2-placement-group-id` - `ghr-ec2-placement-availability-zone-id` - `ghr-ec2-block-device-name` When dynamic EBS or block device labels create an override without an explicit device name, scale-up now loads the default launch template version and uses its block device name for the CreateFleet override. The scale-up Lambda policy now includes `ec2:DescribeLaunchTemplateVersions` for that lookup. ## Test Plan ## Related Issues github-aws-runners#5173
1 parent 246949e commit 58fd1c2

3 files changed

Lines changed: 268 additions & 100 deletions

File tree

lambdas/functions/control-plane/src/scale-runners/scale-up.test.ts

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { DescribeLaunchTemplateVersionsCommand, EC2Client } from '@aws-sdk/client-ec2';
12
import { PutParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
23
import { mockClient } from 'aws-sdk-client-mock';
34
import 'aws-sdk-client-mock-jest/vitest';
@@ -32,6 +33,7 @@ const mockOctokit = {
3233

3334
const mockCreateRunner = vi.mocked(createRunner);
3435
const mockListRunners = vi.mocked(listEC2Runners);
36+
const mockEC2Client = mockClient(EC2Client);
3537
const mockSSMClient = mockClient(SSMClient);
3638
const mockSSMgetParameter = vi.mocked(getParameter);
3739
const mockPublishRetryMessage = vi.mocked(publishRetryMessage);
@@ -142,6 +144,22 @@ beforeEach(() => {
142144
vi.clearAllMocks();
143145
setDefaults();
144146

147+
mockEC2Client.reset();
148+
mockEC2Client.on(DescribeLaunchTemplateVersionsCommand).resolves({
149+
LaunchTemplateVersions: [
150+
{
151+
LaunchTemplateData: {
152+
BlockDeviceMappings: [
153+
{
154+
DeviceName: '/dev/sda1',
155+
Ebs: {},
156+
},
157+
],
158+
},
159+
},
160+
],
161+
});
162+
145163
defaultSSMGetParameterMockImpl();
146164
defaultOctokitMockImpl();
147165

@@ -650,6 +668,86 @@ describe('scaleUp with GHES', () => {
650668
);
651669
});
652670

671+
it('loads the launch template block device name for dynamic EBS labels without DeviceName', async () => {
672+
mockEC2Client.on(DescribeLaunchTemplateVersionsCommand).resolves({
673+
LaunchTemplateVersions: [
674+
{
675+
LaunchTemplateData: {
676+
BlockDeviceMappings: [
677+
{
678+
DeviceName: '/dev/sdb',
679+
VirtualName: 'ephemeral0',
680+
},
681+
{
682+
DeviceName: '/dev/sdf',
683+
Ebs: {},
684+
},
685+
],
686+
},
687+
},
688+
],
689+
});
690+
691+
const testDataWithEbsLabels = [
692+
{
693+
...TEST_DATA_SINGLE,
694+
labels: ['ghr-ec2-ebs-volume-size:100', 'ghr-ec2-ebs-volume-type:gp3'],
695+
messageId: 'test-ebs-device-name',
696+
},
697+
];
698+
699+
await scaleUpModule.scaleUp(testDataWithEbsLabels);
700+
701+
expect(mockEC2Client).toHaveReceivedCommandWith(DescribeLaunchTemplateVersionsCommand, {
702+
LaunchTemplateName: 'lt-1',
703+
Versions: ['$Default'],
704+
});
705+
expect(createRunner).toBeCalledWith(
706+
expect.objectContaining({
707+
ec2OverrideConfig: expect.objectContaining({
708+
BlockDeviceMappings: [
709+
{
710+
DeviceName: '/dev/sdf',
711+
Ebs: {
712+
VolumeSize: 100,
713+
VolumeType: 'gp3',
714+
},
715+
},
716+
],
717+
}),
718+
}),
719+
);
720+
});
721+
722+
it('does not load launch template block device name when DeviceName is provided by labels', async () => {
723+
const testDataWithEbsLabels = [
724+
{
725+
...TEST_DATA_SINGLE,
726+
labels: ['ghr-ec2-block-device-name:/dev/sdg', 'ghr-ec2-ebs-volume-size:100', 'ghr-ec2-ebs-volume-type:gp3'],
727+
messageId: 'test-explicit-ebs-device-name',
728+
},
729+
];
730+
731+
await scaleUpModule.scaleUp(testDataWithEbsLabels);
732+
733+
expect(mockEC2Client).not.toHaveReceivedCommand(DescribeLaunchTemplateVersionsCommand);
734+
expect(createRunner).toBeCalledWith(
735+
expect.objectContaining({
736+
ec2OverrideConfig: expect.objectContaining({
737+
BlockDeviceMappings: [
738+
{
739+
DeviceName: '/dev/sdg',
740+
Ebs: {
741+
VolumeSize: 100,
742+
VolumeType: 'gp3',
743+
},
744+
},
745+
],
746+
}),
747+
}),
748+
);
749+
});
750+
653751
it('handles messages with no labels gracefully', async () => {
654752
const testDataWithNoLabels = [
655753
{
@@ -2553,11 +2651,16 @@ describe('parseEc2OverrideConfig', () => {
25532651
});
25542652

25552653
describe('Placement', () => {
2556-
it('should parse placement-group label', () => {
2557-
const result = scaleUpModule.parseEc2OverrideConfig(['ghr-ec2-placement-group:my-placement-group']);
2654+
it('should parse placement-group-name label', () => {
2655+
const result = scaleUpModule.parseEc2OverrideConfig(['ghr-ec2-placement-group-name:my-placement-group']);
25582656
expect(result?.Placement?.GroupName).toBe('my-placement-group');
25592657
});
25602658

2659+
it('should parse placement-group-id label', () => {
2660+
const result = scaleUpModule.parseEc2OverrideConfig(['ghr-ec2-placement-group-id:pg-1234567890abcdef0']);
2661+
expect(result?.Placement?.GroupId).toBe('pg-1234567890abcdef0');
2662+
});
2663+
25612664
it('should parse placement-tenancy label', () => {
25622665
const result = scaleUpModule.parseEc2OverrideConfig(['ghr-ec2-placement-tenancy:dedicated']);
25632666
expect(result?.Placement?.Tenancy).toBe('dedicated');
@@ -2583,6 +2686,11 @@ describe('parseEc2OverrideConfig', () => {
25832686
expect(result?.Placement?.AvailabilityZone).toBe('us-west-2b');
25842687
});
25852688

2689+
it('should parse placement-availability-zone-id label', () => {
2690+
const result = scaleUpModule.parseEc2OverrideConfig(['ghr-ec2-placement-availability-zone-id:use1-az1']);
2691+
expect(result?.Placement?.AvailabilityZoneId).toBe('use1-az1');
2692+
});
2693+
25862694
it('should parse placement-spread-domain label', () => {
25872695
const result = scaleUpModule.parseEc2OverrideConfig(['ghr-ec2-placement-spread-domain:my-spread-domain']);
25882696
expect(result?.Placement?.SpreadDomain).toBe('my-spread-domain');
@@ -2599,7 +2707,7 @@ describe('parseEc2OverrideConfig', () => {
25992707

26002708
it('should parse multiple placement labels', () => {
26012709
const result = scaleUpModule.parseEc2OverrideConfig([
2602-
'ghr-ec2-placement-group:group-1',
2710+
'ghr-ec2-placement-group-name:group-1',
26032711
'ghr-ec2-placement-tenancy:dedicated',
26042712
'ghr-ec2-placement-availability-zone:us-east-1b',
26052713
]);
@@ -2610,6 +2718,17 @@ describe('parseEc2OverrideConfig', () => {
26102718
});
26112719

26122720
describe('Block Device Mappings', () => {
2721+
it('should parse block-device-name label', () => {
2722+
const result = scaleUpModule.parseEc2OverrideConfig(['ghr-ec2-block-device-name:/dev/sdg']);
2723+
expect(result?.BlockDeviceMappings?.[0]?.DeviceName).toBe('/dev/sdg');
2724+
});
2725+
2726+
it('should use default block device name when provided', () => {
2727+
const result = scaleUpModule.parseEc2OverrideConfig(['ghr-ec2-ebs-volume-size:100'], '/dev/sda1');
2728+
expect(result?.BlockDeviceMappings?.[0]?.DeviceName).toBe('/dev/sda1');
2729+
expect(result?.BlockDeviceMappings?.[0]?.Ebs?.VolumeSize).toBe(100);
2730+
});
2731+
26132732
it('should parse ebs-volume-size label as number', () => {
26142733
const result = scaleUpModule.parseEc2OverrideConfig(['ghr-ec2-ebs-volume-size:100']);
26152734
expect(result?.BlockDeviceMappings?.[0]?.Ebs?.VolumeSize).toBe(100);
@@ -2690,7 +2809,6 @@ describe('parseEc2OverrideConfig', () => {
26902809
it('should initialize BlockDeviceMappings when not present', () => {
26912810
const result = scaleUpModule.parseEc2OverrideConfig(['ghr-ec2-ebs-volume-size:50']);
26922811
expect(result?.BlockDeviceMappings).toBeDefined();
2693-
// expect(result?.BlockDeviceMappings?.[0]?.DeviceName).toBe('/dev/sda1');
26942812
});
26952813
});
26962814

@@ -3106,7 +3224,7 @@ describe('parseEc2OverrideConfig', () => {
31063224
'ghr-ec2-max-price:0.75',
31073225
'ghr-ec2-priority:1',
31083226
// Placement
3109-
'ghr-ec2-placement-group:my-group',
3227+
'ghr-ec2-placement-group-name:my-group',
31103228
'ghr-ec2-placement-tenancy:dedicated',
31113229
// Block Device
31123230
'ghr-ec2-ebs-volume-size:200',

0 commit comments

Comments
 (0)