Skip to content

Commit 7f52063

Browse files
committed
refactor, prepare for creating muliple runners
1 parent 99c013c commit 7f52063

File tree

6 files changed

+128
-22
lines changed

6 files changed

+128
-22
lines changed

modules/runners/lambdas/runners/jest.config.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ module.exports = {
55
collectCoverageFrom: ['src/**/*.{ts,js,jsx}', '!src/**/*local*.ts', 'src/**/*.d.ts'],
66
coverageThreshold: {
77
global: {
8-
branches: 80,
9-
functions: 80,
10-
lines: 80,
11-
statements: 80,
8+
branches: 92,
9+
functions: 92,
10+
lines: 92,
11+
statements: 92,
1212
},
1313
},
1414
};

modules/runners/lambdas/runners/src/aws/runners.test.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,25 @@ describe('list instances', () => {
126126
const resp = await listEC2Runners();
127127
expect(resp.length).toBe(0);
128128
});
129+
130+
it('Instances with no tags.', async () => {
131+
const noInstances: AWS.EC2.DescribeInstancesResult = {
132+
Reservations: [
133+
{
134+
Instances: [
135+
{
136+
LaunchTime: new Date('2020-10-11T14:48:00.000+09:00'),
137+
InstanceId: 'i-5678',
138+
Tags: undefined,
139+
},
140+
],
141+
},
142+
],
143+
};
144+
mockDescribeInstances.promise.mockReturnValue(noInstances);
145+
const resp = await listEC2Runners();
146+
expect(resp.length).toBe(1);
147+
});
129148
});
130149

