mirror of
https://github.com/docker/setup-buildx-action
synced 2024-11-24 11:31:40 +00:00
Save BuildKit state on client for cache support
Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
parent
74283caced
commit
f3f23a5162
9 changed files with 127 additions and 8 deletions
|
@ -197,8 +197,11 @@ Following inputs can be used as `step.with` keys
|
||||||
| `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 |
|
||||||
|
| `state-dir` | String | Path to [BuildKit state volume](https://github.com/docker/buildx/blob/master/docs/reference/buildx_rm.md#-keep-buildkit-state---keep-state) directory |
|
||||||
|
|
||||||
> `config` and `config-inline` are mutually exclusive.
|
> :bulb: `config` and `config-inline` are mutually exclusive.
|
||||||
|
|
||||||
|
> :bulb: `state-dir` can only be used with the `docker-container` driver and a builder with a single node.
|
||||||
|
|
||||||
> `CSV` type must be a newline-delimited string
|
> `CSV` type must be a newline-delimited string
|
||||||
> ```yaml
|
> ```yaml
|
||||||
|
|
|
@ -38,6 +38,9 @@ inputs:
|
||||||
config-inline:
|
config-inline:
|
||||||
description: 'Inline BuildKit config'
|
description: 'Inline BuildKit config'
|
||||||
required: false
|
required: false
|
||||||
|
state-dir:
|
||||||
|
description: 'Path to BuildKit state volume directory'
|
||||||
|
required: false
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
name:
|
name:
|
||||||
|
|
4
dist/index.js
generated
vendored
4
dist/index.js
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
|
@ -3,11 +3,16 @@ import * as path from 'path';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import * as context from './context';
|
import * as context from './context';
|
||||||
|
import * as docker from './docker';
|
||||||
import * as git from './git';
|
import * as git from './git';
|
||||||
import * as github from './github';
|
import * as github from './github';
|
||||||
import * as core from '@actions/core';
|
import * as core from '@actions/core';
|
||||||
import * as exec from '@actions/exec';
|
import * as exec from '@actions/exec';
|
||||||
import * as tc from '@actions/tool-cache';
|
import * as tc from '@actions/tool-cache';
|
||||||
|
import child_process from 'child_process';
|
||||||
|
|
||||||
|
const uid = parseInt(child_process.execSync(`id -u`, {encoding: 'utf8'}).trim());
|
||||||
|
const gid = parseInt(child_process.execSync(`id -g`, {encoding: 'utf8'}).trim());
|
||||||
|
|
||||||
export type Builder = {
|
export type Builder = {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
@ -81,6 +86,19 @@ 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 createStateVolume(stateDir: string, nodeName: string): Promise<void> {
|
||||||
|
return await docker.volumeCreate(stateDir, `${nodeName}_state`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveStateVolume(dir: string, nodeName: string): Promise<void> {
|
||||||
|
const ctnid = await docker.containerCreate('busybox', `${nodeName}_state:/data`);
|
||||||
|
const outdir = await docker.containerCopy(ctnid, `${ctnid}:/data`);
|
||||||
|
await docker.volumeRemove(`${nodeName}_state`);
|
||||||
|
fs.rmdirSync(dir, {recursive: true});
|
||||||
|
fs.renameSync(outdir, dir);
|
||||||
|
await docker.containerRemove(ctnid);
|
||||||
|
}
|
||||||
|
|
||||||
export async function inspect(name: string): Promise<Builder> {
|
export async function inspect(name: string): Promise<Builder> {
|
||||||
return await exec
|
return await exec
|
||||||
.getExecOutput(`docker`, ['buildx', 'inspect', name], {
|
.getExecOutput(`docker`, ['buildx', 'inspect', name], {
|
||||||
|
|
|
@ -30,6 +30,7 @@ export interface Inputs {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
config: string;
|
config: string;
|
||||||
configInline: string;
|
configInline: string;
|
||||||
|
stateDir: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getInputs(): Promise<Inputs> {
|
export async function getInputs(): Promise<Inputs> {
|
||||||
|
@ -42,7 +43,8 @@ export async function getInputs(): Promise<Inputs> {
|
||||||
use: core.getBooleanInput('use'),
|
use: core.getBooleanInput('use'),
|
||||||
endpoint: core.getInput('endpoint'),
|
endpoint: core.getInput('endpoint'),
|
||||||
config: core.getInput('config'),
|
config: core.getInput('config'),
|
||||||
configInline: core.getInput('config-inline')
|
configInline: core.getInput('config-inline'),
|
||||||
|
stateDir: core.getInput('state-dir')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
71
src/docker.ts
Normal file
71
src/docker.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as uuid from 'uuid';
|
||||||
|
import * as context from './context';
|
||||||
|
import * as exec from '@actions/exec';
|
||||||
|
|
||||||
|
export async function volumeCreate(dir: string, name: string): Promise<void> {
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, {recursive: true});
|
||||||
|
}
|
||||||
|
return await exec
|
||||||
|
.getExecOutput(`docker`, ['volume', 'create', '--name', `${name}`, '--driver', 'local', '--opt', `o=bind,acl`, '--opt', 'type=none', '--opt', `device=${dir}`], {
|
||||||
|
ignoreReturnCode: true
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.stderr.length > 0 && res.exitCode != 0) {
|
||||||
|
throw new Error(res.stderr.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function volumeRemove(name: string): Promise<void> {
|
||||||
|
return await exec
|
||||||
|
.getExecOutput(`docker`, ['volume', 'rm', '-f', `${name}`], {
|
||||||
|
ignoreReturnCode: true
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.stderr.length > 0 && res.exitCode != 0) {
|
||||||
|
throw new Error(res.stderr.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function containerCreate(image: string, volume: string): Promise<string> {
|
||||||
|
return await exec
|
||||||
|
.getExecOutput(`docker`, ['create', '--rm', '-v', `${volume}`, `${image}`], {
|
||||||
|
ignoreReturnCode: true
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.stderr.length > 0 && res.exitCode != 0) {
|
||||||
|
throw new Error(res.stderr.trim());
|
||||||
|
}
|
||||||
|
return res.stdout.trim();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function containerCopy(ctnid: string, src: string): Promise<string> {
|
||||||
|
const outdir = path.join(context.tmpDir(), `ctn-copy-${uuid.v4()}`).split(path.sep).join(path.posix.sep);
|
||||||
|
return await exec
|
||||||
|
.getExecOutput(`docker`, ['cp', '-a', `${src}`, `${outdir}`], {
|
||||||
|
ignoreReturnCode: true
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.stderr.length > 0 && res.exitCode != 0) {
|
||||||
|
throw new Error(res.stderr.trim());
|
||||||
|
}
|
||||||
|
return outdir;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function containerRemove(ctnid: string): Promise<void> {
|
||||||
|
return await exec
|
||||||
|
.getExecOutput(`docker`, ['rm', '-f', '-v', `${ctnid}`], {
|
||||||
|
ignoreReturnCode: true
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.stderr.length > 0 && res.exitCode != 0) {
|
||||||
|
throw new Error(res.stderr.trim());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
22
src/main.ts
22
src/main.ts
|
@ -16,8 +16,10 @@ async function run(): Promise<void> {
|
||||||
core.endGroup();
|
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 builderName: string = inputs.driver == 'docker' ? 'default' : `builder-${uuid.v4()}`;
|
||||||
|
stateHelper.setStateDir(inputs.stateDir);
|
||||||
|
|
||||||
|
const dockerConfigHome: string = process.env.DOCKER_CONFIG || path.join(os.homedir(), '.docker');
|
||||||
if (util.isValidUrl(inputs.version)) {
|
if (util.isValidUrl(inputs.version)) {
|
||||||
core.startGroup(`Build and install buildx`);
|
core.startGroup(`Build and install buildx`);
|
||||||
await buildx.build(inputs.version, dockerConfigHome);
|
await buildx.build(inputs.version, dockerConfigHome);
|
||||||
|
@ -29,11 +31,15 @@ async function run(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildxVersion = await buildx.getVersion();
|
const buildxVersion = await buildx.getVersion();
|
||||||
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') {
|
||||||
|
if (inputs.stateDir.length > 0) {
|
||||||
|
await core.group(`Creating BuildKit state volume from ${inputs.stateDir}`, async () => {
|
||||||
|
await buildx.createStateVolume(inputs.stateDir, `buildx_buildkit_${builderName}0`);
|
||||||
|
});
|
||||||
|
}
|
||||||
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> = ['buildx', 'create', '--name', builderName, '--driver', inputs.driver];
|
||||||
if (buildx.satisfies(buildxVersion, '>=0.3.0')) {
|
if (buildx.satisfies(buildxVersion, '>=0.3.0')) {
|
||||||
|
@ -114,8 +120,12 @@ async function cleanup(): Promise<void> {
|
||||||
|
|
||||||
if (stateHelper.builderName.length > 0) {
|
if (stateHelper.builderName.length > 0) {
|
||||||
core.startGroup(`Removing builder`);
|
core.startGroup(`Removing builder`);
|
||||||
|
const rmArgs: Array<string> = ['buildx', 'rm', `${stateHelper.builderName}`];
|
||||||
|
if (stateHelper.stateDir.length > 0) {
|
||||||
|
rmArgs.push('--keep-state');
|
||||||
|
}
|
||||||
await exec
|
await exec
|
||||||
.getExecOutput('docker', ['buildx', 'rm', `${stateHelper.builderName}`], {
|
.getExecOutput('docker', rmArgs, {
|
||||||
ignoreReturnCode: true
|
ignoreReturnCode: true
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
@ -125,6 +135,12 @@ async function cleanup(): Promise<void> {
|
||||||
});
|
});
|
||||||
core.endGroup();
|
core.endGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stateHelper.stateDir.length > 0) {
|
||||||
|
core.startGroup(`Saving state volume`);
|
||||||
|
await buildx.saveStateVolume(stateHelper.stateDir, stateHelper.containerName);
|
||||||
|
core.endGroup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stateHelper.IsPost) {
|
if (!stateHelper.IsPost) {
|
||||||
|
|
|
@ -2,8 +2,10 @@ 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 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'] || '';
|
||||||
|
export const stateDir = process.env['STATE_stateDir'] || '';
|
||||||
|
|
||||||
export function setDebug(debug: string) {
|
export function setDebug(debug: string) {
|
||||||
core.saveState('isDebug', debug);
|
core.saveState('isDebug', debug);
|
||||||
|
@ -17,6 +19,10 @@ export function setContainerName(containerName: string) {
|
||||||
core.saveState('containerName', containerName);
|
core.saveState('containerName', containerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setStateDir(stateDir: string) {
|
||||||
|
core.saveState('stateDir', stateDir);
|
||||||
|
}
|
||||||
|
|
||||||
if (!IsPost) {
|
if (!IsPost) {
|
||||||
core.saveState('isPost', 'true');
|
core.saveState('isPost', 'true');
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue