Skip to content
This repository was archived by the owner on Jan 16, 2025. It is now read-only.

Commit a1a47a4

Browse files
mcaulifnnpalm
andauthored
feat(runners): Namespace Application tag (#2182)
Tag `Application` on runner instances will be replaced by `ghr:Application` Co-authored-by: Niek Palm <[email protected]>
1 parent e5073eb commit a1a47a4

File tree

3 files changed

+157
-88
lines changed

3 files changed

+157
-88
lines changed

Diff for: modules/runners/lambdas/runners/src/aws/runners.test.ts

+66-34
Original file line numberDiff line numberDiff line change
@@ -15,41 +15,53 @@ const ORG_NAME = 'SomeAwesomeCoder';
1515
const REPO_NAME = `${ORG_NAME}/some-amazing-library`;
1616
const ENVIRONMENT = 'unit-test-environment';
1717

18-
describe('list instances', () => {
19-
const mockDescribeInstances = { promise: jest.fn() };
20-
beforeEach(() => {
21-
jest.clearAllMocks();
22-
mockEC2.describeInstances.mockImplementation(() => mockDescribeInstances);
23-
const mockRunningInstances: AWS.EC2.DescribeInstancesResult = {
24-
Reservations: [
18+
const mockDescribeInstances = { promise: jest.fn() };
19+
mockEC2.describeInstances.mockImplementation(() => mockDescribeInstances);
20+
const mockRunningInstances: AWS.EC2.DescribeInstancesResult = {
21+
Reservations: [
22+
{
23+
Instances: [
2524
{
26-
Instances: [
27-
{
28-
LaunchTime: new Date('2020-10-10T14:48:00.000+09:00'),
29-
InstanceId: 'i-1234',
30-
Tags: [
31-
{ Key: 'Application', Value: 'github-action-runner' },
32-
{ Key: 'Type', Value: 'Org' },
33-
{ Key: 'Owner', Value: 'CoderToCat' },
34-
],
35-
},
36-
{
37-
LaunchTime: new Date('2020-10-11T14:48:00.000+09:00'),
38-
InstanceId: 'i-5678',
39-
Tags: [
40-
{ Key: 'Owner', Value: REPO_NAME },
41-
{ Key: 'Type', Value: 'Repo' },
42-
{ Key: 'Application', Value: 'github-action-runner' },
43-
],
44-
},
25+
LaunchTime: new Date('2020-10-10T14:48:00.000+09:00'),
26+
InstanceId: 'i-1234',
27+
Tags: [
28+
{ Key: 'ghr:Application', Value: 'github-action-runner' },
29+
{ Key: 'Type', Value: 'Org' },
30+
{ Key: 'Owner', Value: 'CoderToCat' },
4531
],
4632
},
4733
],
48-
};
49-
mockDescribeInstances.promise.mockReturnValue(mockRunningInstances);
34+
},
35+
],
36+
};
37+
const mockRunningInstancesLegacy: AWS.EC2.DescribeInstancesResult = {
38+
Reservations: [
39+
{
40+
Instances: [
41+
{
42+
LaunchTime: new Date('2020-10-11T14:48:00.000+09:00'),
43+
InstanceId: 'i-5678',
44+
Tags: [
45+
{ Key: 'Owner', Value: REPO_NAME },
46+
{ Key: 'Type', Value: 'Repo' },
47+
{ Key: 'Application', Value: 'github-action-runner' },
48+
],
49+
},
50+
],
51+
},
52+
],
53+
};
54+
55+
describe('list instances', () => {
56+
beforeEach(() => {
57+
jest.resetModules();
58+
jest.clearAllMocks();
5059
});
5160

5261
it('returns a list of instances', async () => {
62+
mockDescribeInstances.promise
63+
.mockReturnValueOnce(mockRunningInstances)
64+
.mockReturnValueOnce(mockRunningInstancesLegacy);
5365
const resp = await listEC2Runners();
5466
expect(resp.length).toBe(2);
5567
expect(resp).toContainEqual({
@@ -67,41 +79,61 @@ describe('list instances', () => {
6779
});
6880

6981
it('calls EC2 describe instances', async () => {
82+
mockDescribeInstances.promise
83+
.mockReturnValueOnce(mockRunningInstances)
84+
.mockReturnValueOnce(mockRunningInstancesLegacy);
7085
await listEC2Runners();
7186
expect(mockEC2.describeInstances).toBeCalled();
7287
});
7388

7489
it('filters instances on repo name', async () => {
90+
mockDescribeInstances.promise
91+
.mockReturnValueOnce(mockRunningInstances)
92+
.mockReturnValueOnce(mockRunningInstancesLegacy);
7593
await listEC2Runners({ runnerType: 'Repo', runnerOwner: REPO_NAME, environment: undefined });
7694
expect(mockEC2.describeInstances).toBeCalledWith({
7795
Filters: [
78-
{ Name: 'tag:Application', Values: ['github-action-runner'] },
7996
{ Name: 'instance-state-name', Values: ['running', 'pending'] },
8097
{ Name: 'tag:Type', Values: ['Repo'] },
8198
{ Name: 'tag:Owner', Values: [REPO_NAME] },
99+
{ Name: 'tag:ghr:Application', Values: ['github-action-runner'] },
100+
],
101+
});
102+
expect(mockEC2.describeInstances).toBeCalledWith({
103+
Filters: [
104+
{ Name: 'instance-state-name', Values: ['running', 'pending'] },
105+
{ Name: 'tag:Type', Values: ['Repo'] },
106+
{ Name: 'tag:Owner', Values: [REPO_NAME] },
107+
{ Name: 'tag:Application', Values: ['github-action-runner'] },
82108
],
83109
});
84110
});
85111

86112
it('filters instances on org name', async () => {
113+
mockDescribeInstances.promise
114+
.mockReturnValueOnce(mockRunningInstances)
115+
.mockReturnValueOnce(mockRunningInstancesLegacy);
87116
await listEC2Runners({ runnerType: 'Org', runnerOwner: ORG_NAME, environment: undefined });
88117
expect(mockEC2.describeInstances).toBeCalledWith({
89118
Filters: [
90-
{ Name: 'tag:Application', Values: ['github-action-runner'] },
91119
{ Name: 'instance-state-name', Values: ['running', 'pending'] },
92120
{ Name: 'tag:Type', Values: ['Org'] },
93121
{ Name: 'tag:Owner', Values: [ORG_NAME] },
122+
{ Name: 'tag:ghr:Application', Values: ['github-action-runner'] },
94123
],
95124
});
96125
});
97126

98127
it('filters instances on environment', async () => {
128+
mockDescribeInstances.promise
129+
.mockReturnValueOnce(mockRunningInstances)
130+
.mockReturnValueOnce(mockRunningInstancesLegacy);
99131
await listEC2Runners({ environment: ENVIRONMENT });
100132
expect(mockEC2.describeInstances).toBeCalledWith({
101133
Filters: [
102-
{ Name: 'tag:Application', Values: ['github-action-runner'] },
103134
{ Name: 'instance-state-name', Values: ['running', 'pending'] },
104135
{ Name: 'tag:ghr:environment', Values: [ENVIRONMENT] },
136+
{ Name: 'tag:ghr:Application', Values: ['github-action-runner'] },
105137
],
106138
});
107139
});
@@ -123,7 +155,7 @@ describe('list instances', () => {
123155
},
124156
],
125157
};
126-
mockDescribeInstances.promise.mockReturnValue(noInstances);
158+
mockDescribeInstances.promise.mockReturnValueOnce(noInstances).mockReturnValueOnce(noInstances);
127159
const resp = await listEC2Runners();
128160
expect(resp.length).toBe(0);
129161
});
@@ -142,7 +174,7 @@ describe('list instances', () => {
142174
},
143175
],
144176
};
145-
mockDescribeInstances.promise.mockReturnValue(noInstances);
177+
mockDescribeInstances.promise.mockReturnValueOnce(noInstances).mockReturnValue({});
146178
const resp = await listEC2Runners();
147179
expect(resp.length).toBe(1);
148180
});
@@ -459,7 +491,7 @@ function expectedCreateFleetRequest(expectedValues: ExpectedFleetRequestValues):
459491
{
460492
ResourceType: 'instance',
461493
Tags: [
462-
{ Key: 'Application', Value: 'github-action-runner' },
494+
{ Key: 'ghr:Application', Value: 'github-action-runner' },
463495
{ Key: 'Type', Value: expectedValues.type },
464496
{ Key: 'Owner', Value: REPO_NAME },
465497
],

Diff for: modules/runners/lambdas/runners/src/aws/runners.ts

+33-10
Original file line numberDiff line numberDiff line change
@@ -45,24 +45,47 @@ export interface RunnerInputParameters {
4545
amiIdSsmParameterName?: string;
4646
}
4747

48+
interface Ec2Filter {
49+
Name: string;
50+
Values: string[];
51+
}
52+
4853
export async function listEC2Runners(filters: ListRunnerFilters | undefined = undefined): Promise<RunnerList[]> {
49-
const ec2Statuses = filters?.statuses ? filters.statuses : ['running', 'pending'];
50-
const ec2 = new EC2();
51-
const ec2Filters = [
52-
{ Name: 'tag:Application', Values: ['github-action-runner'] },
53-
{ Name: 'instance-state-name', Values: ec2Statuses },
54-
];
54+
const ec2Filters = constructFilters(filters);
55+
const runners: RunnerList[] = [];
56+
for (const filter of ec2Filters) {
57+
runners.push(...(await getRunners(filter)));
58+
}
59+
return runners;
60+
}
5561

62+
function constructFilters(filters?: ListRunnerFilters): Ec2Filter[][] {
63+
const ec2Statuses = filters?.statuses ? filters.statuses : ['running', 'pending'];
64+
const ec2Filters: Ec2Filter[][] = [];
65+
const ec2FiltersBase = [{ Name: 'instance-state-name', Values: ec2Statuses }];
5666
if (filters) {
5767
if (filters.environment !== undefined) {
58-
ec2Filters.push({ Name: 'tag:ghr:environment', Values: [filters.environment] });
68+
ec2FiltersBase.push({ Name: 'tag:ghr:environment', Values: [filters.environment] });
5969
}
6070
if (filters.runnerType && filters.runnerOwner) {
61-
ec2Filters.push({ Name: `tag:Type`, Values: [filters.runnerType] });
62-
ec2Filters.push({ Name: `tag:Owner`, Values: [filters.runnerOwner] });
71+
ec2FiltersBase.push({ Name: `tag:Type`, Values: [filters.runnerType] });
72+
ec2FiltersBase.push({ Name: `tag:Owner`, Values: [filters.runnerOwner] });
6373
}
6474
}
6575

76+
// ***Deprecation Notice***
77+
// Support for legacy `Application` tag keys
78+
// will be removed in next major release.
79+
for (const key of ['tag:ghr:Application', 'tag:Application']) {
80+
const filter = [...ec2FiltersBase];
81+
filter.push({ Name: key, Values: ['github-action-runner'] });
82+
ec2Filters.push(filter);
83+
}
84+
return ec2Filters;
85+
}
86+
87+
async function getRunners(ec2Filters: Ec2Filter[]): Promise<RunnerList[]> {
88+
const ec2 = new EC2();
6689
const runners: RunnerList[] = [];
6790
let nextToken;
6891
let hasNext = true;
@@ -191,7 +214,7 @@ export async function createRunner(runnerParameters: RunnerInputParameters): Pro
191214
{
192215
ResourceType: 'instance',
193216
Tags: [
194-
{ Key: 'Application', Value: 'github-action-runner' },
217+
{ Key: 'ghr:Application', Value: 'github-action-runner' },
195218
{ Key: 'Type', Value: runnerParameters.runnerType },
196219
{ Key: 'Owner', Value: runnerParameters.runnerOwner },
197220
],

Diff for: modules/runners/policies/lambda-scale-down.json

+58-44
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,62 @@
11
{
2-
"Version": "2012-10-17",
3-
"Statement": [
4-
{
5-
"Effect": "Allow",
6-
"Action": [
7-
"ec2:DescribeInstances",
8-
"ec2:DescribeTags"
9-
],
10-
"Resource": [
11-
"*"
12-
]
13-
},
14-
{
15-
"Effect": "Allow",
16-
"Action": [
17-
"ec2:TerminateInstances"
18-
],
19-
"Resource": [
20-
"*"
21-
],
22-
"Condition": {
23-
"StringEquals": {
24-
"ec2:ResourceTag/Application": "github-action-runner"
25-
}
26-
}
27-
},
28-
{
29-
"Effect": "Allow",
30-
"Action": [
31-
"ssm:GetParameter"
32-
],
33-
"Resource": [
34-
"${github_app_key_base64_arn}",
35-
"${github_app_id_arn}"
36-
]
2+
"Version": "2012-10-17",
3+
"Statement": [
4+
{
5+
"Effect": "Allow",
6+
"Action": [
7+
"ec2:DescribeInstances",
8+
"ec2:DescribeTags"
9+
],
10+
"Resource": [
11+
"*"
12+
]
13+
},
14+
{
15+
"Effect": "Allow",
16+
"Action": [
17+
"ec2:TerminateInstances"
18+
],
19+
"Resource": [
20+
"*"
21+
],
22+
"Condition": {
23+
"StringEquals": {
24+
"ec2:ResourceTag/ghr:Application": "github-action-runner"
25+
}
26+
}
27+
},
28+
{
29+
"Effect": "Allow",
30+
"Action": [
31+
"ec2:TerminateInstances"
32+
],
33+
"Resource": [
34+
"*"
35+
],
36+
"Condition": {
37+
"StringEquals": {
38+
"ec2:ResourceTag/Application": "github-action-runner"
39+
}
40+
}
41+
},
42+
{
43+
"Effect": "Allow",
44+
"Action": [
45+
"ssm:GetParameter"
46+
],
47+
"Resource": [
48+
"${github_app_key_base64_arn}",
49+
"${github_app_id_arn}"
50+
]
3751
%{ if kms_key_arn != "" ~}
38-
},
39-
{
40-
"Effect": "Allow",
41-
"Action": [
42-
"kms:Decrypt"
43-
],
44-
"Resource": "${kms_key_arn}"
52+
},
53+
{
54+
"Effect": "Allow",
55+
"Action": [
56+
"kms:Decrypt"
57+
],
58+
"Resource": "${kms_key_arn}"
4559
%{ endif ~}
46-
}
47-
]
60+
}
61+
]
4862
}

0 commit comments

Comments
 (0)