diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 505418a1..941fe491 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -416,3 +416,28 @@ jobs: echo "::error::Should have failed" exit 1 fi + + append: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Create dummy contexts + run: | + docker context create ctxbuilder2 + docker context create ctxbuilder3 + - + name: Set up Docker Buildx + uses: ./ + with: + append: | + - name: builder2 + endpoint: ctxbuilder2 + platforms: linux/amd64 + driver-opts: + - image=moby/buildkit:master + - network=host + - endpoint: ctxbuilder3 + platforms: linux/arm64 diff --git a/README.md b/README.md index 9f1373c2..54cb261d 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ ___ * [Usage](#usage) * [Advanced usage](#advanced-usage) * [Authentication to a remote node](docs/advanced/auth.md) + * [Append additional nodes to the builder](docs/advanced/append-nodes.md) * [Install by default](docs/advanced/install-default.md) * [BuildKit daemon configuration](docs/advanced/buildkit-config.md) * [Standalone mode](docs/advanced/standalone.md) @@ -59,7 +60,8 @@ jobs: ## Advanced usage -* [Authentication to a remote node](docs/advanced/auth.md) +* [Authenticationto a remote node](docs/advanced/auth.md) +* [Append additional nodes to the builder](docs/advanced/append-nodes.md) * [Install by default](docs/advanced/install-default.md) * [BuildKit daemon configuration](docs/advanced/buildkit-config.md) * [Standalone mode](docs/advanced/standalone.md) @@ -81,6 +83,7 @@ Following inputs can be used as `step.with` keys | `endpoint` | String | [Optional address for docker socket](https://docs.docker.com/engine/reference/commandline/buildx_create/#description) or context from `docker context ls` | | `config`¹ | String | [BuildKit config file](https://docs.docker.com/engine/reference/commandline/buildx_create/#config) | | `config-inline`¹ | String | Same as `config` but inline | +| `append` | YAML | [Append additional nodes](docs/advanced/append-nodes.md) to the builder | > * ¹ `config` and `config-inline` are mutually exclusive diff --git a/action.yml b/action.yml index 2555ed95..20d19f22 100644 --- a/action.yml +++ b/action.yml @@ -38,6 +38,9 @@ inputs: config-inline: description: 'Inline BuildKit config' required: false + append: + description: 'Append additional nodes to the builder' + required: false outputs: name: diff --git a/docs/advanced/append-nodes.md b/docs/advanced/append-nodes.md new file mode 100644 index 00000000..9e7a1a6b --- /dev/null +++ b/docs/advanced/append-nodes.md @@ -0,0 +1,99 @@ +# Append additional nodes to the builder + +You can append nodes to the builder that is going to be created with the +`append` input in the form of a YAML string document to remove limitations +intrinsically linked to GitHub Actions (only string format is handled in the +input fields). Following fields are supported: + +* `name`: [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. +* `endpoint`: [Docker context or endpoint](https://docs.docker.com/engine/reference/commandline/buildx_create/#description) of the node to add to the builder +* `driver-opts`: List of additional [driver-specific options](https://docs.docker.com/engine/reference/commandline/buildx_create/#driver-opt) +* `buildkitd-flags`: [Flags for buildkitd](https://docs.docker.com/engine/reference/commandline/buildx_create/#buildkitd-flags) daemon +* `platforms`: 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. + +```yaml +name: ci + +on: + push: + +jobs: + buildx: + runs-on: ubuntu-latest + steps: + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + append: | + - endpoint: ssh://me@graviton2 + platforms: linux/arm64 + - endpoint: ssh://foo@linuxone + driver-opts: + - image=moby/buildkit:master +``` + +In this example, a `docker-container` builder will be created on the GitHub +Runner with a local node and two remote nodes. + +To [set up the SSH authentication](auth.md) for the remote nodes, you can use +the following workflow: + +```yaml +name: ci + +on: + push: + +jobs: + buildx: + runs-on: ubuntu-latest + steps: + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + append: | + - endpoint: ssh://me@graviton2 + platforms: linux/arm64 + - endpoint: ssh://foo@linuxone + platforms: linux/s390x + env: + BUILDER_NODE_1_AUTH_SSH_PPK: ${{ secrets.GRAVITON2_SSH_PPK }} + BUILDER_NODE_2_AUTH_SSH_PPK: ${{ secrets.LINUXONE_SSH_PPK }} +``` + +Here is another example using only remote nodes with the `remote` driver: + +```yaml +name: ci + +on: + push: + +jobs: + buildx: + runs-on: ubuntu-latest + steps: + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + driver: remote + endpoint: tcp://oneprovider:1234 + append: | + - endpoint: tcp://graviton2:1234 + platforms: linux/arm64 + - endpoint: tcp://linuxone:1234 + platforms: linux/s390x + env: + BUILDER_NODE_0_AUTH_TLS_CACERT: ${{ secrets.ONEPROVIDER_CA }} + BUILDER_NODE_0_AUTH_TLS_CERT: ${{ secrets.ONEPROVIDER_CERT }} + BUILDER_NODE_0_AUTH_TLS_KEY: ${{ secrets.ONEPROVIDER_KEY }} + BUILDER_NODE_1_AUTH_TLS_CACERT: ${{ secrets.GRAVITON2_CA }} + BUILDER_NODE_1_AUTH_TLS_CERT: ${{ secrets.GRAVITON2_CERT }} + BUILDER_NODE_1_AUTH_TLS_KEY: ${{ secrets.GRAVITON2_KEY }} + BUILDER_NODE_2_AUTH_TLS_CACERT: ${{ secrets.LINUXONE_CA }} + BUILDER_NODE_2_AUTH_TLS_CERT: ${{ secrets.LINUXONE_CERT }} + BUILDER_NODE_2_AUTH_TLS_KEY: ${{ secrets.LINUXONE_KEY }} +``` diff --git a/jest.config.ts b/jest.config.ts index ebf22a56..7ff4e46e 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,10 +1,13 @@ module.exports = { clearMocks: true, moduleFileExtensions: ['js', 'ts'], - setupFiles: ["dotenv/config"], + setupFiles: ['dotenv/config'], testMatch: ['**/*.test.ts'], transform: { '^.+\\.ts$': 'ts-jest' }, + moduleNameMapper: { + '^csv-parse/sync': '/node_modules/csv-parse/dist/cjs/sync.cjs' + }, verbose: true -} +}; diff --git a/package.json b/package.json index f99f45fe..4a0b0161 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1", "@actions/tool-cache": "^2.0.1", + "csv-parse": "^5.1.0", + "js-yaml": "^4.1.0", "semver": "^7.3.7", "tmp": "^0.2.1", "uuid": "^8.3.2" diff --git a/src/context.ts b/src/context.ts index 3e92e492..4b016bc9 100644 --- a/src/context.ts +++ b/src/context.ts @@ -3,7 +3,9 @@ import * as os from 'os'; import path from 'path'; import * as tmp from 'tmp'; import * as uuid from 'uuid'; +import {parse} from 'csv-parse/sync'; import * as buildx from './buildx'; +import * as nodes from './nodes'; import * as core from '@actions/core'; import {issueCommand} from '@actions/core/lib/command'; @@ -33,6 +35,7 @@ export interface Inputs { endpoint: string; config: string; configInline: string; + append: string; } export async function getInputs(): Promise { @@ -46,7 +49,8 @@ export async function getInputs(): Promise { use: core.getBooleanInput('use'), endpoint: core.getInput('endpoint'), config: core.getInput('config'), - configInline: core.getInput('config-inline') + configInline: core.getInput('config-inline'), + append: core.getInput('append') }; } @@ -80,6 +84,25 @@ export async function getCreateArgs(inputs: Inputs, buildxVersion: string): Prom return args; } +export async function getAppendArgs(inputs: Inputs, node: nodes.Node, buildxVersion: string): Promise> { + const args: Array = ['create', '--name', inputs.name, '--append']; + if (node.name) { + args.push('--node', node.name); + } + if (node['driver-opts'] && buildx.satisfies(buildxVersion, '>=0.3.0')) { + await asyncForEach(node['driver-opts'], async driverOpt => { + args.push('--driver-opt', driverOpt); + }); + } + if (node.platforms) { + args.push('--platform', node.platforms); + } + if (node.endpoint) { + args.push(node.endpoint); + } + return args; +} + export async function getInspectArgs(inputs: Inputs, buildxVersion: string): Promise> { const args: Array = ['inspect', '--bootstrap']; if (buildx.satisfies(buildxVersion, '>=0.4.0')) { @@ -89,14 +112,33 @@ export async function getInspectArgs(inputs: Inputs, buildxVersion: string): Pro } export async function getInputList(name: string, ignoreComma?: boolean): Promise { + const res: Array = []; + const items = core.getInput(name); if (items == '') { - return []; + return res; } - return items - .split(/\r?\n/) - .filter(x => x) - .reduce((acc, line) => acc.concat(!ignoreComma ? line.split(',').filter(x => x) : line).map(pat => pat.trim()), []); + + const records = parse(items, { + columns: false, + relaxQuotes: true, + comment: '#', + relaxColumnCount: true, + skipEmptyLines: true + }); + + for (const record of records as Array) { + if (record.length == 1) { + res.push(record[0]); + continue; + } else if (!ignoreComma) { + res.push(...record); + continue; + } + res.push(record.join(',')); + } + + return res.filter(item => item).map(pat => pat.trim()); } export const asyncForEach = async (array, callback) => { diff --git a/src/main.ts b/src/main.ts index dce5f210..38002937 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ import * as auth from './auth'; import * as buildx from './buildx'; import * as context from './context'; import * as docker from './docker'; +import * as nodes from './nodes'; import * as stateHelper from './state-helper'; import * as util from './util'; import * as core from '@actions/core'; @@ -71,6 +72,21 @@ async function run(): Promise { core.endGroup(); } + if (inputs.append) { + core.startGroup(`Appending node(s) to builder`); + let nodeIndex = 1; + for (const node of nodes.Parse(inputs.append)) { + const authOpts = auth.setCredentials(credsdir, nodeIndex, node.endpoint || ''); + if (authOpts.length > 0) { + node['driver-opts'] = [...(node['driver-opts'] || []), ...authOpts]; + } + const appendCmd = buildx.getCommand(await context.getAppendArgs(inputs, node, buildxVersion), standalone); + await exec.exec(appendCmd.commandLine, appendCmd.args); + nodeIndex++; + } + core.endGroup(); + } + core.startGroup(`Booting builder`); const inspectCmd = buildx.getCommand(await context.getInspectArgs(inputs, buildxVersion), standalone); await exec.exec(inspectCmd.commandLine, inspectCmd.args); diff --git a/src/nodes.ts b/src/nodes.ts new file mode 100644 index 00000000..60443e83 --- /dev/null +++ b/src/nodes.ts @@ -0,0 +1,13 @@ +import * as yaml from 'js-yaml'; + +export type Node = { + name?: string; + endpoint?: string; + 'driver-opts'?: Array; + 'buildkitd-flags'?: string; + platforms?: string; +}; + +export function Parse(data: string): Node[] { + return yaml.load(data) as Node[]; +} diff --git a/yarn.lock b/yarn.lock index cddb21db..3fdbbe00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1450,6 +1450,11 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +csv-parse@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.3.0.tgz#85cc02fc9d1c89bd1b02e69069c960f8b8064322" + integrity sha512-UXJCGwvJ2fep39purtAn27OUYmxB1JQto+zhZ4QlJpzsirtSFbzLvip1aIgziqNdZp/TptvsKEV5BZSxe10/DQ== + data-urls@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"