Skip to content

Commit 2dfca37

Browse files
committed
append nodes to builder support
Signed-off-by: CrazyMax <[email protected]>
1 parent 95cb08c commit 2dfca37

File tree

12 files changed

+249
-14
lines changed

12 files changed

+249
-14
lines changed

.github/workflows/ci.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,13 @@ jobs:
305305
platforms: ${{ matrix.qemu-platforms }}
306306
-
307307
name: Set up Docker Buildx
308+
id: buildx
308309
uses: ./
309310
with:
310311
version: ${{ matrix.buildx-version }}
312+
-
313+
name: List builder platforms
314+
run: echo ${{ steps.buildx.outputs.platforms }}
311315

312316
build-ref:
313317
runs-on: ubuntu-latest
@@ -416,3 +420,32 @@ jobs:
416420
echo "::error::Should have failed"
417421
exit 1
418422
fi
423+
424+
append:
425+
runs-on: ubuntu-latest
426+
steps:
427+
-
428+
name: Checkout
429+
uses: actions/checkout@v3
430+
-
431+
name: Create dummy contexts
432+
run: |
433+
docker context create ctxbuilder2
434+
docker context create ctxbuilder3
435+
-
436+
name: Set up Docker Buildx
437+
id: buildx
438+
uses: ./
439+
with:
440+
append: |
441+
- name: builder2
442+
endpoint: ctxbuilder2
443+
platforms: linux/amd64
444+
driver-opts:
445+
- image=moby/buildkit:master
446+
- network=host
447+
- endpoint: ctxbuilder3
448+
platforms: linux/arm64
449+
-
450+
name: List builder platforms
451+
run: echo ${{ steps.buildx.outputs.platforms }}

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ ___
2121
* [Usage](#usage)
2222
* [Advanced usage](#advanced-usage)
2323
* [Authentication support](docs/advanced/auth.md)
24+
* [Append additional nodes to the builder](docs/advanced/append-nodes.md)
2425
* [Install by default](docs/advanced/install-default.md)
2526
* [BuildKit daemon configuration](docs/advanced/buildkit-config.md)
2627
* [Standalone mode](docs/advanced/standalone.md)
@@ -61,6 +62,7 @@ jobs:
6162
## Advanced usage
6263
6364
* [Authentication support](docs/advanced/auth.md)
65+
* [Append additional nodes to the builder](docs/advanced/append-nodes.md)
6466
* [Install by default](docs/advanced/install-default.md)
6567
* [BuildKit daemon configuration](docs/advanced/buildkit-config.md)
6668
* [Standalone mode](docs/advanced/standalone.md)
@@ -82,6 +84,7 @@ Following inputs can be used as `step.with` keys
8284
| `endpoint` | String | [Optional address for docker socket](https://docs.docker.com/engine/reference/commandline/buildx_create/#description) or context from `docker context ls` |
8385
| `config`¹ | String | [BuildKit config file](https://docs.docker.com/engine/reference/commandline/buildx_create/#config) |
8486
| `config-inline`¹ | String | Same as `config` but inline |
87+
| `append` | YAML | [Append additional nodes](docs/advanced/append-nodes.md) to the builder |
8588

8689
> * ¹ `config` and `config-inline` are mutually exclusive
8790

__tests__/context.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as os from 'os';
44
import * as path from 'path';
55
import * as uuid from 'uuid';
66
import * as context from '../src/context';
7+
import * as nodes from '../src/nodes';
78

89
const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-setup-buildx-')).split(path.sep).join(path.posix.sep);
910
jest.spyOn(context, 'tmpDir').mockImplementation((): string => {
@@ -103,6 +104,57 @@ describe('getCreateArgs', () => {
103104
);
104105
});
105106

107+
describe('getAppendArgs', () => {
108+
beforeEach(() => {
109+
process.env = Object.keys(process.env).reduce((object, key) => {
110+
if (!key.startsWith('INPUT_')) {
111+
object[key] = process.env[key];
112+
}
113+
return object;
114+
}, {});
115+
});
116+
117+
// prettier-ignore
118+
test.each([
119+
[
120+
0,
121+
new Map<string, string>([
122+
['install', 'false'],
123+
['use', 'true'],
124+
]),
125+
{
126+
"name": "aws_graviton2",
127+
"endpoint": "ssh://me@graviton2",
128+
"driver-opts": [
129+
"image=moby/buildkit:latest"
130+
],
131+
"buildkitd-flags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host",
132+
"platforms": "linux/arm64"
133+
},
134+
[
135+
'create',
136+
'--name', 'builder-9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d',
137+
'--append',
138+
'--node', 'aws_graviton2',
139+
'--driver-opt', 'image=moby/buildkit:latest',
140+
'--buildkitd-flags', '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host',
141+
'--platform', 'linux/arm64',
142+
'ssh://me@graviton2'
143+
]
144+
]
145+
])(
146+
'[%d] given %p as inputs, returns %p',
147+
async (num: number, inputs: Map<string, string>, node: nodes.Node, expected: Array<string>) => {
148+
inputs.forEach((value: string, name: string) => {
149+
setInput(name, value);
150+
});
151+
const inp = await context.getInputs();
152+
const res = await context.getAppendArgs(inp, node, '0.9.0');
153+
expect(res).toEqual(expected);
154+
}
155+
);
156+
});
157+
106158
describe('getInputList', () => {
107159
it('handles single line correctly', async () => {
108160
await setInput('foo', 'bar');

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ inputs:
3838
config-inline:
3939
description: 'Inline BuildKit config'
4040
required: false
41+
append:
42+
description: 'Append additional nodes to the builder'
43+
required: false
4144

4245
outputs:
4346
name:

docs/advanced/append-nodes.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Append additional nodes to the builder
2+
3+
Buildx also supports running builds on multiple machines. This is useful for
4+
building [multi-platform images](https://docs.docker.com/build/building/multi-platform/)
5+
on native nodes for more complicated cases that are not handled by QEMU and
6+
generally have better performance or for distributing the build across multiple
7+
machines.
8+
9+
You can append nodes to the builder that is going to be created with the
10+
`append` input in the form of a YAML string document to remove limitations
11+
intrinsically linked to GitHub Actions (only string format is handled in the
12+
input fields):
13+
14+
| Name | Type | Description |
15+
|-------------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
16+
| `name` | String | [Name of the node](https://docs.docker.com/engine/reference/commandline/buildx_create/#node). If empty, it is the name of the builder it belongs to, with an index number suffix. This is useful to set it if you want to modify/remove a node in an underlying step of you workflow. |
17+
| `endpoint` | String | [Docker context or endpoint](https://docs.docker.com/engine/reference/commandline/buildx_create/#description) of the node to add to the builder |
18+
| `driver-opts` | List | List of additional [driver-specific options](https://docs.docker.com/engine/reference/commandline/buildx_create/#driver-opt) |
19+
| `buildkitd-flags` | String | [Flags for buildkitd](https://docs.docker.com/engine/reference/commandline/buildx_create/#buildkitd-flags) daemon |
20+
| `platforms` | String | Fixed [platforms](https://docs.docker.com/engine/reference/commandline/buildx_create/#platform) for the node. If not empty, values take priority over the detected ones. |
21+
22+
Here is an example using remote nodes with the [`remote` driver](https://docs.docker.com/build/building/drivers/remote/)
23+
and [TLS authentication](auth.md#tls-authentication):
24+
25+
```yaml
26+
name: ci
27+
28+
on:
29+
push:
30+
31+
jobs:
32+
buildx:
33+
runs-on: ubuntu-latest
34+
steps:
35+
-
36+
name: Set up Docker Buildx
37+
uses: docker/setup-buildx-action@v2
38+
with:
39+
driver: remote
40+
endpoint: tcp://oneprovider:1234
41+
append: |
42+
- endpoint: tcp://graviton2:1234
43+
platforms: linux/arm64
44+
- endpoint: tcp://linuxone:1234
45+
platforms: linux/s390x
46+
env:
47+
BUILDER_NODE_0_AUTH_TLS_CACERT: ${{ secrets.ONEPROVIDER_CA }}
48+
BUILDER_NODE_0_AUTH_TLS_CERT: ${{ secrets.ONEPROVIDER_CERT }}
49+
BUILDER_NODE_0_AUTH_TLS_KEY: ${{ secrets.ONEPROVIDER_KEY }}
50+
BUILDER_NODE_1_AUTH_TLS_CACERT: ${{ secrets.GRAVITON2_CA }}
51+
BUILDER_NODE_1_AUTH_TLS_CERT: ${{ secrets.GRAVITON2_CERT }}
52+
BUILDER_NODE_1_AUTH_TLS_KEY: ${{ secrets.GRAVITON2_KEY }}
53+
BUILDER_NODE_2_AUTH_TLS_CACERT: ${{ secrets.LINUXONE_CA }}
54+
BUILDER_NODE_2_AUTH_TLS_CERT: ${{ secrets.LINUXONE_CERT }}
55+
BUILDER_NODE_2_AUTH_TLS_KEY: ${{ secrets.LINUXONE_KEY }}
56+
```

docs/advanced/auth.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,6 @@ the node in the list of nodes:
4141
* `BUILDER_NODE_<idx>_AUTH_TLS_CERT`
4242
* `BUILDER_NODE_<idx>_AUTH_TLS_KEY`
4343

44-
> **Note**
45-
>
46-
> The index is always `0` at the moment as we don't support (yet) appending new
47-
> nodes with this action.
48-
4944
```yaml
5045
name: ci
5146

jest.config.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
module.exports = {
22
clearMocks: true,
33
moduleFileExtensions: ['js', 'ts'],
4-
setupFiles: ["dotenv/config"],
4+
setupFiles: ['dotenv/config'],
55
testMatch: ['**/*.test.ts'],
66
transform: {
77
'^.+\\.ts$': 'ts-jest'
88
},
9+
moduleNameMapper: {
10+
'^csv-parse/sync': '<rootDir>/node_modules/csv-parse/dist/cjs/sync.cjs'
11+
},
912
verbose: true
10-
}
13+
};

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
"@actions/exec": "^1.1.1",
3232
"@actions/http-client": "^2.0.1",
3333
"@actions/tool-cache": "^2.0.1",
34+
"csv-parse": "^5.1.0",
35+
"js-yaml": "^4.1.0",
3436
"semver": "^7.3.7",
3537
"tmp": "^0.2.1",
3638
"uuid": "^9.0.0"

src/context.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import * as os from 'os';
33
import path from 'path';
44
import * as tmp from 'tmp';
55
import * as uuid from 'uuid';
6+
import {parse} from 'csv-parse/sync';
67
import * as buildx from './buildx';
8+
import * as nodes from './nodes';
79
import * as core from '@actions/core';
810

911
let _tmpDir: string;
@@ -32,6 +34,7 @@ export interface Inputs {
3234
endpoint: string;
3335
config: string;
3436
configInline: string;
37+
append: string;
3538
}
3639

3740
export async function getInputs(): Promise<Inputs> {
@@ -45,7 +48,8 @@ export async function getInputs(): Promise<Inputs> {
4548
use: core.getBooleanInput('use'),
4649
endpoint: core.getInput('endpoint'),
4750
config: core.getInput('config'),
48-
configInline: core.getInput('config-inline')
51+
configInline: core.getInput('config-inline'),
52+
append: core.getInput('append')
4953
};
5054
}
5155

@@ -79,6 +83,28 @@ export async function getCreateArgs(inputs: Inputs, buildxVersion: string): Prom
7983
return args;
8084
}
8185

86+
export async function getAppendArgs(inputs: Inputs, node: nodes.Node, buildxVersion: string): Promise<Array<string>> {
87+
const args: Array<string> = ['create', '--name', inputs.name, '--append'];
88+
if (node.name) {
89+
args.push('--node', node.name);
90+
}
91+
if (node['driver-opts'] && buildx.satisfies(buildxVersion, '>=0.3.0')) {
92+
await asyncForEach(node['driver-opts'], async driverOpt => {
93+
args.push('--driver-opt', driverOpt);
94+
});
95+
if (inputs.driver != 'remote' && node['buildkitd-flags']) {
96+
args.push('--buildkitd-flags', node['buildkitd-flags']);
97+
}
98+
}
99+
if (node.platforms) {
100+
args.push('--platform', node.platforms);
101+
}
102+
if (node.endpoint) {
103+
args.push(node.endpoint);
104+
}
105+
return args;
106+
}
107+
82108
export async function getInspectArgs(inputs: Inputs, buildxVersion: string): Promise<Array<string>> {
83109
const args: Array<string> = ['inspect', '--bootstrap'];
84110
if (buildx.satisfies(buildxVersion, '>=0.4.0')) {
@@ -88,14 +114,33 @@ export async function getInspectArgs(inputs: Inputs, buildxVersion: string): Pro
88114
}
89115

90116
export async function getInputList(name: string, ignoreComma?: boolean): Promise<string[]> {
117+
const res: Array<string> = [];
118+
91119
const items = core.getInput(name);
92120
if (items == '') {
93-
return [];
121+
return res;
122+
}
123+
124+
const records = parse(items, {
125+
columns: false,
126+
relaxQuotes: true,
127+
comment: '#',
128+
relaxColumnCount: true,
129+
skipEmptyLines: true
130+
});
131+
132+
for (const record of records as Array<string[]>) {
133+
if (record.length == 1) {
134+
res.push(record[0]);
135+
continue;
136+
} else if (!ignoreComma) {
137+
res.push(...record);
138+
continue;
139+
}
140+
res.push(record.join(','));
94141
}
95-
return items
96-
.split(/\r?\n/)
97-
.filter(x => x)
98-
.reduce<string[]>((acc, line) => acc.concat(!ignoreComma ? line.split(',').filter(x => x) : line).map(pat => pat.trim()), []);
142+
143+
return res.filter(item => item).map(pat => pat.trim());
99144
}
100145

101146
export const asyncForEach = async (array, callback) => {

src/main.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as auth from './auth';
55
import * as buildx from './buildx';
66
import * as context from './context';
77
import * as docker from './docker';
8+
import * as nodes from './nodes';
89
import * as stateHelper from './state-helper';
910
import * as util from './util';
1011
import * as core from '@actions/core';
@@ -71,6 +72,21 @@ async function run(): Promise<void> {
7172
core.endGroup();
7273
}
7374

75+
if (inputs.append) {
76+
core.startGroup(`Appending node(s) to builder`);
77+
let nodeIndex = 1;
78+
for (const node of nodes.Parse(inputs.append)) {
79+
const authOpts = auth.setCredentials(credsdir, nodeIndex, inputs.driver, node.endpoint || '');
80+
if (authOpts.length > 0) {
81+
node['driver-opts'] = [...(node['driver-opts'] || []), ...authOpts];
82+
}
83+
const appendCmd = buildx.getCommand(await context.getAppendArgs(inputs, node, buildxVersion), standalone);
84+
await exec.exec(appendCmd.commandLine, appendCmd.args);
85+
nodeIndex++;
86+
}
87+
core.endGroup();
88+
}
89+
7490
core.startGroup(`Booting builder`);
7591
const inspectCmd = buildx.getCommand(await context.getInspectArgs(inputs, buildxVersion), standalone);
7692
await exec.exec(inspectCmd.commandLine, inspectCmd.args);
@@ -88,9 +104,18 @@ async function run(): Promise<void> {
88104
core.startGroup(`Inspect builder`);
89105
const builder = await buildx.inspect(inputs.name, standalone);
90106
const firstNode = builder.nodes[0];
107+
const reducedPlatforms: Array<string> = [];
108+
for (const node of builder.nodes) {
109+
for (const platform of node.platforms?.split(',') || []) {
110+
if (reducedPlatforms.indexOf(platform) > -1) {
111+
continue;
112+
}
113+
reducedPlatforms.push(platform);
114+
}
115+
}
91116
core.info(JSON.stringify(builder, undefined, 2));
92117
core.setOutput('driver', builder.driver);
93-
core.setOutput('platforms', firstNode.platforms);
118+
core.setOutput('platforms', reducedPlatforms.join(','));
94119
core.setOutput('nodes', JSON.stringify(builder.nodes, undefined, 2));
95120
core.setOutput('endpoint', firstNode.endpoint); // TODO: deprecated, to be removed in a later version
96121
core.setOutput('status', firstNode.status); // TODO: deprecated, to be removed in a later version

0 commit comments

Comments
 (0)