Skip to content

Commit f3f23a5

Browse files
committed
Save BuildKit state on client for cache support
Signed-off-by: CrazyMax <[email protected]>
1 parent 74283ca commit f3f23a5

9 files changed

+127
-8
lines changed

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,11 @@ Following inputs can be used as `step.with` keys
197197
| `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` |
198198
| `config` | String | [BuildKit config file](https://github.com/docker/buildx/blob/master/docs/reference/buildx_create.md#config) |
199199
| `config-inline` | String | Same as `config` but inline |
200+
| `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 |
200201

201-
> `config` and `config-inline` are mutually exclusive.
202+
> :bulb: `config` and `config-inline` are mutually exclusive.
203+
204+
> :bulb: `state-dir` can only be used with the `docker-container` driver and a builder with a single node.
202205

203206
> `CSV` type must be a newline-delimited string
204207
> ```yaml

action.yml

+3
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+
state-dir:
42+
description: 'Path to BuildKit state volume directory'
43+
required: false
4144

4245
outputs:
4346
name:

dist/index.js

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/buildx.ts

+18
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import * as path from 'path';
33
import * as semver from 'semver';
44
import * as util from 'util';
55
import * as context from './context';
6+
import * as docker from './docker';
67
import * as git from './git';
78
import * as github from './github';
89
import * as core from '@actions/core';
910
import * as exec from '@actions/exec';
1011
import * as tc from '@actions/tool-cache';
12+
import child_process from 'child_process';
13+
14+
const uid = parseInt(child_process.execSync(`id -u`, {encoding: 'utf8'}).trim());
15+
const gid = parseInt(child_process.execSync(`id -g`, {encoding: 'utf8'}).trim());
1116

1217
export type Builder = {
1318
name?: string;
@@ -81,6 +86,19 @@ export function satisfies(version: string, range: string): boolean {
8186
return semver.satisfies(version, range) || /^[0-9a-f]{7}$/.exec(version) !== null;
8287
}
8388

89+
export async function createStateVolume(stateDir: string, nodeName: string): Promise<void> {
90+
return await docker.volumeCreate(stateDir, `${nodeName}_state`);
91+
}
92+
93+
export async function saveStateVolume(dir: string, nodeName: string): Promise<void> {
94+
const ctnid = await docker.containerCreate('busybox', `${nodeName}_state:/data`);
95+
const outdir = await docker.containerCopy(ctnid, `${ctnid}:/data`);
96+
await docker.volumeRemove(`${nodeName}_state`);
97+
fs.rmdirSync(dir, {recursive: true});
98+
fs.renameSync(outdir, dir);
99+
await docker.containerRemove(ctnid);
100+
}
101+
84102
export async function inspect(name: string): Promise<Builder> {
85103
return await exec
86104
.getExecOutput(`docker`, ['buildx', 'inspect', name], {

src/context.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface Inputs {
3030
endpoint: string;
3131
config: string;
3232
configInline: string;
33+
stateDir: string;
3334
}
3435

3536
export async function getInputs(): Promise<Inputs> {
@@ -42,7 +43,8 @@ export async function getInputs(): Promise<Inputs> {
4243
use: core.getBooleanInput('use'),
4344
endpoint: core.getInput('endpoint'),
4445
config: core.getInput('config'),
45-
configInline: core.getInput('config-inline')
46+
configInline: core.getInput('config-inline'),
47+
stateDir: core.getInput('state-dir')
4648
};
4749
}
4850

src/docker.ts

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import * as uuid from 'uuid';
4+
import * as context from './context';
5+
import * as exec from '@actions/exec';
6+
7+
export async function volumeCreate(dir: string, name: string): Promise<void> {
8+
if (!fs.existsSync(dir)) {
9+
fs.mkdirSync(dir, {recursive: true});
10+
}
11+
return await exec
12+
.getExecOutput(`docker`, ['volume', 'create', '--name', `${name}`, '--driver', 'local', '--opt', `o=bind,acl`, '--opt', 'type=none', '--opt', `device=${dir}`], {
13+
ignoreReturnCode: true
14+
})
15+
.then(res => {
16+
if (res.stderr.length > 0 && res.exitCode != 0) {
17+
throw new Error(res.stderr.trim());
18+
}
19+
});
20+
}
21+
22+
export async function volumeRemove(name: string): Promise<void> {
23+
return await exec
24+
.getExecOutput(`docker`, ['volume', 'rm', '-f', `${name}`], {
25+
ignoreReturnCode: true
26+
})
27+
.then(res => {
28+
if (res.stderr.length > 0 && res.exitCode != 0) {
29+
throw new Error(res.stderr.trim());
30+
}
31+
});
32+
}
33+
34+
export async function containerCreate(image: string, volume: string): Promise<string> {
35+
return await exec
36+
.getExecOutput(`docker`, ['create', '--rm', '-v', `${volume}`, `${image}`], {
37+
ignoreReturnCode: true
38+
})
39+
.then(res => {
40+
if (res.stderr.length > 0 && res.exitCode != 0) {
41+
throw new Error(res.stderr.trim());
42+
}
43+
return res.stdout.trim();
44+
});
45+
}
46+
47+
export async function containerCopy(ctnid: string, src: string): Promise<string> {
48+
const outdir = path.join(context.tmpDir(), `ctn-copy-${uuid.v4()}`).split(path.sep).join(path.posix.sep);
49+
return await exec
50+
.getExecOutput(`docker`, ['cp', '-a', `${src}`, `${outdir}`], {
51+
ignoreReturnCode: true
52+
})
53+
.then(res => {
54+
if (res.stderr.length > 0 && res.exitCode != 0) {
55+
throw new Error(res.stderr.trim());
56+
}
57+
return outdir;
58+
});
59+
}
60+
61+
export async function containerRemove(ctnid: string): Promise<void> {
62+
return await exec
63+
.getExecOutput(`docker`, ['rm', '-f', '-v', `${ctnid}`], {
64+
ignoreReturnCode: true
65+
})
66+
.then(res => {
67+
if (res.stderr.length > 0 && res.exitCode != 0) {
68+
throw new Error(res.stderr.trim());
69+
}
70+
});
71+
}

src/main.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ async function run(): Promise<void> {
1616
core.endGroup();
1717

1818
const inputs: context.Inputs = await context.getInputs();
19-
const dockerConfigHome: string = process.env.DOCKER_CONFIG || path.join(os.homedir(), '.docker');
19+
const builderName: string = inputs.driver == 'docker' ? 'default' : `builder-${uuid.v4()}`;
20+
stateHelper.setStateDir(inputs.stateDir);
2021

22+
const dockerConfigHome: string = process.env.DOCKER_CONFIG || path.join(os.homedir(), '.docker');
2123
if (util.isValidUrl(inputs.version)) {
2224
core.startGroup(`Build and install buildx`);
2325
await buildx.build(inputs.version, dockerConfigHome);
@@ -29,11 +31,15 @@ async function run(): Promise<void> {
2931
}
3032

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

3637
if (inputs.driver !== 'docker') {
38+
if (inputs.stateDir.length > 0) {
39+
await core.group(`Creating BuildKit state volume from ${inputs.stateDir}`, async () => {
40+
await buildx.createStateVolume(inputs.stateDir, `buildx_buildkit_${builderName}0`);
41+
});
42+
}
3743
core.startGroup(`Creating a new builder instance`);
3844
const createArgs: Array<string> = ['buildx', 'create', '--name', builderName, '--driver', inputs.driver];
3945
if (buildx.satisfies(buildxVersion, '>=0.3.0')) {
@@ -114,8 +120,12 @@ async function cleanup(): Promise<void> {
114120

115121
if (stateHelper.builderName.length > 0) {
116122
core.startGroup(`Removing builder`);
123+
const rmArgs: Array<string> = ['buildx', 'rm', `${stateHelper.builderName}`];
124+
if (stateHelper.stateDir.length > 0) {
125+
rmArgs.push('--keep-state');
126+
}
117127
await exec
118-
.getExecOutput('docker', ['buildx', 'rm', `${stateHelper.builderName}`], {
128+
.getExecOutput('docker', rmArgs, {
119129
ignoreReturnCode: true
120130
})
121131
.then(res => {
@@ -125,6 +135,12 @@ async function cleanup(): Promise<void> {
125135
});
126136
core.endGroup();
127137
}
138+
139+
if (stateHelper.stateDir.length > 0) {
140+
core.startGroup(`Saving state volume`);
141+
await buildx.saveStateVolume(stateHelper.stateDir, stateHelper.containerName);
142+
core.endGroup();
143+
}
128144
}
129145

130146
if (!stateHelper.IsPost) {

src/state-helper.ts

+6
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import * as core from '@actions/core';
22

33
export const IsPost = !!process.env['STATE_isPost'];
44
export const IsDebug = !!process.env['STATE_isDebug'];
5+
56
export const builderName = process.env['STATE_builderName'] || '';
67
export const containerName = process.env['STATE_containerName'] || '';
8+
export const stateDir = process.env['STATE_stateDir'] || '';
79

810
export function setDebug(debug: string) {
911
core.saveState('isDebug', debug);
@@ -17,6 +19,10 @@ export function setContainerName(containerName: string) {
1719
core.saveState('containerName', containerName);
1820
}
1921

22+
export function setStateDir(stateDir: string) {
23+
core.saveState('stateDir', stateDir);
24+
}
25+
2026
if (!IsPost) {
2127
core.saveState('isPost', 'true');
2228
}

0 commit comments

Comments
 (0)