Some improvements

- Use classes
- Split buildx/builder modules
- Additional tests

Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax 2023-01-23 10:07:14 +01:00
parent c7dfb4c220
commit aae4a2d7bc
No known key found for this signature in database
GPG Key ID: 3248E46B6BB8C7F7
17 changed files with 1045 additions and 712 deletions

18
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,18 @@
name: publish
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Publish
run: |
PUBLISH_VERSION=${} make publish

158
__tests__/builder.test.ts Normal file
View File

@ -0,0 +1,158 @@
import {describe, expect, it, test} from '@jest/globals';
import * as fs from 'fs';
import * as path from 'path';
import {Builder} from '../src/builder';
describe('inspect', () => {
it('valid', async () => {
const builder = new Builder();
const builderInfo = await builder.inspect('');
expect(builderInfo).not.toBeUndefined();
expect(builderInfo.name).not.toEqual('');
expect(builderInfo.driver).not.toEqual('');
expect(builderInfo.nodes).not.toEqual({});
}, 100000);
});
describe('parseInspect', () => {
// prettier-ignore
test.each([
[
'inspect1.txt',
{
"name": "builder-5cb467f7-0940-47e1-b94b-d51f54054d62",
"driver": "docker-container",
"nodes": [
{
"name": "builder-5cb467f7-0940-47e1-b94b-d51f54054d620",
"endpoint": "unix:///var/run/docker.sock",
"status": "running",
"buildkitdFlags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host",
"buildkitVersion": "v0.10.4",
"platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/amd64/v4,linux/arm64,linux/riscv64,linux/386,linux/arm/v7,linux/arm/v6"
}
]
}
],
[
'inspect2.txt',
{
"name": "builder-5f449644-ff29-48af-8344-abb0292d0673",
"driver": "docker-container",
"nodes": [
{
"name": "builder-5f449644-ff29-48af-8344-abb0292d06730",
"endpoint": "unix:///var/run/docker.sock",
"driverOpts": [
"image=moby/buildkit:latest"
],
"status": "running",
"buildkitdFlags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host",
"buildkitVersion": "v0.10.4",
"platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/amd64/v4,linux/386"
}
]
}
],
[
'inspect3.txt',
{
"name": "builder-9929e463-7954-4dc3-89cd-514cca29ff80",
"driver": "docker-container",
"nodes": [
{
"name": "builder-9929e463-7954-4dc3-89cd-514cca29ff800",
"endpoint": "unix:///var/run/docker.sock",
"driverOpts": [
"image=moby/buildkit:master",
"network=host"
],
"status": "running",
"buildkitdFlags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host",
"buildkitVersion": "3fab389",
"platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/amd64/v4,linux/386"
}
]
}
],
[
'inspect4.txt',
{
"name": "default",
"driver": "docker",
"nodes": [
{
"name": "default",
"endpoint": "default",
"status": "running",
"buildkitVersion": "20.10.17",
"platforms": "linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7,linux/arm/v6"
}
]
}
],
[
'inspect5.txt',
{
"name": "remote-builder",
"driver": "remote",
"nodes": [
{
"name": "aws_graviton2",
"endpoint": "tcp://1.23.45.67:1234",
"driverOpts": [
"cert=/home/user/.certs/aws_graviton2/cert.pem",
"key=/home/user/.certs/aws_graviton2/key.pem",
"cacert=/home/user/.certs/aws_graviton2/ca.pem"
],
"status": "running",
"platforms": "darwin/arm64,linux/arm64,linux/arm/v5,linux/arm/v6,linux/arm/v7,windows/arm64"
}
]
}
],
[
'inspect6.txt',
{
"nodes": [
{
"name": "builder-17cfff01-48d9-4c3d-9332-9992e308a5100",
"endpoint": "unix:///var/run/docker.sock",
"status": "running",
"buildkitdFlags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host",
"platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/386"
}
],
"name": "builder-17cfff01-48d9-4c3d-9332-9992e308a510",
"driver": "docker-container"
}
],
[
'inspect7.txt',
{
"name": "builder2",
"driver": "docker-container",
"lastActivity": new Date("2023-01-16T09:45:23.000Z"),
"nodes": [
{
"buildkitVersion": "v0.11.0",
"buildkitdFlags": "--debug --allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host",
"driverOpts": [
"BUILDKIT_STEP_LOG_MAX_SIZE=10485760",
"BUILDKIT_STEP_LOG_MAX_SPEED=10485760",
"JAEGER_TRACE=localhost:6831",
"image=moby/buildkit:latest",
"network=host"
],
"endpoint": "unix:///var/run/docker.sock",
"name": "builder20",
"platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6",
"status": "running"
}
]
}
]
])('given %p', async (inspectFile, expected) => {
expect(await Builder.parseInspect(fs.readFileSync(path.join(__dirname, 'fixtures', inspectFile)).toString())).toEqual(expected);
});
});

View File

