support standalone mode and display version

Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax 2022-04-17 17:22:03 +02:00
parent 2a6fbda6d8
commit 3472856dd9
No known key found for this signature in database
GPG key ID: 3248E46B6BB8C7F7
9 changed files with 223 additions and 46 deletions

View file

@ -24,6 +24,7 @@ ___
* [BuildKit daemon configuration](#buildkit-daemon-configuration) * [BuildKit daemon configuration](#buildkit-daemon-configuration)
* [Registry mirror](#registry-mirror) * [Registry mirror](#registry-mirror)
* [Max parallelism](#max-parallelism) * [Max parallelism](#max-parallelism)
* [Standalone mode](#standalone-mode)
* [Customizing](#customizing) * [Customizing](#customizing)
* [inputs](#inputs) * [inputs](#inputs)
* [outputs](#outputs) * [outputs](#outputs)
@ -180,6 +181,33 @@ jobs:
config: .github/buildkitd.toml config: .github/buildkitd.toml
``` ```
### Standalone mode
If you don't have the Docker CLI installed on the GitHub Runner, buildx binary
is invoked directly, instead of calling it as a docker plugin. This can be
useful if you want to use the `kubernetes` driver in your self-hosted runner:
```yaml
name: ci
on:
push:
jobs:
buildx:
runs-on: ubuntu-latest
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
with:
driver: kubernetes
-
name: Build
run: |
buildx build .
```
## Customizing ## Customizing
### inputs ### inputs
@ -195,10 +223,10 @@ Following inputs can be used as `step.with` keys
| `install` | Bool | Sets up `docker build` command as an alias to `docker buildx` (default `false`) | | `install` | Bool | Sets up `docker build` command as an alias to `docker buildx` (default `false`) |
| `use` | Bool | Switch to this builder instance (default `true`) | | `use` | Bool | Switch to this builder instance (default `true`) |
| `endpoint` | String | [Optional address for docker socket](https://github.com/docker/buildx/blob/master/docs/reference/buildx_create.md#description) or context from `docker context ls` | | `endpoint` | String | [Optional address for docker socket](https://github.com/docker/buildx/blob/master/docs/reference/buildx_create.md#description) or context from `docker context ls` |
| `config` | String | [BuildKit config file](https://github.com/docker/buildx/blob/master/docs/reference/buildx_create.md#config) | | `config`¹ | String | [BuildKit config file](https://github.com/docker/buildx/blob/master/docs/reference/buildx_create.md#config) |
| `config-inline` | String | Same as `config` but inline | | `config-inline`¹ | String | Same as `config` but inline |
> `config` and `config-inline` are mutually exclusive. > * ¹ `config` and `config-inline` are mutually exclusive
> `CSV` type must be a newline-delimited string > `CSV` type must be a newline-delimited string
> ```yaml > ```yaml

View file

@ -32,6 +32,17 @@ describe('isAvailable', () => {
}); });
}); });
describe('isAvailable standalone', () => {
const execSpy = jest.spyOn(exec, 'getExecOutput');
buildx.isAvailable(true);
// eslint-disable-next-line jest/no-standalone-expect
expect(execSpy).toHaveBeenCalledWith(`buildx`, [], {
silent: true,
ignoreReturnCode: true
});
});
describe('getVersion', () => { describe('getVersion', () => {
it('valid', async () => { it('valid', async () => {
const version = await buildx.getVersion(); const version = await buildx.getVersion();
@ -75,29 +86,32 @@ describe('build', () => {
// eslint-disable-next-line jest/no-disabled-tests // eslint-disable-next-line jest/no-disabled-tests
it.skip('builds refs/pull/648/head', async () => { it.skip('builds refs/pull/648/head', async () => {
const buildxBin = await buildx.build('https://github.com/docker/buildx.git#refs/pull/648/head', tmpDir); const buildxBin = await buildx.build('https://github.com/docker/buildx.git#refs/pull/648/head', tmpDir, false);
expect(fs.existsSync(buildxBin)).toBe(true); expect(fs.existsSync(buildxBin)).toBe(true);
}, 100000); }, 100000);
// eslint-disable-next-line jest/no-disabled-tests // eslint-disable-next-line jest/no-disabled-tests
it.skip('builds 67bd6f4dc82a9cd96f34133dab3f6f7af803bb14', async () => { it.skip('builds 67bd6f4dc82a9cd96f34133dab3f6f7af803bb14', async () => {
const buildxBin = await buildx.build('https://github.com/docker/buildx.git#67bd6f4dc82a9cd96f34133dab3f6f7af803bb14', tmpDir); const buildxBin = await buildx.build('https://github.com/docker/buildx.git#67bd6f4dc82a9cd96f34133dab3f6f7af803bb14', tmpDir, false);
expect(fs.existsSync(buildxBin)).toBe(true); expect(fs.existsSync(buildxBin)).toBe(true);
}, 100000); }, 100000);
}); });
describe('install', () => { describe('install', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'setup-buildx-')); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'setup-buildx-'));
test.each([
it('acquires v0.4.1 version of buildx', async () => { ['v0.4.1', false],
const buildxBin = await buildx.install('v0.4.1', tmpDir); ['latest', false],
['v0.4.1', true],
['latest', true]
])(
'acquires %p of buildx (standalone: %p)',
async (version, standalone) => {
const buildxBin = await buildx.install(version, tmpDir, standalone);
expect(fs.existsSync(buildxBin)).toBe(true); expect(fs.existsSync(buildxBin)).toBe(true);
}, 100000); },
100000
it('acquires latest version of buildx', async () => { );
const buildxBin = await buildx.install('latest', tmpDir);
expect(fs.existsSync(buildxBin)).toBe(true);
}, 100000);
}); });
describe('getConfig', () => { describe('getConfig', () => {

16
__tests__/docker.test.ts Normal file
View file

@ -0,0 +1,16 @@
import {describe, expect, it, jest} from '@jest/globals';
import * as docker from '../src/docker';
import * as exec from '@actions/exec';
describe('isAvailable', () => {
it('cli', () => {
const execSpy = jest.spyOn(exec, 'getExecOutput');
docker.isAvailable();
// eslint-disable-next-line jest/no-standalone-expect
expect(execSpy).toHaveBeenCalledWith(`docker`, undefined, {
silent: true,
ignoreReturnCode: true
});
});
});

4
dist/index.js generated vendored

File diff suppressed because one or more lines are too long

2
dist/index.js.map generated vendored

File diff suppressed because one or more lines are too long

View file

@ -41,9 +41,10 @@ export async function getConfig(s: string, file: boolean): Promise<string> {
return configFile; return configFile;
} }
export async function isAvailable(): Promise<boolean> { export async function isAvailable(standalone?: boolean): Promise<boolean> {
const cmd = getCommand([], standalone);
return await exec return await exec
.getExecOutput('docker', ['buildx'], { .getExecOutput(cmd.commandLine, cmd.args, {
ignoreReturnCode: true, ignoreReturnCode: true,
silent: true silent: true
}) })
@ -52,12 +53,17 @@ export async function isAvailable(): Promise<boolean> {
return false; return false;
} }
return res.exitCode == 0; return res.exitCode == 0;
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.catch(error => {
return false;
}); });
} }
export async function getVersion(): Promise<string> { export async function getVersion(standalone?: boolean): Promise<string> {
const cmd = getCommand(['version'], standalone);
return await exec return await exec
.getExecOutput('docker', ['buildx', 'version'], { .getExecOutput(cmd.commandLine, cmd.args, {
ignoreReturnCode: true, ignoreReturnCode: true,
silent: true silent: true
}) })
@ -81,9 +87,10 @@ export function satisfies(version: string, range: string): boolean {
return semver.satisfies(version, range) || /^[0-9a-f]{7}$/.exec(version) !== null; return semver.satisfies(version, range) || /^[0-9a-f]{7}$/.exec(version) !== null;
} }
export async function inspect(name: string): Promise<Builder> { export async function inspect(name: string, standalone?: boolean): Promise<Builder> {
const cmd = getCommand(['inspect', name], standalone);
return await exec return await exec
.getExecOutput(`docker`, ['buildx', 'inspect', name], { .getExecOutput(cmd.commandLine, cmd.args, {
ignoreReturnCode: true, ignoreReturnCode: true,
silent: true silent: true
}) })
@ -133,7 +140,7 @@ export async function inspect(name: string): Promise<Builder> {
}); });
} }
export async function build(inputBuildRef: string, dockerConfigHome: string): Promise<string> { export async function build(inputBuildRef: string, dest: string, standalone: boolean): Promise<string> {
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let [repo, ref] = inputBuildRef.split('#'); let [repo, ref] = inputBuildRef.split('#');
if (ref.length == 0) { if (ref.length == 0) {
@ -152,8 +159,27 @@ export async function build(inputBuildRef: string, dockerConfigHome: string): Pr
toolPath = tc.find('buildx', vspec); toolPath = tc.find('buildx', vspec);
if (!toolPath) { if (!toolPath) {
const outFolder = path.join(context.tmpDir(), 'out').split(path.sep).join(path.posix.sep); const outFolder = path.join(context.tmpDir(), 'out').split(path.sep).join(path.posix.sep);
let buildWithStandalone = false;
const standaloneFound = await isAvailable(true);
const pluginFound = await isAvailable(false);
if (standalone && standaloneFound) {
core.debug(`Buildx standalone found, build with it`);
buildWithStandalone = true;
} else if (!standalone && pluginFound) {
core.debug(`Buildx plugin found, build with it`);
buildWithStandalone = false;
} else if (standaloneFound) {
core.debug(`Buildx plugin not found, but standalone found so trying to build with it`);
buildWithStandalone = true;
} else if (pluginFound) {
core.debug(`Buildx standalone not found, but plugin found so trying to build with it`);
buildWithStandalone = false;
} else {
throw new Error(`Neither buildx standalone or plugin have been found to build from ref`);
}
const buildCmd = getCommand(['build', '--target', 'binaries', '--build-arg', 'BUILDKIT_CONTEXT_KEEP_GIT_DIR=1', '--output', `type=local,dest=${outFolder}`, inputBuildRef], buildWithStandalone);
toolPath = await exec toolPath = await exec
.getExecOutput('docker', ['buildx', 'build', '--target', 'binaries', '--build-arg', 'BUILDKIT_CONTEXT_KEEP_GIT_DIR=1', '--output', `type=local,dest=${outFolder}`, inputBuildRef], { .getExecOutput(buildCmd.commandLine, buildCmd.args, {
ignoreReturnCode: true ignoreReturnCode: true
}) })
.then(res => { .then(res => {
@ -164,10 +190,13 @@ export async function build(inputBuildRef: string, dockerConfigHome: string): Pr
}); });
} }
return setPlugin(toolPath, dockerConfigHome); if (standalone) {
return setStandalone(toolPath, dest);
}
return setPlugin(toolPath, dest);
} }
export async function install(inputVersion: string, dockerConfigHome: string): Promise<string> { export async function install(inputVersion: string, dest: string, standalone: boolean): Promise<string> {
const release: github.GitHubRelease | null = await github.getRelease(inputVersion); const release: github.GitHubRelease | null = await github.getRelease(inputVersion);
if (!release) { if (!release) {
throw new Error(`Cannot find buildx ${inputVersion} release`); throw new Error(`Cannot find buildx ${inputVersion} release`);
@ -185,10 +214,40 @@ export async function install(inputVersion: string, dockerConfigHome: string): P
toolPath = await download(version); toolPath = await download(version);
} }
return setPlugin(toolPath, dockerConfigHome); if (standalone) {
return setStandalone(toolPath, dest);
}
return setPlugin(toolPath, dest);
}
async function setStandalone(toolPath: string, dest: string): Promise<string> {
core.info('Standalone mode');
const toolBinPath = path.join(toolPath, context.osPlat == 'win32' ? 'docker-buildx.exe' : 'docker-buildx');
const binDir = path.join(dest, 'bin');
core.debug(`Bin dir is ${binDir}`);
if (!fs.existsSync(binDir)) {
fs.mkdirSync(binDir, {recursive: true});
}
const filename: string = context.osPlat == 'win32' ? 'buildx.exe' : 'buildx';
const buildxPath: string = path.join(binDir, filename);
core.debug(`Bin path is ${buildxPath}`);
fs.copyFileSync(toolBinPath, buildxPath);
core.info('Fixing perms');
fs.chmodSync(buildxPath, '0755');
core.addPath(binDir);
core.info('Added buildx to the path');
return buildxPath;
} }
async function setPlugin(toolPath: string, dockerConfigHome: string): Promise<string> { async function setPlugin(toolPath: string, dockerConfigHome: string): Promise<string> {
core.info('Docker plugin mode');
const toolBinPath = path.join(toolPath, context.osPlat == 'win32' ? 'docker-buildx.exe' : 'docker-buildx');
const pluginsDir: string = path.join(dockerConfigHome, 'cli-plugins'); const pluginsDir: string = path.join(dockerConfigHome, 'cli-plugins');
core.debug(`Plugins dir is ${pluginsDir}`); core.debug(`Plugins dir is ${pluginsDir}`);
if (!fs.existsSync(pluginsDir)) { if (!fs.existsSync(pluginsDir)) {
@ -198,7 +257,7 @@ async function setPlugin(toolPath: string, dockerConfigHome: string): Promise<st
const filename: string = context.osPlat == 'win32' ? 'docker-buildx.exe' : 'docker-buildx'; const filename: string = context.osPlat == 'win32' ? 'docker-buildx.exe' : 'docker-buildx';
const pluginPath: string = path.join(pluginsDir, filename); const pluginPath: string = path.join(pluginsDir, filename);
core.debug(`Plugin path is ${pluginPath}`); core.debug(`Plugin path is ${pluginPath}`);
fs.copyFileSync(path.join(toolPath, filename), pluginPath); fs.copyFileSync(toolBinPath, pluginPath);
core.info('Fixing perms'); core.info('Fixing perms');
fs.chmodSync(pluginPath, '0755'); fs.chmodSync(pluginPath, '0755');
@ -269,3 +328,10 @@ export async function getBuildKitVersion(containerID: string): Promise<string> {
return bkitimage.stdout.trim(); return bkitimage.stdout.trim();
}); });
} }
export function getCommand(args: Array<string>, standalone?: boolean) {
return {
commandLine: standalone ? 'buildx' : 'docker',
args: standalone ? args : ['buildx', ...args]
};
}

19
src/docker.ts Normal file
View file

@ -0,0 +1,19 @@
import * as exec from '@actions/exec';
export async function isAvailable(): Promise<boolean> {
return await exec
.getExecOutput('docker', undefined, {
ignoreReturnCode: true,
silent: true
})
.then(res => {
if (res.stderr.length > 0 && res.exitCode != 0) {
return false;
}
return res.exitCode == 0;
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.catch(error => {
return false;
});
}

View file

@ -3,6 +3,7 @@ import * as path from 'path';
import * as uuid from 'uuid'; import * as uuid from 'uuid';
import * as buildx from './buildx'; import * as buildx from './buildx';
import * as context from './context'; import * as context from './context';
import * as docker from './docker';
import * as stateHelper from './state-helper'; import * as stateHelper from './state-helper';
import * as util from './util'; import * as util from './util';
import * as core from '@actions/core'; import * as core from '@actions/core';
@ -10,32 +11,54 @@ import * as exec from '@actions/exec';
async function run(): Promise<void> { async function run(): Promise<void> {
try { try {
core.startGroup(`Docker info`);
await exec.exec('docker', ['version']);
await exec.exec('docker', ['info']);
core.endGroup();
const inputs: context.Inputs = await context.getInputs(); const inputs: context.Inputs = await context.getInputs();
const dockerConfigHome: string = process.env.DOCKER_CONFIG || path.join(os.homedir(), '.docker'); const dockerConfigHome: string = process.env.DOCKER_CONFIG || path.join(os.homedir(), '.docker');
if (util.isValidUrl(inputs.version)) { // standalone if docker cli not available
core.startGroup(`Build and install buildx`); const standalone = !(await docker.isAvailable());
await buildx.build(inputs.version, dockerConfigHome); stateHelper.setStandalone(standalone);
core.startGroup(`Docker info`);
if (standalone) {
core.info(`Docker info skipped in standalone mode`);
} else {
await exec.exec('docker', ['version'], {
failOnStdErr: false
});
await exec.exec('docker', ['info'], {
failOnStdErr: false
});
}
core.endGroup(); core.endGroup();
} else if (!(await buildx.isAvailable()) || inputs.version) {
if (util.isValidUrl(inputs.version)) {
if (standalone) {
throw new Error(`Cannot build from source without the Docker CLI`);
}
core.startGroup(`Build and install buildx`);
await buildx.build(inputs.version, dockerConfigHome, standalone);
core.endGroup();
} else if (!(await buildx.isAvailable(standalone)) || inputs.version) {
core.startGroup(`Download and install buildx`); core.startGroup(`Download and install buildx`);
await buildx.install(inputs.version || 'latest', dockerConfigHome); await buildx.install(inputs.version || 'latest', standalone ? context.tmpDir() : dockerConfigHome, standalone);
core.endGroup(); core.endGroup();
} }
const buildxVersion = await buildx.getVersion(); const buildxVersion = await buildx.getVersion(standalone);
await core.group(`Buildx version`, async () => {
const versionCmd = buildx.getCommand(['version'], standalone);
await exec.exec(versionCmd.commandLine, versionCmd.args, {
failOnStdErr: false
});
});
const builderName: string = inputs.driver == 'docker' ? 'default' : `builder-${uuid.v4()}`; const builderName: string = inputs.driver == 'docker' ? 'default' : `builder-${uuid.v4()}`;
context.setOutput('name', builderName); context.setOutput('name', builderName);
stateHelper.setBuilderName(builderName); stateHelper.setBuilderName(builderName);
if (inputs.driver !== 'docker') { if (inputs.driver !== 'docker') {
core.startGroup(`Creating a new builder instance`); core.startGroup(`Creating a new builder instance`);
const createArgs: Array<string> = ['buildx', 'create', '--name', builderName, '--driver', inputs.driver]; const createArgs: Array<string> = ['create', '--name', builderName, '--driver', inputs.driver];
if (buildx.satisfies(buildxVersion, '>=0.3.0')) { if (buildx.satisfies(buildxVersion, '>=0.3.0')) {
await context.asyncForEach(inputs.driverOpts, async driverOpt => { await context.asyncForEach(inputs.driverOpts, async driverOpt => {
createArgs.push('--driver-opt', driverOpt); createArgs.push('--driver-opt', driverOpt);
@ -55,26 +78,31 @@ async function run(): Promise<void> {
} else if (inputs.configInline) { } else if (inputs.configInline) {
createArgs.push('--config', await buildx.getConfigInline(inputs.configInline)); createArgs.push('--config', await buildx.getConfigInline(inputs.configInline));
} }
await exec.exec('docker', createArgs); const createCmd = buildx.getCommand(createArgs, standalone);
await exec.exec(createCmd.commandLine, createCmd.args);
core.endGroup(); core.endGroup();
core.startGroup(`Booting builder`); core.startGroup(`Booting builder`);
const bootstrapArgs: Array<string> = ['buildx', 'inspect', '--bootstrap']; const bootstrapArgs: Array<string> = ['inspect', '--bootstrap'];
if (buildx.satisfies(buildxVersion, '>=0.4.0')) { if (buildx.satisfies(buildxVersion, '>=0.4.0')) {
bootstrapArgs.push('--builder', builderName); bootstrapArgs.push('--builder', builderName);
} }
await exec.exec('docker', bootstrapArgs); const bootstrapCmd = buildx.getCommand(bootstrapArgs, standalone);
await exec.exec(bootstrapCmd.commandLine, bootstrapCmd.args);
core.endGroup(); core.endGroup();
} }
if (inputs.install) { if (inputs.install) {
if (standalone) {
throw new Error(`Cannot set buildx as default builder without the Docker CLI`);
}
core.startGroup(`Setting buildx as default builder`); core.startGroup(`Setting buildx as default builder`);
await exec.exec('docker', ['buildx', 'install']); await exec.exec('docker', ['buildx', 'install']);
core.endGroup(); core.endGroup();
} }
core.startGroup(`Inspect builder`); core.startGroup(`Inspect builder`);
const builder = await buildx.inspect(builderName); const builder = await buildx.inspect(builderName, standalone);
core.info(JSON.stringify(builder, undefined, 2)); core.info(JSON.stringify(builder, undefined, 2));
context.setOutput('driver', builder.driver); context.setOutput('driver', builder.driver);
context.setOutput('endpoint', builder.node_endpoint); context.setOutput('endpoint', builder.node_endpoint);
@ -83,7 +111,7 @@ async function run(): Promise<void> {
context.setOutput('platforms', builder.node_platforms); context.setOutput('platforms', builder.node_platforms);
core.endGroup(); core.endGroup();
if (inputs.driver == 'docker-container') { if (!standalone && inputs.driver == 'docker-container') {
stateHelper.setContainerName(`buildx_buildkit_${builder.node_name}`); stateHelper.setContainerName(`buildx_buildkit_${builder.node_name}`);
core.startGroup(`BuildKit version`); core.startGroup(`BuildKit version`);
core.info(await buildx.getBuildKitVersion(`buildx_buildkit_${builder.node_name}`)); core.info(await buildx.getBuildKitVersion(`buildx_buildkit_${builder.node_name}`));
@ -114,8 +142,9 @@ async function cleanup(): Promise<void> {
if (stateHelper.builderName.length > 0) { if (stateHelper.builderName.length > 0) {
core.startGroup(`Removing builder`); core.startGroup(`Removing builder`);
const rmCmd = buildx.getCommand(['rm', stateHelper.builderName], /true/i.test(stateHelper.standalone));
await exec await exec
.getExecOutput('docker', ['buildx', 'rm', `${stateHelper.builderName}`], { .getExecOutput(rmCmd.commandLine, rmCmd.args, {
ignoreReturnCode: true ignoreReturnCode: true
}) })
.then(res => { .then(res => {

View file

@ -2,6 +2,7 @@ import * as core from '@actions/core';
export const IsPost = !!process.env['STATE_isPost']; export const IsPost = !!process.env['STATE_isPost'];
export const IsDebug = !!process.env['STATE_isDebug']; export const IsDebug = !!process.env['STATE_isDebug'];
export const standalone = process.env['STATE_standalone'] || '';
export const builderName = process.env['STATE_builderName'] || ''; export const builderName = process.env['STATE_builderName'] || '';
export const containerName = process.env['STATE_containerName'] || ''; export const containerName = process.env['STATE_containerName'] || '';
@ -9,6 +10,10 @@ export function setDebug(debug: string) {
core.saveState('isDebug', debug); core.saveState('isDebug', debug);
} }
export function setStandalone(standalone: boolean) {
core.saveState('standalone', standalone);
}
export function setBuilderName(builderName: string) { export function setBuilderName(builderName: string) {
core.saveState('builderName', builderName); core.saveState('builderName', builderName);
} }