Skip to content

Commit 5ff1a05

Browse files
gertjanmaassjagoe
authored andcommitted
feat: fetch prerelease binaries (#165)
* Update changelog * Fetch latest runner pre-release to avoid runner self-update on start * Update lambda to support prerelease binaries * Add variable to allow prerelease binaries to terraform * Update docs * Update changelog * Fix review comments * Added PR number and contributor to changelog Co-authored-by: Simon Jagoe <[email protected]>
1 parent f7792d1 commit 5ff1a05

22 files changed

+1430
-499
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- feat: Manage log groups via module. When upgrading you have to import the log groups by AWS into your state. See below the example commands for the default example.
13+
1314
```bash
1415
terraform import module.runners.module.runner_binaries.aws_cloudwatch_log_group.syncer "/aws/lambda/default-syncer"
1516
terraform import module.runners.module.runners.aws_cloudwatch_log_group.scale_up "/aws/lambda/default-scale-up"
1617
terraform import module.runners.module.runners.aws_cloudwatch_log_group.scale_down "/aws/lambda/default-scale-down"
1718
terraform import module.runners.module.webhook.aws_cloudwatch_log_group.webhook "/aws/lambda/default-webhook"
1819
```
1920

21+
- feat: Added option to binaries syncer to upgrade to prereleases, preventing any auto-updating on startup. Option `runner_allow_prerelease_binaries` is disabled by default. (#141, #165) @sjagoe
22+
2023
## [0.4.0] - 2020-08-10
2124

2225
### Added

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ No requirements.
301301
| minimum\_running\_time\_in\_minutes | The time an ec2 action runner should be running at minimum before terminated if non busy. | `number` | `5` | no |
302302
| role\_path | The path that will be added to role path for created roles, if not set the environment name will be used. | `string` | `null` | no |
303303
| role\_permissions\_boundary | Permissions boundary that will be added to the created roles. | `string` | `null` | no |
304+
| runner\_allow\_prerelease\_binaries | Allow the runners to update to prerelease binaries. | `bool` | `false` | no |
304305
| runner\_as\_root | Run the action runner under the root user. | `bool` | `false` | no |
305306
| runner\_binaries\_syncer\_lambda\_timeout | Time out of the binaries sync lambda in seconds. | `number` | `300` | no |
306307
| runner\_binaries\_syncer\_lambda\_zip | File location of the binaries sync lambda zip file. | `string` | `null` | no |

main.tf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ module "runner_binaries" {
100100

101101
distribution_bucket_name = "${var.environment}-dist-${random_string.random.result}"
102102

103-
runner_architecture = substr(var.instance_type, 0, 2) == "a1" || substr(var.instance_type, 1, 2) == "6g" ? "arm64" : "x64"
103+
runner_architecture = substr(var.instance_type, 0, 2) == "a1" || substr(var.instance_type, 1, 2) == "6g" ? "arm64" : "x64"
104+
runner_allow_prerelease_binaries = var.runner_allow_prerelease_binaries
104105

105106
lambda_zip = var.runner_binaries_syncer_lambda_zip
106107
lambda_timeout = var.runner_binaries_syncer_lambda_timeout

modules/download-lambda/README.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,27 @@ module "lambdas" {
2525
```
2626

2727
<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
28+
## Requirements
29+
30+
No requirements.
31+
32+
## Providers
33+
34+
| Name | Version |
35+
|------|---------|
36+
| null | n/a |
2837

2938
## Inputs
3039

31-
| Name | Description | Type | Default | Required |
32-
| ------- | ------------------------------------- | :----: | :-----: | :------: |
33-
| lambdas | Name and tag for lambdas to download. | object | n/a | yes |
40+
| Name | Description | Type | Default | Required |
41+
|------|-------------|------|---------|:--------:|
42+
| lambdas | Name and tag for lambdas to download. | <pre>list(object({<br> name = string<br> tag = string<br> }))</pre> | n/a | yes |
3443

3544
## Outputs
3645

37-
| Name | Description |
38-
| ----- | ----------- |
39-
| files | |
46+
| Name | Description |
47+
|------|-------------|
48+
| files | n/a |
4049

4150
<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
4251

modules/runner-binaries-syncer/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ No requirements.
5757
| logging\_retention\_in\_days | Specifies the number of days you want to retain log events for the lambda log group. Possible values are: 0, 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, and 3653. | `number` | `7` | no |
5858
| role\_path | The path that will be added to the role, if not set the environment name will be used. | `string` | `null` | no |
5959
| role\_permissions\_boundary | Permissions boundary that will be added to the created role for the lambda. | `string` | `null` | no |
60+
| runner\_allow\_prerelease\_binaries | Allow the runners to update to prerelease binaries. | `bool` | `false` | no |
6061
| runner\_architecture | The platform architecture for the runner instance (x64, arm64), defaults to 'x64' | `string` | `"x64"` | no |
6162
| tags | Map of tags that will be added to created resources. By default resources will be tagged with name and environment. | `map(string)` | `{}` | no |
6263

modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,8 @@
2626
"ts-jest": "^26.2.0",
2727
"ts-node-dev": "^1.0.0-pre.60",
2828
"typescript": "^3.9.6"
29+
},
30+
"dependencies": {
31+
"yn": "^4.0.0"
2932
}
3033
}

modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/src/lambda.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { handle } from './syncer/handler';
22

3+
// eslint-disable-next-line
34
module.exports.handler = async (event: any, context: any, callback: any): Promise<any> => {
45
await handle();
56
return callback();

modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/src/syncer/handler.test.ts

Lines changed: 103 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { handle } from './handler';
2-
import latestReleases from '../../test/resources/github-latest-releases.json';
3-
import latestReleasesEmpty from '../../test/resources/github-latest-releases-empty.json';
4-
import latestReleasesNoLinux from '../../test/resources/github-latest-releases-no-linux.json';
5-
import latestReleasesNoArm64 from '../../test/resources/github-latest-releases-no-arm64.json';
2+
import listReleases from '../../test/resources/github-list-releases.json';
3+
import listReleasesEmpty from '../../test/resources/github-list-releases-empty-assets.json';
4+
import listReleasesNoLinux from '../../test/resources/github-list-releases-no-linux.json';
5+
import listReleasesNoArm64 from '../../test/resources/github-list-releases-no-arm64.json';
66

77
const mockOctokit = {
88
repos: {
9-
getLatestRelease: jest.fn(),
9+
listReleases: jest.fn(),
1010
},
1111
};
1212
jest.mock('@octokit/rest', () => ({
@@ -31,31 +31,51 @@ describe('Synchronize action distribution.', () => {
3131
beforeEach(() => {
3232
process.env.S3_BUCKET_NAME = bucketName;
3333
process.env.S3_OBJECT_KEY = bucketObjectKey;
34+
process.env.GITHUB_RUNNER_ALLOW_PRERELEASE_BINARIES = 'false';
3435

35-
mockOctokit.repos.getLatestRelease.mockImplementation(() => ({
36-
data: latestReleases.data,
36+
mockOctokit.repos.listReleases.mockImplementation(() => ({
37+
data: listReleases,
3738
}));
3839
});
3940

40-
it('Distribution is up-to-date.', async () => {
41+
it('Distribution is up-to-date with latest release.', async () => {
4142
mockS3.getObjectTagging.mockImplementation(() => {
4243
return {
4344
promise() {
44-
return Promise.resolve({ TagSet: [{ Key: 'name', Value: 'actions-runner-linux-x64-2.262.1.tar.gz' }] });
45+
return Promise.resolve({ TagSet: [{ Key: 'name', Value: 'actions-runner-linux-x64-2.272.0.tar.gz' }] });
4546
},
4647
};
4748
});
4849

4950
await handle();
50-
expect(mockOctokit.repos.getLatestRelease).toBeCalledTimes(1);
51+
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
5152
expect(mockS3.getObjectTagging).toBeCalledWith({
5253
Bucket: bucketName,
5354
Key: bucketObjectKey,
5455
});
5556
expect(mockS3.upload).toBeCalledTimes(0);
5657
});
5758

58-
it('Distribution should update.', async () => {
59+
it('Distribution is up-to-date with latest prerelease.', async () => {
60+
process.env.GITHUB_RUNNER_ALLOW_PRERELEASE_BINARIES = 'true';
61+
mockS3.getObjectTagging.mockImplementation(() => {
62+
return {
63+
promise() {
64+
return Promise.resolve({ TagSet: [{ Key: 'name', Value: 'actions-runner-linux-x64-2.273.0.tar.gz' }] });
65+
},
66+
};
67+
});
68+
69+
await handle();
70+
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
71+
expect(mockS3.getObjectTagging).toBeCalledWith({
72+
Bucket: bucketName,
73+
Key: bucketObjectKey,
74+
});
75+
expect(mockS3.upload).toBeCalledTimes(0);
76+
});
77+
78+
it('Distribution should update to release.', async () => {
5979
mockS3.getObjectTagging.mockImplementation(() => {
6080
return {
6181
promise() {
@@ -65,12 +85,63 @@ describe('Synchronize action distribution.', () => {
6585
});
6686

6787
await handle();
68-
expect(mockOctokit.repos.getLatestRelease).toBeCalledTimes(1);
88+
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
6989
expect(mockS3.getObjectTagging).toBeCalledWith({
7090
Bucket: bucketName,
7191
Key: bucketObjectKey,
7292
});
7393
expect(mockS3.upload).toBeCalledTimes(1);
94+
const s3JsonBody = mockS3.upload.mock.calls[0][0];
95+
expect(s3JsonBody['Tagging']).toEqual('name=actions-runner-linux-x64-2.272.0.tar.gz');
96+
});
97+
98+
it('Distribution should update to prerelease.', async () => {
99+
process.env.GITHUB_RUNNER_ALLOW_PRERELEASE_BINARIES = 'true';
100+
mockS3.getObjectTagging.mockImplementation(() => {
101+
return {
102+
promise() {
103+
return Promise.resolve({ TagSet: [{ Key: 'name', Value: 'actions-runner-linux-x64-0.tar.gz' }] });
104+
},
105+
};
106+
});
107+
108+
await handle();
109+
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
110+
expect(mockS3.getObjectTagging).toBeCalledWith({
111+
Bucket: bucketName,
112+
Key: bucketObjectKey,
113+
});
114+
expect(mockS3.upload).toBeCalledTimes(1);
115+
const s3JsonBody = mockS3.upload.mock.calls[0][0];
116+
expect(s3JsonBody['Tagging']).toEqual('name=actions-runner-linux-x64-2.273.0.tar.gz');
117+
});
118+
119+
it('Distribution should not update to prerelease if there is a newer release.', async () => {
120+
process.env.GITHUB_RUNNER_ALLOW_PRERELEASE_BINARIES = 'true';
121+
const releases = listReleases;
122+
releases[0].prerelease = false;
123+
releases[1].prerelease = true;
124+
125+
mockOctokit.repos.listReleases.mockImplementation(() => ({
126+
data: releases,
127+
}));
128+
mockS3.getObjectTagging.mockImplementation(() => {
129+
return {
130+
promise() {
131+
return Promise.resolve({ TagSet: [{ Key: 'name', Value: 'actions-runner-linux-x64-0.tar.gz' }] });
132+
},
133+
};
134+
});
135+
136+
await handle();
137+
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
138+
expect(mockS3.getObjectTagging).toBeCalledWith({
139+
Bucket: bucketName,
140+
Key: bucketObjectKey,
141+
});
142+
expect(mockS3.upload).toBeCalledTimes(1);
143+
const s3JsonBody = mockS3.upload.mock.calls[0][0];
144+
expect(s3JsonBody['Tagging']).toEqual('name=actions-runner-linux-x64-2.273.0.tar.gz');
74145
});
75146

76147
it('No tag in S3, distribution should update.', async () => {
@@ -83,7 +154,7 @@ describe('Synchronize action distribution.', () => {
83154
});
84155

85156
await handle();
86-
expect(mockOctokit.repos.getLatestRelease).toBeCalledTimes(1);
157+
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
87158
expect(mockS3.getObjectTagging).toBeCalledWith({
88159
Bucket: bucketName,
89160
Key: bucketObjectKey,
@@ -101,7 +172,7 @@ describe('Synchronize action distribution.', () => {
101172
});
102173

103174
await handle();
104-
expect(mockOctokit.repos.getLatestRelease).toBeCalledTimes(1);
175+
expect(mockOctokit.repos.listReleases).toBeCalledTimes(1);
105176
expect(mockS3.getObjectTagging).toBeCalledWith({
106177
Bucket: bucketName,
107178
Key: bucketObjectKey,
@@ -118,16 +189,16 @@ describe('No release assets found.', () => {
118189
});
119190

120191
it('Empty list of assets.', async () => {
121-
mockOctokit.repos.getLatestRelease.mockImplementation(() => ({
122-
data: latestReleasesEmpty.data,
192+
mockOctokit.repos.listReleases.mockImplementation(() => ({
193+
data: listReleasesEmpty,
123194
}));
124195

125196
await expect(handle()).rejects.toThrow(errorMessage);
126197
});
127198

128199
it('No linux x64 asset.', async () => {
129-
mockOctokit.repos.getLatestRelease.mockImplementation(() => ({
130-
data: latestReleasesNoLinux.data,
200+
mockOctokit.repos.listReleases.mockImplementation(() => ({
201+
data: [listReleasesNoLinux],
131202
}));
132203

133204
await expect(handle()).rejects.toThrow(errorMessage);
@@ -154,18 +225,18 @@ describe('Invalid config', () => {
154225
});
155226

156227
describe('Synchronize action distribution for arm64.', () => {
157-
const errorMessage = 'Cannot find GitHub release asset.';
158-
beforeEach(() => {
159-
process.env.S3_BUCKET_NAME = bucketName;
160-
process.env.S3_OBJECT_KEY = bucketObjectKey;
161-
process.env.GITHUB_RUNNER_ARCHITECTURE = 'arm64';
162-
});
163-
164-
it('No linux arm64 asset.', async () => {
165-
mockOctokit.repos.getLatestRelease.mockImplementation(() => ({
166-
data: latestReleasesNoArm64.data,
167-
}));
168-
169-
await expect(handle()).rejects.toThrow(errorMessage);
170-
});
228+
const errorMessage = 'Cannot find GitHub release asset.';
229+
beforeEach(() => {
230+
process.env.S3_BUCKET_NAME = bucketName;
231+
process.env.S3_OBJECT_KEY = bucketObjectKey;
232+
process.env.GITHUB_RUNNER_ARCHITECTURE = 'arm64';
233+
});
234+
235+
it('No linux arm64 asset.', async () => {
236+
mockOctokit.repos.listReleases.mockImplementation(() => ({
237+
data: [listReleasesNoArm64],
238+
}));
239+
240+
await expect(handle()).rejects.toThrow(errorMessage);
241+
});
171242
});

modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/src/syncer/handler.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { PassThrough } from 'stream';
33
import request from 'request';
44
import { S3 } from 'aws-sdk';
55
import AWS from 'aws-sdk';
6+
import yn from 'yn';
67

78
const versionKey = 'name';
89

@@ -31,13 +32,31 @@ interface ReleaseAsset {
3132
downloadUrl: string;
3233
}
3334

34-
async function getLinuxReleaseAsset(runnerArch = 'x64'): Promise<ReleaseAsset | undefined> {
35+
async function getLinuxReleaseAsset(
36+
runnerArch = 'x64',
37+
fetchPrereleaseBinaries = false,
38+
): Promise<ReleaseAsset | undefined> {
3539
const githubClient = new Octokit();
36-
const assets = await githubClient.repos.getLatestRelease({
40+
const assetsList = await githubClient.repos.listReleases({
3741
owner: 'actions',
3842
repo: 'runner',
3943
});
40-
const linuxAssets = assets.data.assets?.filter((a) => a.name?.includes(`actions-runner-linux-${runnerArch}-`));
44+
if (assetsList.data?.length === 0) {
45+
return undefined;
46+
}
47+
48+
const latestPrereleaseIndex = assetsList.data.findIndex((a) => a.prerelease === true);
49+
const latestReleaseIndex = assetsList.data.findIndex((a) => a.prerelease === false);
50+
51+
let asset = undefined;
52+
if (fetchPrereleaseBinaries && latestPrereleaseIndex < latestReleaseIndex) {
53+
asset = assetsList.data[latestPrereleaseIndex];
54+
} else if (latestReleaseIndex != -1) {
55+
asset = assetsList.data[latestReleaseIndex];
56+
} else {
57+
return undefined;
58+
}
59+
const linuxAssets = asset.assets?.filter((a) => a.name?.includes(`actions-runner-linux-${runnerArch}-`));
4160

4261
return linuxAssets?.length === 1
4362
? { name: linuxAssets[0].name, downloadUrl: linuxAssets[0].browser_download_url }
@@ -73,7 +92,8 @@ async function uploadToS3(s3: S3, cacheObject: CacheObject, actionRunnerReleaseA
7392
export const handle = async (): Promise<void> => {
7493
const s3 = new AWS.S3();
7594

76-
const runnerArch = process.env.GITHUB_RUNNER_ARCHITECTURE || 'x64'
95+
const runnerArch = process.env.GITHUB_RUNNER_ARCHITECTURE || 'x64';
96+
const fetchPrereleaseBinaries = yn(process.env.GITHUB_RUNNER_ALLOW_PRERELEASE_BINARIES, { default: false });
7797

7898
const cacheObject: CacheObject = {
7999
bucket: process.env.S3_BUCKET_NAME as string,
@@ -83,7 +103,7 @@ export const handle = async (): Promise<void> => {
83103
throw Error('Please check all mandatory variables are set.');
84104
}
85105

86-
const actionRunnerReleaseAsset = await getLinuxReleaseAsset(runnerArch);
106+
const actionRunnerReleaseAsset = await getLinuxReleaseAsset(runnerArch, fetchPrereleaseBinaries);
87107
if (actionRunnerReleaseAsset === undefined) {
88108
throw Error('Cannot find GitHub release asset.');
89109
}

modules/runner-binaries-syncer/lambdas/runner-binaries-syncer/test/resources/github-latest-releases-empty.json

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)