@ -1,32 +1,40 @@
import {describe, expect, it, jest, test} from '@jest/globals';
import {afterEach, describe, expect, it, jest, test} from '@jest/globals';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as semver from 'semver';
import * as buildkit from '../src/buildkit';
import * as buildx from '../src/buildx';
import * as util from '../src/util';
import rimraf from 'rimraf';
const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-actions-toolkit-')).split(path.sep).join(path.posix.sep);
jest.spyOn(util, 'tmpDir').mockImplementation((): string => {
return tmpdir;
import {BuildKit} from '../src/buildkit';
import {Builder, BuilderInfo} from '../src/builder';
const tmpDir = path.join('/tmp/.docker-actions-toolkit-jest').split(path.sep).join(path.posix.sep);
const tmpName = path.join(tmpDir, '.tmpname-jest').split(path.sep).join(path.posix.sep);
jest.spyOn(BuildKit.prototype as any, 'tmpDir').mockImplementation((): string => {
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir, {recursive: true});
}
return tmpDir;
});
const tmpname = path.join(tmpdir, '.tmpname').split(path.sep).join(path.posix.sep);
jest.spyOn(util, 'tmpNameSync').mockImplementation((): string => {
return tmpname;
jest.spyOn(BuildKit.prototype as any, 'tmpName').mockImplementation((): string => {
return tmpName;
});
jest.spyOn(buildx, 'inspect').mockImplementation(async (): Promise<buildx.Builder> => {
afterEach(() => {
rimraf.sync(tmpDir);
});
jest.spyOn(Builder.prototype, 'inspect').mockImplementation(async (): Promise<BuilderInfo> => {
return {
name: 'builder2',
driver: 'docker-container',
'last-activity': new Date('2023-01-16 09:45:23 +0000 UTC'),
lastActivity: new Date('2023-01-16 09:45:23 +0000 UTC'),
nodes: [
{
buildkit: 'v0.11.0',
'buildkitd-flags': '--debug --allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host',
'driver-opts': ['BUILDKIT_STEP_LOG_MAX_SIZE=10485760', 'BUILDKIT_STEP_LOG_MAX_SPEED=10485760', 'JAEGER_TRACE=localhost:6831', 'image=moby/buildkit:latest', 'network=host'],
buildkitVersion: 'v0.11.0',
buildkitdFlags: '--debug --allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host',
driverOpts: ['BUILDKIT_STEP_LOG_MAX_SIZE=10485760', 'BUILDKIT_STEP_LOG_MAX_SPEED=10485760', 'JAEGER_TRACE=localhost:6831', 'image=moby/buildkit:latest', 'network=host'],
endpoint: 'unix:///var/run/docker.sock',
name: 'builder20',
platforms: 'linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6',
@ -38,6 +46,7 @@ jest.spyOn(buildx, 'inspect').mockImplementation(async (): Promise<buildx.Builde
describe('getVersion', () => {
it('valid', async () => {
const buildkit = new BuildKit();
const version = await buildkit.getVersion('builder2');
expect(semver.valid(version)).not.toBeNull();
});
@ -48,14 +57,15 @@ describe('satisfies', () => {
['builder2', '>=0.10.0', true],
['builder2', '>0.11.0', false]
])('given %p', async (builderName, range, expected) => {
expect(await buildkit.satisfies(builderName, range)).toBe(expected);
const buildkit = new BuildKit();
expect(await buildkit.versionSatisfies(builderName, range)).toBe(expected);
});
});
describe('getConfig', () => {
describe('generateConfig', () => {
test.each([
['debug = true', false, 'debug = true', false],
[`notfound.toml`, true, '', true],
['debug = true', false, 'debug = true', null],
[`notfound.toml`, true, '', new Error('config file notfound.toml not found')],
[
`${path.join(__dirname, 'fixtures', 'buildkitd.toml').split(path.sep).join(path.posix.sep)}`,
true,
@ -63,23 +73,23 @@ describe('getConfig', () => {
[registry."docker.io"]
mirrors = ["mirror.gcr.io"]
`,
false
null
]
])('given %p config', async (val, file, exValue, invalid) => {
])('given %p config', async (val, file, exValue, error: Error) => {
try {
const buildkit = new BuildKit();
let config: string;
if (file) {
config = await buildkit.getConfigFile(val);
config = buildkit.generateConfigFile(val);
} else {
config = await buildkit.getConfigInline(val);
config = buildkit.generateConfigInline(val);
}
expect(true).toBe(!invalid);
expect(config).toEqual(tmpname);
const configValue = fs.readFileSync(tmpname, 'utf-8');
expect(config).toEqual(tmpName);
const configValue = fs.readFileSync(tmpName, 'utf-8');
expect(configValue).toEqual(exValue);
} catch (err) {
} catch (e) {
// eslint-disable-next-line jest/no-conditional-expect
expect(true).toBe(invalid);
expect(e.message).toEqual(error?.message);
}
});
});

View File

@ -1,53 +1,61 @@
import {describe, expect, it, jest, test} from '@jest/globals';
import {afterEach, describe, expect, it, jest, test} from '@jest/globals';
import * as fs from 'fs';
import * as path from 'path';
import * as semver from 'semver';
import * as exec from '@actions/exec';
import * as buildx from '../src/buildx';
import * as util from '../src/util';
import rimraf from 'rimraf';
const tmpNameSync = path.join('/tmp/.docker-actions-toolkit-jest', '.tmpname-jest').split(path.sep).join(path.posix.sep);
const imageID = 'sha256:bfb45ab72e46908183546477a08f8867fc40cebadd00af54b071b097aed127a9';
import {Buildx} from '../src/buildx';
const tmpDir = path.join('/tmp/.docker-actions-toolkit-jest').split(path.sep).join(path.posix.sep);
const tmpName = path.join(tmpDir, '.tmpname-jest').split(path.sep).join(path.posix.sep);
const metadata = `{
"containerimage.config.digest": "sha256:059b68a595b22564a1cbc167af369349fdc2ecc1f7bc092c2235cbf601a795fd",
"containerimage.digest": "sha256:b09b9482c72371486bb2c1d2c2a2633ed1d0b8389e12c8d52b9e052725c0c83c"
}`;
jest.spyOn(util, 'tmpDir').mockImplementation((): string => {
const tmpDir = path.join('/tmp/.docker-actions-toolkit-jest').split(path.sep).join(path.posix.sep);
jest.spyOn(Buildx.prototype as any, 'tmpDir').mockImplementation((): string => {
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir, {recursive: true});
}
return tmpDir;
});
jest.spyOn(util, 'tmpNameSync').mockImplementation((): string => {
return tmpNameSync;
afterEach(() => {
rimraf.sync(tmpDir);
});
describe('getImageID', () => {
jest.spyOn(Buildx.prototype as any, 'tmpName').mockImplementation((): string => {
return tmpName;
});
describe('getBuildImageID', () => {
it('matches', async () => {
const imageIDFile = await buildx.getImageIDFile();
const buildx = new Buildx();
const imageID = 'sha256:bfb45ab72e46908183546477a08f8867fc40cebadd00af54b071b097aed127a9';
const imageIDFile = buildx.getBuildImageIDFilePath();
await fs.writeFileSync(imageIDFile, imageID);
const expected = await buildx.getImageID();
const expected = buildx.getBuildImageID();
expect(expected).toEqual(imageID);
});
});
describe('getMetadata', () => {
describe('getBuildMetadata', () => {
it('matches', async () => {
const metadataFile = await buildx.getMetadataFile();
const buildx = new Buildx();
const metadataFile = buildx.getBuildMetadataFilePath();
await fs.writeFileSync(metadataFile, metadata);
const expected = await buildx.getMetadata();
const expected = buildx.getBuildMetadata();
expect(expected).toEqual(metadata);
});
});
describe('getDigest', () => {
it('matches', async () => {
const metadataFile = await buildx.getMetadataFile();
const buildx = new Buildx();
const metadataFile = buildx.getBuildMetadataFilePath();
await fs.writeFileSync(metadataFile, metadata);
const expected = await buildx.getDigest(metadata);
const expected = buildx.getDigest();
expect(expected).toEqual('sha256:b09b9482c72371486bb2c1d2c2a2633ed1d0b8389e12c8d52b9e052725c0c83c');
});
});
@ -62,14 +70,15 @@ describe('hasLocalOrTarExporter', () => {
[['"type=tar","dest=/tmp/image.tar"'], true],
[['" type= local" , dest=./release-out'], true],
[['.'], true]
])('given %p returns %p', async (outputs: Array<string>, expected: boolean) => {
expect(buildx.hasLocalOrTarExporter(outputs)).toEqual(expected);
])('given %p returns %p', async (exporters: Array<string>, expected: boolean) => {
expect(Buildx.hasLocalExporter(exporters) || Buildx.hasTarExporter(exporters)).toEqual(expected);
});
});
describe('isAvailable', () => {
it('docker cli', async () => {
const execSpy = jest.spyOn(exec, 'getExecOutput');
const buildx = new Buildx();
await buildx.isAvailable();
// eslint-disable-next-line jest/no-standalone-expect
expect(execSpy).toHaveBeenCalledWith(`docker`, ['buildx'], {
@ -79,7 +88,10 @@ describe('isAvailable', () => {
});
it('standalone', async () => {
const execSpy = jest.spyOn(exec, 'getExecOutput');
await buildx.isAvailable(true);
const buildx = new Buildx({
standalone: true
});
await buildx.isAvailable();
// eslint-disable-next-line jest/no-standalone-expect
expect(execSpy).toHaveBeenCalledWith(`buildx`, [], {
silent: true,
@ -88,161 +100,9 @@ describe('isAvailable', () => {
});
});
describe('inspect', () => {
it('valid', async () => {
const builder = await buildx.inspect('');
expect(builder).not.toBeUndefined();
expect(builder.name).not.toEqual('');
expect(builder.driver).not.toEqual('');
expect(builder.nodes).not.toEqual({});
}, 100000);
});
describe('parseInspect', () => {
// prettier-ignore
test.each([
[
'inspect1.txt',
{
"name": "builder-5cb467f7-0940-47e1-b94b-d51f54054d62",
"driver": "docker-container",
"nodes": [
{
"name": "builder-5cb467f7-0940-47e1-b94b-d51f54054d620",
"endpoint": "unix:///var/run/docker.sock",
"status": "running",
"buildkitd-flags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host",
"buildkit": "v0.10.4",
"platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/amd64/v4,linux/arm64,linux/riscv64,linux/386,linux/arm/v7,linux/arm/v6"
}
]
}
],
[
'inspect2.txt',
{
"name": "builder-5f449644-ff29-48af-8344-abb0292d0673",
"driver": "docker-container",
"nodes": [
{
"name": "builder-5f449644-ff29-48af-8344-abb0292d06730",
"endpoint": "unix:///var/run/docker.sock",
"driver-opts": [
"image=moby/buildkit:latest"
],
"status": "running",
"buildkitd-flags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host",
"buildkit": "v0.10.4",
"platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/amd64/v4,linux/386"
}
]
}
],
[
'inspect3.txt',
{
"name": "builder-9929e463-7954-4dc3-89cd-514cca29ff80",
"driver": "docker-container",
"nodes": [
{
"name": "builder-9929e463-7954-4dc3-89cd-514cca29ff800",
"endpoint": "unix:///var/run/docker.sock",
"driver-opts": [
"image=moby/buildkit:master",
"network=host"
],
"status": "running",
"buildkitd-flags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host",
"buildkit": "3fab389",
"platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/amd64/v4,linux/386"
}
]
}
],
[
'inspect4.txt',
{
"name": "default",
"driver": "docker",
"nodes": [
{
"name": "default",
"endpoint": "default",
"status": "running",
"buildkit": "20.10.17",
"platforms": "linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/arm/v7,linux/arm/v6"
}
]
}
],
[
'inspect5.txt',
{
"name": "remote-builder",
"driver": "remote",
"nodes": [
{
"name": "aws_graviton2",
"endpoint": "tcp://1.23.45.67:1234",
"driver-opts": [
"cert=/home/user/.certs/aws_graviton2/cert.pem",
"key=/home/user/.certs/aws_graviton2/key.pem",
"cacert=/home/user/.certs/aws_graviton2/ca.pem"
],
"status": "running",
"platforms": "darwin/arm64,linux/arm64,linux/arm/v5,linux/arm/v6,linux/arm/v7,windows/arm64"
}
]
}
],
[
'inspect6.txt',
{
"nodes": [
{
"name": "builder-17cfff01-48d9-4c3d-9332-9992e308a5100",
"endpoint": "unix:///var/run/docker.sock",
"status": "running",
"buildkitd-flags": "--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host",
"platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/386"
}
],
"name": "builder-17cfff01-48d9-4c3d-9332-9992e308a510",
"driver": "docker-container"
}
],
[
'inspect7.txt',
{
"name": "builder2",
"driver": "docker-container",
"last-activity": new Date("2023-01-16T09:45:23.000Z"),
"nodes": [
{
"buildkit": "v0.11.0",
"buildkitd-flags": "--debug --allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host",
"driver-opts": [
"BUILDKIT_STEP_LOG_MAX_SIZE=10485760",
"BUILDKIT_STEP_LOG_MAX_SPEED=10485760",
"JAEGER_TRACE=localhost:6831",
"image=moby/buildkit:latest",
"network=host"
],
"endpoint": "unix:///var/run/docker.sock",
"name": "builder20",
"platforms": "linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6",
"status": "running"
}
]
}
]
])('given %p', async (inspectFile, expected) => {
expect(await buildx.parseInspect(fs.readFileSync(path.join(__dirname, 'fixtures', inspectFile)).toString())).toEqual(expected);
});
});
describe('getVersion', () => {
it('valid', async () => {
const buildx = new Buildx();
const version = await buildx.getVersion();
expect(semver.valid(version)).not.toBeNull();
});
@ -255,44 +115,54 @@ describe('parseVersion', () => {
['github.com/docker/buildx v0.4.2 fb7b670b764764dc4716df3eba07ffdae4cc47b2', '0.4.2'],
['github.com/docker/buildx f117971 f11797113e5a9b86bd976329c5dbb8a8bfdfadfa', 'f117971']
])('given %p', async (stdout, expected) => {
expect(buildx.parseVersion(stdout)).toEqual(expected);
expect(Buildx.parseVersion(stdout)).toEqual(expected);
});
});
describe('satisfies', () => {
describe('versionSatisfies', () => {
test.each([
['0.4.1', '>=0.3.2', true],
['bda4882a65349ca359216b135896bddc1d92461c', '>0.1.0', false],
['f117971', '>0.6.0', true]
])('given %p', async (version, range, expected) => {
expect(buildx.satisfies(version, range)).toBe(expected);
expect(Buildx.versionSatisfies(version, range)).toBe(expected);
});
});
describe('getSecret', () => {
describe('generateBuildSecret', () => {
test.each([
['A_SECRET=abcdef0123456789', false, 'A_SECRET', 'abcdef0123456789', false],
['GIT_AUTH_TOKEN=abcdefghijklmno=0123456789', false, 'GIT_AUTH_TOKEN', 'abcdefghijklmno=0123456789', false],
['MY_KEY=c3RyaW5nLXdpdGgtZXF1YWxzCg==', false, 'MY_KEY', 'c3RyaW5nLXdpdGgtZXF1YWxzCg==', false],
['aaaaaaaa', false, '', '', true],
['aaaaaaaa=', false, '', '', true],
['=bbbbbbb', false, '', '', true],
[`foo=${path.join(__dirname, 'fixtures', 'secret.txt').split(path.sep).join(path.posix.sep)}`, true, 'foo', 'bar', false],
[`notfound=secret`, true, '', '', true]
])('given %p key and %p secret', async (kvp, file, exKey, exValue, invalid) => {
['A_SECRET=abcdef0123456789', false, 'A_SECRET', 'abcdef0123456789', null],
['GIT_AUTH_TOKEN=abcdefghijklmno=0123456789', false, 'GIT_AUTH_TOKEN', 'abcdefghijklmno=0123456789', null],
['MY_KEY=c3RyaW5nLXdpdGgtZXF1YWxzCg==', false, 'MY_KEY', 'c3RyaW5nLXdpdGgtZXF1YWxzCg==', null],
['aaaaaaaa', false, '', '', new Error('aaaaaaaa is not a valid secret')],
['aaaaaaaa=', false, '', '', new Error('aaaaaaaa= is not a valid secret')],
['=bbbbbbb', false, '', '', new Error('=bbbbbbb is not a valid secret')],
[`foo=${path.join(__dirname, 'fixtures', 'secret.txt').split(path.sep).join(path.posix.sep)}`, true, 'foo', 'bar', null],
[`notfound=secret`, true, '', '', new Error('secret file secret not found')]
])('given %p key and %p secret', async (kvp: string, file: boolean, exKey: string, exValue: string, error: Error) => {
try {
const buildx = new Buildx();
let secret: string;
if (file) {
secret = await buildx.getSecretFile(kvp);
secret = buildx.generateBuildSecretFile(kvp);
} else {
secret = await buildx.getSecretString(kvp);
secret = buildx.generateBuildSecretString(kvp);
}
expect(true).toBe(!invalid);
expect(secret).toEqual(`id=${exKey},src=${tmpNameSync}`);
expect(fs.readFileSync(tmpNameSync, 'utf-8')).toEqual(exValue);
} catch (err) {
expect(secret).toEqual(`id=${exKey},src=${tmpName}`);
expect(fs.readFileSync(tmpName, 'utf-8')).toEqual(exValue);
} catch (e) {
// eslint-disable-next-line jest/no-conditional-expect
expect(true).toBe(invalid);
expect(e.message).toEqual(error?.message);
}
});
});
describe('hasGitAuthTokenSecret', () => {
// prettier-ignore
test.each([
[['A_SECRET=abcdef0123456789'], false],
[['GIT_AUTH_TOKEN=abcdefghijklmno=0123456789'], true],
])('given %p secret', async (kvp: Array<string>, expected: boolean) => {
expect(Buildx.hasGitAuthTokenSecret(kvp)).toBe(expected);
});
});

View File

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

View File

@ -0,0 +1,195 @@
{
"after": "860c1904a1ce19322e91ac35af1ab07466440c37",
"base_ref": null,
"before": "5f3331d7f7044c18ca9f12c77d961c4d7cf3276a",
"commits": [
{
"author": {
"email": "crazy-max@users.noreply.github.com",
"name": "CrazyMax",
"username": "crazy-max"
},
"committer": {
"email": "crazy-max@users.noreply.github.com",
"name": "CrazyMax",
"username": "crazy-max"
},
"distinct": true,
"id": "860c1904a1ce19322e91ac35af1ab07466440c37",
"message": "hello dev",
"timestamp": "2022-04-19T11:27:24+02:00",
"tree_id": "d2c60af597e863787d2d27f569e30495b0b92820",
"url": "https://github.com/docker/test-docker-action/commit/860c1904a1ce19322e91ac35af1ab07466440c37"
}
],
"compare": "https://github.com/docker/test-docker-action/compare/5f3331d7f704...860c1904a1ce",
"created": false,
"deleted": false,
"forced": false,
"head_commit": {
"author": {
"email": "crazy-max@users.noreply.github.com",
"name": "CrazyMax",
"username": "crazy-max"
},
"committer": {
"email": "crazy-max@users.noreply.github.com",
"name": "CrazyMax",
"username": "crazy-max"
},
"distinct": true,
"id": "860c1904a1ce19322e91ac35af1ab07466440c37",
"message": "hello dev",
"timestamp": "2022-04-19T11:27:24+02:00",
"tree_id": "d2c60af597e863787d2d27f569e30495b0b92820",
"url": "https://github.com/docker/test-docker-action/commit/860c1904a1ce19322e91ac35af1ab07466440c37"
},
"organization": {
"avatar_url": "https://avatars.githubusercontent.com/u/5429470?v=4",
"description": "Docker helps developers bring their ideas to life by conquering the complexity of app development.",
"events_url": "https://api.github.com/orgs/docker/events",
"hooks_url": "https://api.github.com/orgs/docker/hooks",
"id": 5429470,
"issues_url": "https://api.github.com/orgs/docker/issues",
"login": "docker",
"members_url": "https://api.github.com/orgs/docker/members{/member}",
"node_id": "MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
"public_members_url": "https://api.github.com/orgs/docker/public_members{/member}",
"repos_url": "https://api.github.com/orgs/docker/repos",
"url": "https://api.github.com/orgs/docker"
},
"pusher": {
"email": "github@crazymax.dev",
"name": "crazy-max"
},
"ref": "refs/heads/dev",
"repository": {
"allow_forking": true,
"archive_url": "https://api.github.com/repos/docker/test-docker-action/{archive_format}{/ref}",
"archived": false,
"assignees_url": "https://api.github.com/repos/docker/test-docker-action/assignees{/user}",
"blobs_url": "https://api.github.com/repos/docker/test-docker-action/git/blobs{/sha}",
"branches_url": "https://api.github.com/repos/docker/test-docker-action/branches{/branch}",
"clone_url": "https://github.com/docker/test-docker-action.git",
"collaborators_url": "https://api.github.com/repos/docker/test-docker-action/collaborators{/collaborator}",
"comments_url": "https://api.github.com/repos/docker/test-docker-action/comments{/number}",
"commits_url": "https://api.github.com/repos/docker/test-docker-action/commits{/sha}",
"compare_url": "https://api.github.com/repos/docker/test-docker-action/compare/{base}...{head}",
"contents_url": "https://api.github.com/repos/docker/test-docker-action/contents/{+path}",
"contributors_url": "https://api.github.com/repos/docker/test-docker-action/contributors",
"created_at": 1596792180,
"default_branch": "master",
"deployments_url": "https://api.github.com/repos/docker/test-docker-action/deployments",
"description": "Test \"Docker\" Actions",
"disabled": false,
"downloads_url": "https://api.github.com/repos/docker/test-docker-action/downloads",
"events_url": "https://api.github.com/repos/docker/test-docker-action/events",
"fork": false,
"forks": 1,
"forks_count": 1,
"forks_url": "https://api.github.com/repos/docker/test-docker-action/forks",
"full_name": "docker/test-docker-action",
"git_commits_url": "https://api.github.com/repos/docker/test-docker-action/git/commits{/sha}",
"git_refs_url": "https://api.github.com/repos/docker/test-docker-action/git/refs{/sha}",
"git_tags_url": "https://api.github.com/repos/docker/test-docker-action/git/tags{/sha}",
"git_url": "git://github.com/docker/test-docker-action.git",
"has_downloads": true,
"has_issues": true,
"has_pages": false,
"has_projects": true,
"has_wiki": true,
"homepage": "",
"hooks_url": "https://api.github.com/repos/docker/test-docker-action/hooks",
"html_url": "https://github.com/docker/test-docker-action",
"id": 285789493,
"is_template": false,
"issue_comment_url": "https://api.github.com/repos/docker/test-docker-action/issues/comments{/number}",
"issue_events_url": "https://api.github.com/repos/docker/test-docker-action/issues/events{/number}",
"issues_url": "https://api.github.com/repos/docker/test-docker-action/issues{/number}",
"keys_url": "https://api.github.com/repos/docker/test-docker-action/keys{/key_id}",
"labels_url": "https://api.github.com/repos/docker/test-docker-action/labels{/name}",
"language": "JavaScript",
"languages_url": "https://api.github.com/repos/docker/test-docker-action/languages",
"license": {
"key": "mit",
"name": "MIT License",
"node_id": "MDc6TGljZW5zZTEz",
"spdx_id": "MIT",
"url": "https://api.github.com/licenses/mit"
},
"master_branch": "master",
"merges_url": "https://api.github.com/repos/docker/test-docker-action/merges",
"milestones_url": "https://api.github.com/repos/docker/test-docker-action/milestones{/number}",
"mirror_url": null,
"name": "test-docker-action",
"node_id": "MDEwOlJlcG9zaXRvcnkyODU3ODk0OTM=",
"notifications_url": "https://api.github.com/repos/docker/test-docker-action/notifications{?since,all,participating}",
"open_issues": 6,
"open_issues_count": 6,
"organization": "docker",
"owner": {
"avatar_url": "https://avatars.githubusercontent.com/u/5429470?v=4",
"email": "info@docker.com",
"events_url": "https://api.github.com/users/docker/events{/privacy}",
"followers_url": "https://api.github.com/users/docker/followers",
"following_url": "https://api.github.com/users/docker/following{/other_user}",
"gists_url": "https://api.github.com/users/docker/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/docker",
"id": 5429470,
"login": "docker",
"name": "docker",
"node_id": "MDEyOk9yZ2FuaXphdGlvbjU0Mjk0NzA=",
"organizations_url": "https://api.github.com/users/docker/orgs",
"received_events_url": "https://api.github.com/users/docker/received_events",
"repos_url": "https://api.github.com/users/docker/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/docker/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/docker/subscriptions",
"type": "Organization",
"url": "https://api.github.com/users/docker"
},
"private": true,
"pulls_url": "https://api.github.com/repos/docker/test-docker-action/pulls{/number}",
"pushed_at": 1650360446,
"releases_url": "https://api.github.com/repos/docker/test-docker-action/releases{/id}",
"size": 796,
"ssh_url": "git@github.com:docker/test-docker-action.git",
"stargazers": 0,
"stargazers_count": 0,
"stargazers_url": "https://api.github.com/repos/docker/test-docker-action/stargazers",
"statuses_url": "https://api.github.com/repos/docker/test-docker-action/statuses/{sha}",
"subscribers_url": "https://api.github.com/repos/docker/test-docker-action/subscribers",
"subscription_url": "https://api.github.com/repos/docker/test-docker-action/subscription",
"svn_url": "https://github.com/docker/test-docker-action",
"tags_url": "https://api.github.com/repos/docker/test-docker-action/tags",
"teams_url": "https://api.github.com/repos/docker/test-docker-action/teams",
"topics": [],
"trees_url": "https://api.github.com/repos/docker/test-docker-action/git/trees{/sha}",
"updated_at": "2022-04-19T09:05:09Z",
"url": "https://github.com/docker/test-docker-action",
"visibility": "private",
"watchers": 0,
"watchers_count": 0
},
"sender": {
"avatar_url": "https://avatars.githubusercontent.com/u/1951866?v=4",
"events_url": "https://api.github.com/users/crazy-max/events{/privacy}",
"followers_url": "https://api.github.com/users/crazy-max/followers",
"following_url": "https://api.github.com/users/crazy-max/following{/other_user}",
"gists_url": "https://api.github.com/users/crazy-max/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/crazy-max",
"id": 1951866,
"login": "crazy-max",
"node_id": "MDQ6VXNlcjE5NTE4NjY=",
"organizations_url": "https://api.github.com/users/crazy-max/orgs",
"received_events_url": "https://api.github.com/users/crazy-max/received_events",
"repos_url": "https://api.github.com/users/crazy-max/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/crazy-max/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/crazy-max/subscriptions",
"type": "User",
"url": "https://api.github.com/users/crazy-max"
}
}

View File

@ -1,14 +1,41 @@
import {describe, expect, jest, it} from '@jest/globals';
import * as github from '../src/github';
import {Context} from '@actions/github/lib/context';
import {GitHub, Payload, ReposGetResponseData} from '../src/github';
jest.spyOn(GitHub.prototype, 'context').mockImplementation((): Context => {
return new Context();
});
import * as repoFixture from './fixtures/repo.json';
jest.spyOn(github, 'repo').mockImplementation((): Promise<github.ReposGetResponseData> => {
return <Promise<github.ReposGetResponseData>>(repoFixture as unknown);
jest.spyOn(GitHub.prototype, 'repo').mockImplementation((): Promise<ReposGetResponseData> => {
return <Promise<ReposGetResponseData>>(repoFixture as unknown);
});
import * as payloadFixture from './fixtures/github-payload.json';
jest.spyOn(GitHub.prototype as any, 'payload').mockImplementation((): Promise<Payload> => {
return <Promise<Payload>>(payloadFixture as unknown);
});
jest.spyOn(GitHub.prototype as any, 'ref').mockImplementation((): string => {
return 'refs/heads/master';
});
describe('gitContext', () => {
it('returns refs/heads/master', async () => {
expect(GitHub.getInstance().gitContext()).toEqual('https://github.com/docker/test-docker-action.git#refs/heads/master');
});
});
describe('repo', () => {
it('returns GitHub repository', async () => {
const repo = await github.repo(process.env.GITHUB_TOKEN || '');
expect(repo.name).not.toBeNull();
const repo = await GitHub.getInstance().repo(process.env.GITHUB_TOKEN || '');
expect(repo.name).toEqual('Hello-World');
});
});
describe('fromPayload', () => {
it('returns repository name from payload', async () => {
const repoName = await GitHub.getInstance().fromPayload('repository.name');
expect(repoName).toEqual('test-docker-action');
});
});

View File

@ -1,76 +1,61 @@
import {describe, expect, it, jest, test} from '@jest/globals';
import {describe, expect, it, test} from '@jest/globals';
import * as fs from 'fs';
import * as path from 'path';
import * as util from '../src/util';
jest.spyOn(util, 'defaultContext').mockImplementation((): string => {
return 'https://github.com/docker/actions-toolkit.git#refs/heads/test-jest';
});
jest.spyOn(util, 'tmpDir').mockImplementation((): string => {
const tmpDir = path.join('/tmp/.docker-build-push-jest').split(path.sep).join(path.posix.sep);
if (!fs.existsSync(tmpDir)) {
fs.mkdirSync(tmpDir, {recursive: true});
}
return tmpDir;
});
jest.spyOn(util, 'tmpNameSync').mockImplementation((): string => {
return path.join('/tmp/.docker-build-push-jest', '.tmpname-jest').split(path.sep).join(path.posix.sep);
});
import {Util} from '../src/util';
describe('getInputList', () => {
it('single line correctly', async () => {
await setInput('foo', 'bar');
const res = util.getInputList('foo');
const res = Util.getInputList('foo');
expect(res).toEqual(['bar']);
});
it('multiline correctly', async () => {
setInput('foo', 'bar\nbaz');
const res = util.getInputList('foo');
const res = Util.getInputList('foo');
expect(res).toEqual(['bar', 'baz']);
});
it('empty lines correctly', async () => {
setInput('foo', 'bar\n\nbaz');
const res = util.getInputList('foo');
const res = Util.getInputList('foo');
expect(res).toEqual(['bar', 'baz']);
});
it('comma correctly', async () => {
setInput('foo', 'bar,baz');
const res = util.getInputList('foo');
const res = Util.getInputList('foo');
expect(res).toEqual(['bar', 'baz']);
});
it('empty result correctly', async () => {
setInput('foo', 'bar,baz,');
const res = util.getInputList('foo');
const res = Util.getInputList('foo');
expect(res).toEqual(['bar', 'baz']);
});
it('different new lines correctly', async () => {
setInput('foo', 'bar\r\nbaz');
const res = util.getInputList('foo');
const res = Util.getInputList('foo');
expect(res).toEqual(['bar', 'baz']);
});
it('different new lines and comma correctly', async () => {
setInput('foo', 'bar\r\nbaz,bat');
const res = util.getInputList('foo');
const res = Util.getInputList('foo');
expect(res).toEqual(['bar', 'baz', 'bat']);
});
it('multiline and ignoring comma correctly', async () => {
setInput('cache-from', 'user/app:cache\ntype=local,src=path/to/dir');
const res = util.getInputList('cache-from', true);
const res = Util.getInputList('cache-from', true);
expect(res).toEqual(['user/app:cache', 'type=local,src=path/to/dir']);
});
it('different new lines and ignoring comma correctly', async () => {
setInput('cache-from', 'user/app:cache\r\ntype=local,src=path/to/dir');
const res = util.getInputList('cache-from', true);
const res = Util.getInputList('cache-from', true);
expect(res).toEqual(['user/app:cache', 'type=local,src=path/to/dir']);
});
@ -83,7 +68,7 @@ bbbbbbb
ccccccccc"
FOO=bar`
);
const res = util.getInputList('secrets', true);
const res = Util.getInputList('secrets', true);
expect(res).toEqual([
'GIT_AUTH_TOKEN=abcdefgh,ijklmno=0123456789',
`MYSECRET=aaaaaaaa
@ -106,7 +91,7 @@ FOO=bar
bbbb
ccc"`
);
const res = util.getInputList('secrets', true);
const res = Util.getInputList('secrets', true);
expect(res).toEqual([
'GIT_AUTH_TOKEN=abcdefgh,ijklmno=0123456789',
`MYSECRET=aaaaaaaa
@ -129,7 +114,7 @@ bbbbbbb
ccccccccc
FOO=bar`
);
const res = util.getInputList('secrets', true);
const res = Util.getInputList('secrets', true);
expect(res).toEqual(['GIT_AUTH_TOKEN=abcdefgh,ijklmno=0123456789', 'MYSECRET=aaaaaaaa', 'bbbbbbb', 'ccccccccc', 'FOO=bar']);
});
@ -140,7 +125,7 @@ FOO=bar`
`"GPG_KEY=${pgp}"
FOO=bar`
);
const res = util.getInputList('secrets', true);
const res = Util.getInputList('secrets', true);
expect(res).toEqual([`GPG_KEY=${pgp}`, 'FOO=bar']);
});
@ -153,7 +138,7 @@ bbbb""bbb
ccccccccc"
FOO=bar`
);
const res = util.getInputList('secrets', true);
const res = Util.getInputList('secrets', true);
expect(res).toEqual([
'GIT_AUTH_TOKEN=abcdefgh,ijklmno=0123456789',
`MYSECRET=aaaaaaaa
@ -169,7 +154,7 @@ describe('asyncForEach', () => {
const testValues = [1, 2, 3, 4, 5];
const results: number[] = [];
await util.asyncForEach(testValues, async value => {
await Util.asyncForEach(testValues, async value => {
results.push(value);
});
@ -183,7 +168,7 @@ describe('isValidUrl', () => {
['https://github.com/docker/buildx.git#refs/pull/648/head', true],
['v0.4.1', false]
])('given %p', async (url, expected) => {
expect(util.isValidUrl(url)).toEqual(expected);
expect(Util.isValidUrl(url)).toEqual(expected);
});
});

View File

@ -1,4 +1,5 @@
process.env = Object.assign({}, process.env, {
GITHUB_REPOSITORY: 'docker/test-docker-action',
RUNNER_TEMP: '/tmp/github_runner',
RUNNER_TOOL_CACHE: '/tmp/github_tool_cache'
}) as {

View File

@ -58,6 +58,7 @@
"eslint-plugin-prettier": "^4.0.0",
"jest": "^27.2.5",
"prettier": "^2.3.1",
"rimraf": "^4.1.1",
"ts-jest": "^27.1.2",
"ts-node": "^10.7.0",
"typescript": "^4.4.4"

124
src/builder.ts Normal file
View File

@ -0,0 +1,124 @@
import * as exec from '@actions/exec';
import {Buildx} from './buildx';
export interface BuilderInfo {
name?: string;
driver?: string;
lastActivity?: Date;
nodes: NodeInfo[];
}
export interface NodeInfo {
name?: string;
endpoint?: string;
driverOpts?: Array<string>;
status?: string;
buildkitdFlags?: string;
buildkitVersion?: string;
platforms?: string;
}
export interface BuilderOpts {
buildx?: Buildx;
}
export class Builder {
private buildx: Buildx;
constructor(opts?: BuilderOpts) {
this.buildx = opts?.buildx || new Buildx();
}
public async inspect(name: string): Promise<BuilderInfo> {
const cmd = this.buildx.getCommand(['inspect', name]);
return await exec
.getExecOutput(cmd.command, cmd.args, {
ignoreReturnCode: true,
silent: true
})
.then(res => {
if (res.stderr.length > 0 && res.exitCode != 0) {
throw new Error(res.stderr.trim());
}
return Builder.parseInspect(res.stdout);
});
}
public static parseInspect(data: string): BuilderInfo {
const builder: BuilderInfo = {
nodes: []
};
let node: NodeInfo = {};
for (const line of data.trim().split(`\n`)) {
const [key, ...rest] = line.split(':');
const value = rest.map(v => v.trim()).join(':');
if (key.length == 0 || value.length == 0) {
continue;
}
switch (key.toLowerCase()) {
case 'name': {
if (builder.name == undefined) {
builder.name = value;
} else {
if (Object.keys(node).length > 0) {
builder.nodes.push(node);
node = {};
}
node.name = value;
}
break;
}
case 'driver': {
builder.driver = value;
break;
}
case 'last activity': {
builder.lastActivity = new Date(value);
break;
}
case 'endpoint': {
node.endpoint = value;
break;
}
case 'driver options': {
node.driverOpts = (value.match(/(\w+)="([^"]*)"/g) || []).map(v => v.replace(/^(.*)="(.*)"$/g, '$1=$2'));
break;
}
case 'status': {
node.status = value;
break;
}
case 'flags': {
node.buildkitdFlags = value;
break;
}
case 'buildkit': {
node.buildkitVersion = value;
break;
}
case 'platforms': {
let platforms: Array<string> = [];
// if a preferred platform is being set then use only these
// https://docs.docker.com/engine/reference/commandline/buildx_inspect/#get-information-about-a-builder-instance
if (value.includes('*')) {
for (const platform of value.split(', ')) {
if (platform.includes('*')) {
platforms.push(platform.replace('*', ''));
}
}
} else {
// otherwise set all platforms available
platforms = value.split(', ');
}
node.platforms = platforms.join(',');
break;
}
}
}
if (Object.keys(node).length > 0) {
builder.nodes.push(node);
}
return builder;
}
}

View File

@ -1,96 +1,125 @@
import * as fs from 'fs';
import * as semver from 'semver';
import fs from 'fs';
import os from 'os';
import path from 'path';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import * as buildx from './buildx';
import * as util from './util';
import * as semver from 'semver';
import * as tmp from 'tmp';
export async function getConfigInline(s: string): Promise<string> {
return getConfig(s, false);
import {Buildx} from './buildx';
import {Builder, BuilderInfo} from './builder';
export interface BuildKitOpts {
buildx?: Buildx;
}
export async function getConfigFile(s: string): Promise<string> {
return getConfig(s, true);
}
export class BuildKit {
private buildx: Buildx;
private containerNamePrefix = 'buildx_buildkit_';
private tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-actions-toolkit-')).split(path.sep).join(path.posix.sep);
export async function getConfig(s: string, file: boolean): Promise<string> {
if (file) {
if (!fs.existsSync(s)) {
throw new Error(`config file ${s} not found`);
constructor(opts?: BuildKitOpts) {
this.buildx = opts?.buildx || new Buildx();
}
private async getBuilderInfo(name: string): Promise<BuilderInfo> {
const builder = new Builder({buildx: this.buildx});
return builder.inspect(name);
}
public async getVersion(builderName: string): Promise<string | undefined> {
const builderInfo = await this.getBuilderInfo(builderName);
if (builderInfo.nodes.length == 0) {
// a builder always have on node, should not happen.
return undefined;
}
s = fs.readFileSync(s, {encoding: 'utf-8'});
}
const configFile = util.tmpNameSync({
tmpdir: util.tmpDir()
});
fs.writeFileSync(configFile, s);
return configFile;
}
export async function getVersion(builderName: string, standalone?: boolean): Promise<string | undefined> {
const builder = await buildx.inspect(builderName, standalone);
if (builder.nodes.length == 0) {
// a builder always have on node, should not happen.
return undefined;
}
// TODO: get version for all nodes
const node = builder.nodes[0];
if (!node.buildkit && node.name) {
try {
return await getVersionWithinImage(node.name);
} catch (e) {
core.warning(e);
}
}
return node.buildkit;
}
async function getVersionWithinImage(nodeName: string): Promise<string> {
return exec
.getExecOutput(`docker`, ['inspect', '--format', '{{.Config.Image}}', `buildx_buildkit_${nodeName}`], {
ignoreReturnCode: true,
silent: true
})
.then(bkitimage => {
if (bkitimage.exitCode == 0 && bkitimage.stdout.length > 0) {
return exec
.getExecOutput(`docker`, ['run', '--rm', bkitimage.stdout.trim(), '--version'], {
ignoreReturnCode: true,
silent: true
})
.then(bkitversion => {
if (bkitversion.exitCode == 0 && bkitversion.stdout.length > 0) {
return `${bkitimage.stdout.trim()} => ${bkitversion.stdout.trim()}`;
} else if (bkitversion.stderr.length > 0) {
throw new Error(bkitimage.stderr.trim());
}
return bkitversion.stdout.trim();
});
} else if (bkitimage.stderr.length > 0) {
throw new Error(bkitimage.stderr.trim());
}
return bkitimage.stdout.trim();
});
}
export async function satisfies(builderName: string, range: string, standalone?: boolean): Promise<boolean> {
const builder = await buildx.inspect(builderName, standalone);
for (const node of builder.nodes) {
let bkversion = node.buildkit;
if (!bkversion) {
// TODO: get version for all nodes
const node = builderInfo.nodes[0];
if (!node.buildkitVersion && node.name) {
try {
bkversion = await getVersionWithinImage(node.name || '');
return await this.getVersionWithinImage(node.name);
} catch (e) {
core.warning(e);
}
}
return node.buildkitVersion;
}
private async getVersionWithinImage(nodeName: string): Promise<string> {
return exec
.getExecOutput(`docker`, ['inspect', '--format', '{{.Config.Image}}', `${this.containerNamePrefix}${nodeName}`], {
ignoreReturnCode: true,
silent: true
})
.then(bkitimage => {
if (bkitimage.exitCode == 0 && bkitimage.stdout.length > 0) {
return exec
.getExecOutput(`docker`, ['run', '--rm', bkitimage.stdout.trim(), '--version'], {
ignoreReturnCode: true,
silent: true
})
.then(bkitversion => {
if (bkitversion.exitCode == 0 && bkitversion.stdout.length > 0) {
return `${bkitimage.stdout.trim()} => ${bkitversion.stdout.trim()}`;
} else if (bkitversion.stderr.length > 0) {
throw new Error(bkitimage.stderr.trim());
}
return bkitversion.stdout.trim();
});
} else if (bkitimage.stderr.length > 0) {
throw new Error(bkitimage.stderr.trim());
}
return bkitimage.stdout.trim();
});
}
public async versionSatisfies(builderName: string, range: string): Promise<boolean> {
const builderInfo = await this.getBuilderInfo(builderName);
for (const node of builderInfo.nodes) {
let bkversion = node.buildkitVersion;
if (!bkversion) {
try {
bkversion = await this.getVersionWithinImage(node.name || '');
} catch (e) {
return false;
}
}
// BuildKit version reported by moby is in the format of `v0.11.0-moby`
if (builderInfo.driver == 'docker' && !bkversion.endsWith('-moby')) {
return false;
}
if (!semver.satisfies(bkversion.replace(/-moby$/, ''), range)) {
return false;
}
}
// BuildKit version reported by moby is in the format of `v0.11.0-moby`
if (builder.driver == 'docker' && !bkversion.endsWith('-moby')) {
return false;
}
if (!semver.satisfies(bkversion.replace(/-moby$/, ''), range)) {
return false;
}
return true;
}
public generateConfigInline(s: string): string {
return this.generateConfig(s, false);
}
public generateConfigFile(s: string): string {
return this.generateConfig(s, true);
}
private generateConfig(s: string, file: boolean): string {
if (file) {
if (!fs.existsSync(s)) {
throw new Error(`config file ${s} not found`);
}
s = fs.readFileSync(s, {encoding: 'utf-8'});
}
const configFile = this.tmpName({tmpdir: this.tmpDir()});
fs.writeFileSync(configFile, s);
return configFile;
}
private tmpDir() {
return this.tmpdir;
}
private tmpName(options?: tmp.TmpNameOptions): string {
return tmp.tmpNameSync(options);
}
return true;
}

View File

@ -1,281 +1,188 @@
import {parse} from 'csv-parse/sync';
import fs from 'fs';
import os from 'os';
import path from 'path';
import * as semver from 'semver';
import * as exec from '@actions/exec';
import * as util from './util';
import {parse} from 'csv-parse/sync';
import * as semver from 'semver';
import * as tmp from 'tmp';
export type Builder = {
name?: string;
driver?: string;
'last-activity'?: Date;
nodes: Node[];
};
export type Node = {
name?: string;
endpoint?: string;
'driver-opts'?: Array<string>;
status?: string;
'buildkitd-flags'?: string;
buildkit?: string;
platforms?: string;
};
export async function getImageIDFile(): Promise<string> {
return path.join(util.tmpDir(), 'iidfile').split(path.sep).join(path.posix.sep);
export interface BuildxOpts {
standalone?: boolean;
}
export async function getImageID(): Promise<string | undefined> {
const iidFile = await getImageIDFile();
if (!fs.existsSync(iidFile)) {
return undefined;
}
return fs.readFileSync(iidFile, {encoding: 'utf-8'}).trim();
}
export class Buildx {
private standalone: boolean;
private version: Promise<string>;
private tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-actions-toolkit-')).split(path.sep).join(path.posix.sep);
export async function getMetadataFile(): Promise<string> {
return path.join(util.tmpDir(), 'metadata-file').split(path.sep).join(path.posix.sep);
}
export async function getMetadata(): Promise<string | undefined> {
const metadataFile = await getMetadataFile();
if (!fs.existsSync(metadataFile)) {
return undefined;
}
const content = fs.readFileSync(metadataFile, {encoding: 'utf-8'}).trim();
if (content === 'null') {
return undefined;
}
return content;
}
export async function getDigest(metadata: string | undefined): Promise<string | undefined> {
if (metadata === undefined) {
return undefined;
}
const metadataJSON = JSON.parse(metadata);
if (metadataJSON['containerimage.digest']) {
return metadataJSON['containerimage.digest'];
}
return undefined;
}
export async function getSecretString(kvp: string): Promise<string> {
return getSecret(kvp, false);
}
export async function getSecretFile(kvp: string): Promise<string> {
return getSecret(kvp, true);
}
export async function getSecret(kvp: string, file: boolean): Promise<string> {
const delimiterIndex = kvp.indexOf('=');
const key = kvp.substring(0, delimiterIndex);
let value = kvp.substring(delimiterIndex + 1);
if (key.length == 0 || value.length == 0) {
throw new Error(`${kvp} is not a valid secret`);
constructor(opts?: BuildxOpts) {
this.standalone = opts?.standalone ?? false;
this.version = this.getVersion();
}
if (file) {
if (!fs.existsSync(value)) {
throw new Error(`secret file ${value} not found`);
public getCommand(args: Array<string>) {
return {
command: this.standalone ? 'buildx' : 'docker',
args: this.standalone ? args : ['buildx', ...args]
};
}
public async isAvailable(): Promise<boolean> {
const cmd = this.getCommand([]);
return await exec
.getExecOutput(cmd.command, cmd.args, {
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;
});
}
public async getVersion(): Promise<string> {
const cmd = this.getCommand(['version']);
return await exec
.getExecOutput(cmd.command, cmd.args, {
ignoreReturnCode: true,
silent: true
})
.then(res => {
if (res.stderr.length > 0 && res.exitCode != 0) {
throw new Error(res.stderr.trim());
}
return Buildx.parseVersion(res.stdout.trim());
});
}
public static parseVersion(stdout: string): string {
const matches = /\sv?([0-9a-f]{7}|[0-9.]+)/.exec(stdout);
if (!matches) {
throw new Error(`Cannot parse buildx version`);
}
value = fs.readFileSync(value, {encoding: 'utf-8'});
return matches[1];
}
const secretFile = util.tmpNameSync({
tmpdir: util.tmpDir()
});
fs.writeFileSync(secretFile, value);
public static versionSatisfies(version: string, range: string): boolean {
return semver.satisfies(version, range) || /^[0-9a-f]{7}$/.exec(version) !== null;
}
return `id=${key},src=${secretFile}`;
}
public getBuildImageIDFilePath(): string {
return path.join(this.tmpDir(), 'iidfile').split(path.sep).join(path.posix.sep);
}
export function hasLocalExporter(outputs: string[]): boolean {
return hasExporterType('local', outputs);
}
public getBuildMetadataFilePath(): string {
return path.join(this.tmpDir(), 'metadata-file').split(path.sep).join(path.posix.sep);
}
export function hasTarExporter(outputs: string[]): boolean {
return hasExporterType('tar', outputs);
}
export function hasLocalOrTarExporter(outputs: string[]): boolean {
return hasLocalExporter(outputs) || hasTarExporter(outputs);
}
function hasExporterType(name: string, outputs: string[]): boolean {
const records = parse(outputs.join(`\n`), {
delimiter: ',',
trim: true,
columns: false,
relaxColumnCount: true
});
for (const record of records) {
if (record.length == 1 && !record[0].startsWith('type=')) {
// Local if no type is defined
// https://github.com/docker/buildx/blob/d2bf42f8b4784d83fde17acb3ed84703ddc2156b/build/output.go#L29-L43
return name == 'local';
public getBuildImageID(): string | undefined {
const iidFile = this.getBuildImageIDFilePath();
if (!fs.existsSync(iidFile)) {
return undefined;
}
for (const [key, value] of record.map(chunk => chunk.split('=').map(item => item.trim()))) {
if (key == 'type' && value == name) {
return fs.readFileSync(iidFile, {encoding: 'utf-8'}).trim();
}
public getBuildMetadata(): string | undefined {
const metadataFile = this.getBuildMetadataFilePath();
if (!fs.existsSync(metadataFile)) {
return undefined;
}
const content = fs.readFileSync(metadataFile, {encoding: 'utf-8'}).trim();
if (content === 'null') {
return undefined;
}
return content;
}
public getDigest(): string | undefined {
const metadata = this.getBuildMetadata();
if (metadata === undefined) {
return undefined;
}
const metadataJSON = JSON.parse(metadata);
if (metadataJSON['containerimage.digest']) {
return metadataJSON['containerimage.digest'];
}
return undefined;
}
public generateBuildSecretString(kvp: string): string {
return this.generateBuildSecret(kvp, false);
}
public generateBuildSecretFile(kvp: string): string {
return this.generateBuildSecret(kvp, true);
}
private generateBuildSecret(kvp: string, file: boolean): string {
const delimiterIndex = kvp.indexOf('=');
const key = kvp.substring(0, delimiterIndex);
let value = kvp.substring(delimiterIndex + 1);
if (key.length == 0 || value.length == 0) {
throw new Error(`${kvp} is not a valid secret`);
}
if (file) {
if (!fs.existsSync(value)) {
throw new Error(`secret file ${value} not found`);
}
value = fs.readFileSync(value, {encoding: 'utf-8'});
}
const secretFile = this.tmpName({tmpdir: this.tmpDir()});
fs.writeFileSync(secretFile, value);
return `id=${key},src=${secretFile}`;
}
public static hasLocalExporter(exporters: string[]): boolean {
return Buildx.hasExporterType('local', exporters);
}
public static hasTarExporter(exporters: string[]): boolean {
return Buildx.hasExporterType('tar', exporters);
}
public static hasExporterType(name: string, exporters: string[]): boolean {
const records = parse(exporters.join(`\n`), {
delimiter: ',',
trim: true,
columns: false,
relaxColumnCount: true
});
for (const record of records) {
if (record.length == 1 && !record[0].startsWith('type=')) {
// Local if no type is defined
// https://github.com/docker/buildx/blob/d2bf42f8b4784d83fde17acb3ed84703ddc2156b/build/output.go#L29-L43
return name == 'local';
}
for (const [key, value] of record.map(chunk => chunk.split('=').map(item => item.trim()))) {
if (key == 'type' && value == name) {
return true;
}
}
}
return false;
}
public static hasGitAuthTokenSecret(secrets: string[]): boolean {
for (const secret of secrets) {
if (secret.startsWith('GIT_AUTH_TOKEN=')) {
return true;
}
}
return false;
}
return false;
}
export function hasGitAuthToken(secrets: string[]): boolean {
for (const secret of secrets) {
if (secret.startsWith('GIT_AUTH_TOKEN=')) {
return true;
}
private tmpDir() {
return this.tmpdir;
}
return false;
}
export async function isAvailable(standalone?: boolean): Promise<boolean> {
const cmd = getCommand([], standalone);
return await exec
.getExecOutput(cmd.command, cmd.args, {
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;
});
}
export async function inspect(name: string, standalone?: boolean): Promise<Builder> {
const cmd = getCommand(['inspect', name], standalone);
return await exec
.getExecOutput(cmd.command, cmd.args, {
ignoreReturnCode: true,
silent: true
})
.then(res => {
if (res.stderr.length > 0 && res.exitCode != 0) {
throw new Error(res.stderr.trim());
}
return parseInspect(res.stdout);
});
}
export async function parseInspect(data: string): Promise<Builder> {
const builder: Builder = {
nodes: []
};
let node: Node = {};
for (const line of data.trim().split(`\n`)) {
const [key, ...rest] = line.split(':');
const value = rest.map(v => v.trim()).join(':');
if (key.length == 0 || value.length == 0) {
continue;
}
switch (key.toLowerCase()) {
case 'name': {
if (builder.name == undefined) {
builder.name = value;
} else {
if (Object.keys(node).length > 0) {
builder.nodes.push(node);
node = {};
}
node.name = value;
}
break;
}
case 'driver': {
builder.driver = value;
break;
}
case 'last activity': {
builder['last-activity'] = new Date(value);
break;
}
case 'endpoint': {
node.endpoint = value;
break;
}
case 'driver options': {
node['driver-opts'] = (value.match(/(\w+)="([^"]*)"/g) || []).map(v => v.replace(/^(.*)="(.*)"$/g, '$1=$2'));
break;
}
case 'status': {
node.status = value;
break;
}
case 'flags': {
node['buildkitd-flags'] = value;
break;
}
case 'buildkit': {
node.buildkit = value;
break;
}
case 'platforms': {
let platforms: Array<string> = [];
// if a preferred platform is being set then use only these
// https://docs.docker.com/engine/reference/commandline/buildx_inspect/#get-information-about-a-builder-instance
if (value.includes('*')) {
for (const platform of value.split(', ')) {
if (platform.includes('*')) {
platforms.push(platform.replace('*', ''));
}
}
} else {
// otherwise set all platforms available
platforms = value.split(', ');
}
node.platforms = platforms.join(',');
break;
}
}
private tmpName(options?: tmp.TmpNameOptions): string {
return tmp.tmpNameSync(options);
}
if (Object.keys(node).length > 0) {
builder.nodes.push(node);
}
return builder;
}
export async function getVersion(standalone?: boolean): Promise<string> {
const cmd = getCommand(['version'], standalone);
return await exec
.getExecOutput(cmd.command, cmd.args, {
ignoreReturnCode: true,
silent: true
})
.then(res => {
if (res.stderr.length > 0 && res.exitCode != 0) {
throw new Error(res.stderr.trim());
}
return parseVersion(res.stdout.trim());
});
}
export function parseVersion(stdout: string): string {
const matches = /\sv?([0-9a-f]{7}|[0-9.]+)/.exec(stdout);
if (!matches) {
throw new Error(`Cannot parse buildx version`);
}
return matches[1];
}
export function satisfies(version: string, range: string): boolean {
return semver.satisfies(version, range) || /^[0-9a-f]{7}$/.exec(version) !== null;
}
export function getCommand(args: Array<string>, standalone?: boolean) {
return {
command: standalone ? 'buildx' : 'docker',
args: standalone ? args : ['buildx', ...args]
};
}

View File

@ -1,19 +1,21 @@
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) {
export class Docker {
public static async 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;
}
return res.exitCode == 0;
})
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.catch(error => {
return false;
});
});
}
}

View File

@ -2,30 +2,73 @@ import jwt_decode, {JwtPayload} from 'jwt-decode';
import * as github from '@actions/github';
import {Context} from '@actions/github/lib/context';
import {components as OctoOpenApiTypes} from '@octokit/openapi-types';
import * as util from './util';
import {WebhookPayload} from '@actions/github/lib/interfaces';
export type Payload = WebhookPayload;
export type ReposGetResponseData = OctoOpenApiTypes['schemas']['repository'];
export function context(): Context {
return github.context;
}
export async function repo(token: string): Promise<ReposGetResponseData> {
return github
.getOctokit(token)
.rest.repos.get({...github.context.repo})
.then(response => response.data as ReposGetResponseData);
}
interface Jwt extends JwtPayload {
ac?: string;
}
export const parseRuntimeToken = (token: string): Jwt => {
return jwt_decode<Jwt>(token);
};
export class GitHub {
private static instance?: GitHub;
static getInstance = (): GitHub => (GitHub.instance = GitHub.instance ?? new GitHub());
private static _gitContext: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function fromPayload(path: string): any {
return util.select(github.context.payload, path);
private constructor() {
let ref = this.ref();
if (github.context.sha && ref && !ref.startsWith('refs/')) {
ref = `refs/heads/${this.ref()}`;
}
if (github.context.sha && !ref.startsWith(`refs/pull/`)) {
ref = github.context.sha;
}
GitHub._gitContext = `${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${github.context.repo.owner}/${github.context.repo.repo}.git#${ref}`;
}
public context(): Context {
return github.context;
}
private ref(): string {
return github.context.ref;
}
public gitContext() {
return GitHub._gitContext;
}
private payload(): Payload {
return github.context.payload;
}
public repo(token: string): Promise<ReposGetResponseData> {
return github
.getOctokit(token)
.rest.repos.get({...github.context.repo})
.then(response => response.data as ReposGetResponseData);
}
public parseRuntimeToken(): Jwt {
return jwt_decode<Jwt>(process.env['ACTIONS_RUNTIME_TOKEN'] || '');
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public fromPayload(path: string): any {
return this.select(this.payload(), path);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private select(obj: any, path: string): any {
if (!obj) {
return undefined;
}
const i = path.indexOf('.');
if (i < 0) {
return obj[path];
}
const key = path.slice(0, i);
return this.select(obj[key], path.slice(i + 1));
}
}

View File

@ -1,92 +1,49 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as tmp from 'tmp';
import * as core from '@actions/core';
import * as github from '@actions/github';
import {parse} from 'csv-parse/sync';
let _defaultContext, _tmpDir: string;
export class Util {
public static getInputList(name: string, ignoreComma?: boolean): string[] {
const res: Array<string> = [];
export function defaultContext(): string {
if (!_defaultContext) {
let ref = github.context.ref;
if (github.context.sha && ref && !ref.startsWith('refs/')) {
ref = `refs/heads/${github.context.ref}`;
const items = core.getInput(name);
if (items == '') {
return res;
}
if (github.context.sha && !ref.startsWith(`refs/pull/`)) {
ref = github.context.sha;
const records = parse(items, {
columns: false,
relaxQuotes: true,
comment: '#',
relaxColumnCount: true,
skipEmptyLines: true
});
for (const record of records as Array<string[]>) {
if (record.length == 1) {
res.push(record[0]);
continue;
} else if (!ignoreComma) {
res.push(...record);
continue;
}
res.push(record.join(','));
}
_defaultContext = `${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${github.context.repo.owner}/${github.context.repo.repo}.git#${ref}`;
}
return _defaultContext;
}
export function tmpDir(): string {
if (!_tmpDir) {
_tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'docker-actions-toolkit-')).split(path.sep).join(path.posix.sep);
}
return _tmpDir;
}
export function tmpNameSync(options?: tmp.TmpNameOptions): string {
return tmp.tmpNameSync(options);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function select(obj: any, path: string): any {
if (!obj) {
return undefined;
}
const i = path.indexOf('.');
if (i < 0) {
return obj[path];
}
const key = path.slice(0, i);
return select(obj[key], path.slice(i + 1));
}
export function getInputList(name: string, ignoreComma?: boolean): string[] {
const res: Array<string> = [];
const items = core.getInput(name);
if (items == '') {
return res;
return res.filter(item => item).map(pat => pat.trim());
}
const records = parse(items, {
columns: false,
relaxQuotes: true,
comment: '#',
relaxColumnCount: true,
skipEmptyLines: true
});
for (const record of records as Array<string[]>) {
if (record.length == 1) {
res.push(record[0]);
continue;
} else if (!ignoreComma) {
res.push(...record);
continue;
public static async asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
res.push(record.join(','));
}
return res.filter(item => item).map(pat => pat.trim());
}
export const asyncForEach = async (array, callback) => {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
};
export function isValidUrl(url: string): boolean {
try {
new URL(url);
} catch (e) {
return false;
}
return true;
public static isValidUrl(url: string): boolean {
try {
new URL(url);
} catch (e) {
return false;
}
return true;
}
}

View File

@ -3541,6 +3541,11 @@ rimraf@^3.0.0, rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
rimraf@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-4.1.1.tgz#ec29817863e5d82d22bca82f9dc4325be2f1e72b"
integrity sha512-Z4Y81w8atcvaJuJuBB88VpADRH66okZAuEm+Jtaufa+s7rZmIz+Hik2G53kGaNytE7lsfXyWktTmfVz0H9xuDg==
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"