Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save BuildKit state on client for cache support #138

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Expand Up @@ -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` |
| `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 |
| `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
> ```yaml
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Expand Up @@ -38,6 +38,9 @@ inputs:
config-inline:
description: 'Inline BuildKit config'
required: false
state-dir:
description: 'Path to BuildKit state volume directory'
required: false

outputs:
name:
Expand Down
4 changes: 2 additions & 2 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions src/buildx.ts
Expand Up @@ -3,11 +3,16 @@ import * as path from 'path';
import * as semver from 'semver';
import * as util from 'util';
import * as context from './context';
import * as docker from './docker';
import * as git from './git';
import * as github from './github';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
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 = {
name?: string;
Expand Down Expand Up @@ -81,6 +86,19 @@ export function satisfies(version: string, range: string): boolean {
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> {
return await exec
.getExecOutput(`docker`, ['buildx', 'inspect', name], {
Expand Down
4 changes: 3 additions & 1 deletion src/context.ts
Expand Up @@ -30,6 +30,7 @@ export interface Inputs {
endpoint: string;
config: string;
configInline: string;
stateDir: string;
}

export async function getInputs(): Promise<Inputs> {
Expand All @@ -42,7 +43,8 @@ export async function getInputs(): Promise<Inputs> {
use: core.getBooleanInput('use'),
endpoint: core.getInput('endpoint'),
config: core.getInput('config'),
configInline: core.getInput('config-inline')
configInline: core.getInput('config-inline'),
stateDir: core.getInput('state-dir')
};
}

Expand Down
71 changes: 71 additions & 0 deletions src/docker.ts
@@ -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 changes: 19 additions & 3 deletions src/main.ts
Expand Up @@ -16,8 +16,10 @@ async function run(): Promise<void> {
core.endGroup();

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)) {
core.startGroup(`Build and install buildx`);
await buildx.build(inputs.version, dockerConfigHome);
Expand All @@ -29,11 +31,15 @@ async function run(): Promise<void> {
}

const buildxVersion = await buildx.getVersion();
const builderName: string = inputs.driver == 'docker' ? 'default' : `builder-${uuid.v4()}`;
context.setOutput('name', builderName);
stateHelper.setBuilderName(builderName);

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`);
const createArgs: Array<string> = ['buildx', 'create', '--name', builderName, '--driver', inputs.driver];
if (buildx.satisfies(buildxVersion, '>=0.3.0')) {
Expand Down Expand Up @@ -114,8 +120,12 @@ async function cleanup(): Promise<void> {

if (stateHelper.builderName.length > 0) {
core.startGroup(`Removing builder`);
const rmArgs: Array<string> = ['buildx', 'rm', `${stateHelper.builderName}`];
if (stateHelper.stateDir.length > 0) {
rmArgs.push('--keep-state');
}
await exec
.getExecOutput('docker', ['buildx', 'rm', `${stateHelper.builderName}`], {
.getExecOutput('docker', rmArgs, {
ignoreReturnCode: true
})
.then(res => {
Expand All @@ -125,6 +135,12 @@ async function cleanup(): Promise<void> {
});
core.endGroup();
}

if (stateHelper.stateDir.length > 0) {
core.startGroup(`Saving state volume`);
await buildx.saveStateVolume(stateHelper.stateDir, stateHelper.containerName);
core.endGroup();
}
}

if (!stateHelper.IsPost) {
Expand Down
6 changes: 6 additions & 0 deletions src/state-helper.ts
Expand Up @@ -2,8 +2,10 @@ import * as core from '@actions/core';

export const IsPost = !!process.env['STATE_isPost'];
export const IsDebug = !!process.env['STATE_isDebug'];

export const builderName = process.env['STATE_builderName'] || '';
export const containerName = process.env['STATE_containerName'] || '';
export const stateDir = process.env['STATE_stateDir'] || '';

export function setDebug(debug: string) {
core.saveState('isDebug', debug);
Expand All @@ -17,6 +19,10 @@ export function setContainerName(containerName: string) {
core.saveState('containerName', containerName);
}

export function setStateDir(stateDir: string) {
core.saveState('stateDir', stateDir);
}

if (!IsPost) {
core.saveState('isPost', 'true');
}