131150
describe('terminate runner', () => {
@@ -159,6 +178,7 @@ describe('create runner', () => {
159178
type: 'Org',
160179
capacityType: 'spot',
161180
allocationStrategy: 'capacity-optimized',
181+
totalTargetCapacity: 1,
162182
};
163183

164184
beforeEach(() => {
@@ -172,23 +192,47 @@ describe('create runner', () => {
172192
mockSSM.putParameter.mockImplementation(() => mockPutParameter);
173193
});
174194

175-
it('calls run instances with the correct config for repo', async () => {
195+
it('calls create fleet of 1 instance with the correct config for repo', async () => {
176196
await createRunner(createRunnerConfig({ ...defaultRunnerConfig, type: 'Repo' }));
177197
expect(mockEC2.createFleet).toBeCalledWith(
178198
expectedCreateFleetRequest({ ...defaultExpectedFleetRequestValues, type: 'Repo' }),
179199
);
200+
expect(mockSSM.putParameter).toBeCalledTimes(1);
201+
});
202+
203+
it('calls create fleet of 2 instances with the correct config for org ', async () => {
204+
const instances = [{ InstanceIds: ['i-1234', 'i-5678'] }];
205+
mockCreateFleet.promise.mockReturnValue({
206+
Instances: instances,
207+
});
208+
209+
await createRunner({ ...createRunnerConfig(defaultRunnerConfig), numberOfRunners: 2 });
210+
211+
expect(mockEC2.createFleet).toBeCalledWith(
212+
expectedCreateFleetRequest({ ...defaultExpectedFleetRequestValues, totalTargetCapacity: 2 }),
213+
);
214+
expect(mockSSM.putParameter).toBeCalledTimes(2);
215+
for (const instance of instances[0].InstanceIds) {
216+
expect(mockSSM.putParameter).toBeCalledWith({
217+
Name: `unit-test-environment-${instance}`,
218+
Type: 'SecureString',
219+
Value: 'bla',
220+
});
221+
}
180222
});
181223

182-
it('calls run instances with the correct config for org', async () => {
224+
it('calls create fleet of 1 instance with the correct config for org', async () => {
183225
await createRunner(createRunnerConfig(defaultRunnerConfig));
184226
expect(mockEC2.createFleet).toBeCalledWith(expectedCreateFleetRequest(defaultExpectedFleetRequestValues));
227+
expect(mockSSM.putParameter).toBeCalledTimes(1);
185228
});
186229

187-
it('calls run instances with the on-demand capacity', async () => {
230+
it('calls create fleet of 1 instance with the on-demand capacity', async () => {
188231
await createRunner(createRunnerConfig({ ...defaultRunnerConfig, capacityType: 'on-demand' }));
189232
expect(mockEC2.createFleet).toBeCalledWith(
190233
expectedCreateFleetRequest({ ...defaultExpectedFleetRequestValues, capacityType: 'on-demand' }),
191234
);
235+
expect(mockSSM.putParameter).toBeCalledTimes(1);
192236
});
193237

194238
it('calls run instances with the on-demand capacity', async () => {
@@ -226,6 +270,7 @@ describe('create runner with errors', () => {
226270
type: 'Repo',
227271
capacityType: 'spot',
228272
allocationStrategy: 'capacity-optimized',
273+
totalTargetCapacity: 1,
229274
};
230275
beforeEach(() => {
231276
jest.clearAllMocks();
@@ -332,6 +377,7 @@ interface ExpectedFleetRequestValues {
332377
capacityType: EC2.DefaultTargetCapacityType;
333378
allocationStrategy: EC2.AllocationStrategy;
334379
maxSpotPrice?: string;
380+
totalTargetCapacity: number;
335381
}
336382

337383
function expectedCreateFleetRequest(expectedValues: ExpectedFleetRequestValues): AWS.EC2.CreateFleetRequest {
@@ -378,7 +424,7 @@ function expectedCreateFleetRequest(expectedValues: ExpectedFleetRequestValues):
378424
],
379425
TargetCapacitySpecification: {
380426
DefaultTargetCapacityType: expectedValues.capacityType,
381-
TotalTargetCapacity: 1,
427+
TotalTargetCapacity: expectedValues.totalTargetCapacity,
382428
},
383429
Type: 'instant',
384430
};

modules/runners/lambdas/runners/src/aws/runners.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface RunnerInputParameters {
3939
maxSpotPrice?: string;
4040
instanceAllocationStrategy: EC2.SpotAllocationStrategy;
4141
};
42+
numberOfRunners?: number;
4243
}
4344

4445
export async function listEC2Runners(filters: ListRunnerFilters | undefined = undefined): Promise<RunnerList[]> {
@@ -107,6 +108,7 @@ export async function createRunner(runnerParameters: RunnerInputParameters): Pro
107108
logger.debug('Runner configuration: ' + JSON.stringify(runnerParameters), LogFields.print());
108109

109110
const ec2 = new EC2();
111+
const numberOfRunners = runnerParameters.numberOfRunners ? runnerParameters.numberOfRunners : 1;
110112

111113
let fleet: AWS.EC2.CreateFleetResult;
112114
try {
@@ -130,7 +132,7 @@ export async function createRunner(runnerParameters: RunnerInputParameters): Pro
130132
AllocationStrategy: 'capacity-optimized',
131133
},
132134
TargetCapacitySpecification: {
133-
TotalTargetCapacity: 1,
135+
TotalTargetCapacity: numberOfRunners,
134136
DefaultTargetCapacityType: runnerParameters.ec2instanceCriteria.targetCapacityType,
135137
},
136138
TagSpecifications: [

modules/runners/lambdas/runners/src/aws/ssm.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,22 @@ describe('Test getParameterValue', () => {
3737
// Assert
3838
expect(result).toBe(parameterValue);
3939
});
40+
41+
test('Gets invalid parameters and returns string', async () => {
42+
// Arrange
43+
const parameterName = 'invalid';
44+
const output: GetParameterCommandOutput = {
45+
$metadata: {
46+
httpStatusCode: 200,
47+
},
48+
};
49+
50+
SSM.prototype.getParameter = jest.fn().mockResolvedValue(output);
51+
52+
// Act
53+
const result = await getParameterValue(parameterName);
54+
55+
// Assert
56+
expect(result).toBe(undefined);
57+
});
4058
});

modules/runners/lambdas/runners/src/scale-runners/scale-up.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,14 @@ describe('scaleUp with public GH', () => {
379379
expect(listEC2Runners).not.toBeCalled();
380380
});
381381

382+
it('does not list runners when no workflows are queued (check_run)', async () => {
383+
mockOctokit.checks.get.mockImplementation(() => ({
384+
data: { status: 'completed' },
385+
}));
386+
await scaleUpModule.scaleUp('aws:sqs', { ...TEST_DATA, eventType: 'check_run' });
387+
expect(listEC2Runners).not.toBeCalled();
388+
});
389+
382390
describe('on org level', () => {
383391
beforeEach(() => {
384392
process.env.ENABLE_ORGANIZATION_RUNNERS = 'true';

modules/runners/lambdas/runners/src/scale-runners/scale-up.ts

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { listEC2Runners, createRunner } from './../aws/runners';
1+
import { listEC2Runners, createRunner, RunnerInputParameters } from './../aws/runners';
22
import { createOctoClient, createGithubAppAuth, createGithubInstallationAuth } from '../gh-auth/gh-auth';
33
import yn from 'yn';
44
import { Octokit } from '@octokit/rest';
@@ -95,6 +95,44 @@ async function isJobQueued(githubInstallationClient: Octokit, payload: ActionReq
9595
return isQueued;
9696
}
9797

98+
async function createRunners(
99+
enableOrgLevel: boolean,
100+
githubInstallationClient: Octokit,
101+
payload: ActionRequestMessage,
102+
runnerExtraLabels: string | undefined,
103+
runnerGroup: string | undefined,
104+
ghesBaseUrl: string,
105+
ephemeral: boolean,
106+
runnerType: 'Org' | 'Repo',
107+
environment: string,
108+
runnerOwner: string,
109+
subnets: string[],
110+
launchTemplateName: string,
111+
ec2instanceCriteria: RunnerInputParameters['ec2instanceCriteria'],
112+
): Promise<void> {
113+
const token = await getGithubRunnerRegistrationToken(enableOrgLevel, githubInstallationClient, payload);
114+
115+
const runnerServiceConfig = generateRunnerServiceConfig(
116+
runnerExtraLabels,
117+
runnerGroup,
118+
ghesBaseUrl,
119+
ephemeral,
120+
token,
121+
runnerType,
122+
payload,
123+
);
124+
125+
await createRunner({
126+
environment,
127+
runnerServiceConfig,
128+
runnerOwner,
129+
runnerType,
130+
subnets,
131+
launchTemplateName,
132+
ec2instanceCriteria,
133+
});
134+
}
135+
98136
export async function scaleUp(eventSource: string, payload: ActionRequestMessage): Promise<void> {
99137
logger.info(
100138
`Received ${payload.eventType} from ${payload.repositoryOwner}/${payload.repositoryName}`,
@@ -158,32 +196,26 @@ export async function scaleUp(eventSource: string, payload: ActionRequestMessage
158196
if (currentRunners.length < maximumRunners) {
159197
logger.info(`Attempting to launch a new runner`, LogFields.print());
160198

161-
const token = await getGithubRunnerRegistrationToken(enableOrgLevel, githubInstallationClient, payload);
162-
163-
const runnerServiceConfig = generateRunnerServiceConfig(
199+
await createRunners(
200+
enableOrgLevel,
201+
githubInstallationClient,
202+
payload,
164203
runnerExtraLabels,
165204
runnerGroup,
166205
ghesBaseUrl,
167206
ephemeral,
168-
token,
169207
runnerType,
170-
payload,
171-
);
172-
173-
await createRunner({
174208
environment,
175-
runnerServiceConfig,
176209
runnerOwner,
177-
runnerType,
178210
subnets,
179211
launchTemplateName,
180-
ec2instanceCriteria: {
212+
{
181213
instanceTypes,
182214
targetCapacityType: instanceTargetTargetCapacityType,
183215
maxSpotPrice: instanceMaxSpotPrice,
184216
instanceAllocationStrategy: instanceAllocationStrategy,
185217
},
186-
});
218+
);
187219
} else {
188220
logger.info('No runner will be created, maximum number of runners reached.', LogFields.print());
189221
if (ephemeral) {

0 commit comments

Comments
 (0)