From 080cadd33e9e0b1c43635ed576ba550a894f9327 Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Mon, 15 Feb 2021 10:08:19 +0100 Subject: [PATCH 1/2] Allow to use secret file mount Signed-off-by: CrazyMax --- Dockerfile | 2 +- README.md | 6 ++---- __tests__/buildx.test.ts | 39 +++++++++++++++++++++++------------ __tests__/context.test.ts | 21 +++++++++++++++++++ __tests__/fixtures/secret.txt | 1 + dist/index.js | 39 +++++++++++++++++++++++++++++------ src/buildx.ts | 23 ++++++++++++++++++--- src/context.ts | 13 ++++++++++-- 8 files changed, 115 insertions(+), 29 deletions(-) create mode 100644 __tests__/fixtures/secret.txt diff --git a/Dockerfile b/Dockerfile index ee42ad1..c49ccf8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -#syntax=docker/dockerfile:1.1-experimental +#syntax=docker/dockerfile:1.2 FROM node:12 AS deps WORKDIR /src diff --git a/README.md b/README.md index a159c15..1d32e94 100644 --- a/README.md +++ b/README.md @@ -471,9 +471,6 @@ using [actions/cache](https://github.com/actions/cache) with this action: ``` -> If you want to [export layers for all stages](https://github.com/docker/buildx#--cache-tonametypetypekeyvalue), -> you have to specify `mode=max` attribute in `cache-to`. - ### Handle tags and labels If you come from [`v1`](https://github.com/docker/build-push-action/tree/releases/v1#readme) and want an @@ -622,7 +619,8 @@ Following inputs can be used as `step.with` keys | `outputs` | List | List of [output destinations](https://github.com/docker/buildx#-o---outputpath-typetypekeyvalue) (format: `type=local,dest=path`) | | `cache-from` | List | List of [external cache sources](https://github.com/docker/buildx#--cache-fromnametypetypekeyvalue) (eg. `type=local,src=path/to/dir`) | | `cache-to` | List | List of [cache export destinations](https://github.com/docker/buildx#--cache-tonametypetypekeyvalue) (eg. `type=local,dest=path/to/dir`) | -| `secrets` | List | List of secrets to expose to the build (eg. `key=value`, `GIT_AUTH_TOKEN=mytoken`) | +| `secrets` | List | List of secrets to expose to the build (eg. `key=string`, `GIT_AUTH_TOKEN=mytoken`) | +| `secret-files` | List | List of secret files to expose to the build (eg. `key=filename`, `MY_SECRET=./secret.txt`) | | `ssh` | List | List of SSH agent socket or keys to expose to the build | ### outputs diff --git a/__tests__/buildx.test.ts b/__tests__/buildx.test.ts index 9c8a16e..741e29c 100644 --- a/__tests__/buildx.test.ts +++ b/__tests__/buildx.test.ts @@ -119,21 +119,34 @@ describe('parseVersion', () => { describe('getSecret', () => { test.each([ - ['A_SECRET=abcdef0123456789', 'A_SECRET', 'abcdef0123456789', false], - ['GIT_AUTH_TOKEN=abcdefghijklmno=0123456789', 'GIT_AUTH_TOKEN', 'abcdefghijklmno=0123456789', false], - ['MY_KEY=c3RyaW5nLXdpdGgtZXF1YWxzCg==', 'MY_KEY', 'c3RyaW5nLXdpdGgtZXF1YWxzCg==', false], - ['aaaaaaaa', '', '', true], - ['aaaaaaaa=', '', '', true], - ['=bbbbbbb', '', '', true] - ])('given %p key and %p secret', async (kvp, key, secret, invalid) => { + ['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) => { try { - const secretArgs = await buildx.getSecret(kvp); + let secret: string; + if (file) { + secret = await buildx.getSecretFile(kvp); + } else { + secret = await buildx.getSecretString(kvp); + } expect(true).toBe(!invalid); - console.log(`secretArgs: ${secretArgs}`); - expect(secretArgs).toEqual(`id=${key},src=${tmpNameSync}`); - const secretContent = await fs.readFileSync(tmpNameSync, 'utf-8'); - console.log(`secretValue: ${secretContent}`); - expect(secretContent).toEqual(secret); + console.log(`secret: ${secret}`); + expect(secret).toEqual(`id=${exKey},src=${tmpNameSync}`); + const secretValue = await fs.readFileSync(tmpNameSync, 'utf-8'); + console.log(`secretValue: ${secretValue}`); + expect(secretValue).toEqual(exValue); } catch (err) { expect(true).toBe(invalid); } diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts index 8e5dae2..a3e3e0d 100644 --- a/__tests__/context.test.ts +++ b/__tests__/context.test.ts @@ -337,6 +337,27 @@ ccc`], '--push', 'https://github.com/docker/build-push-action.git#heads/master' ] + ], + [ + '0.5.1', + new Map([ + ['context', 'https://github.com/docker/build-push-action.git#heads/master'], + ['tag', 'localhost:5000/name/app:latest'], + ['secret-files', `MY_SECRET=${path.join(__dirname, 'fixtures', 'secret.txt').split(path.sep).join(path.posix.sep)}`], + ['file', './test/Dockerfile'], + ['builder', 'builder-git-context-2'], + ['push', 'true'] + ]), + [ + 'buildx', + 'build', + '--iidfile', '/tmp/.docker-build-push-jest/iidfile', + '--secret', 'id=MY_SECRET,src=/tmp/.docker-build-push-jest/.tmpname-jest', + '--file', './test/Dockerfile', + '--builder', 'builder-git-context-2', + '--push', + 'https://github.com/docker/build-push-action.git#heads/master' + ] ] ])( 'given %p with %p as inputs, returns %p', diff --git a/__tests__/fixtures/secret.txt b/__tests__/fixtures/secret.txt new file mode 100644 index 0000000..ba0e162 --- /dev/null +++ b/__tests__/fixtures/secret.txt @@ -0,0 +1 @@ +bar \ No newline at end of file diff --git a/dist/index.js b/dist/index.js index 67f1095..aa03314 100644 --- a/dist/index.js +++ b/dist/index.js @@ -4581,7 +4581,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.parseVersion = exports.getVersion = exports.isAvailable = exports.hasGitAuthToken = exports.isLocalOrTarExporter = exports.getSecret = exports.getImageID = exports.getImageIDFile = void 0; +exports.parseVersion = exports.getVersion = exports.isAvailable = exports.hasGitAuthToken = exports.isLocalOrTarExporter = exports.getSecret = exports.getSecretFile = exports.getSecretString = exports.getImageID = exports.getImageIDFile = void 0; const sync_1 = __importDefault(__webpack_require__(750)); const fs_1 = __importDefault(__webpack_require__(747)); const path_1 = __importDefault(__webpack_require__(622)); @@ -4604,18 +4604,36 @@ function getImageID() { }); } exports.getImageID = getImageID; -function getSecret(kvp) { +function getSecretString(kvp) { + return __awaiter(this, void 0, void 0, function* () { + return getSecret(kvp, false); + }); +} +exports.getSecretString = getSecretString; +function getSecretFile(kvp) { + return __awaiter(this, void 0, void 0, function* () { + return getSecret(kvp, true); + }); +} +exports.getSecretFile = getSecretFile; +function getSecret(kvp, file) { return __awaiter(this, void 0, void 0, function* () { const delimiterIndex = kvp.indexOf('='); const key = kvp.substring(0, delimiterIndex); - const value = kvp.substring(delimiterIndex + 1); + 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_1.default.existsSync(value)) { + throw new Error(`secret file ${value} not found`); + } + value = fs_1.default.readFileSync(value, { encoding: 'utf-8' }); + } const secretFile = context.tmpNameSync({ tmpdir: context.tmpDir() }); - yield fs_1.default.writeFileSync(secretFile, value); + fs_1.default.writeFileSync(secretFile, value); return `id=${key},src=${secretFile}`; }); } @@ -13003,6 +13021,7 @@ function getInputs(defaultContext) { cacheFrom: yield getInputList('cache-from', true), cacheTo: yield getInputList('cache-to', true), secrets: yield getInputList('secrets', true), + secretFiles: yield getInputList('secret-files', true), githubToken: core.getInput('github-token'), ssh: yield getInputList('ssh') }; @@ -13055,14 +13074,22 @@ function getBuildArgs(inputs, defaultContext, buildxVersion) { })); yield exports.asyncForEach(inputs.secrets, (secret) => __awaiter(this, void 0, void 0, function* () { try { - args.push('--secret', yield buildx.getSecret(secret)); + args.push('--secret', yield buildx.getSecretString(secret)); + } + catch (err) { + core.warning(err.message); + } + })); + yield exports.asyncForEach(inputs.secretFiles, (secretFile) => __awaiter(this, void 0, void 0, function* () { + try { + args.push('--secret', yield buildx.getSecretFile(secretFile)); } catch (err) { core.warning(err.message); } })); if (inputs.githubToken && !buildx.hasGitAuthToken(inputs.secrets) && inputs.context == defaultContext) { - args.push('--secret', yield buildx.getSecret(`GIT_AUTH_TOKEN=${inputs.githubToken}`)); + args.push('--secret', yield buildx.getSecretString(`GIT_AUTH_TOKEN=${inputs.githubToken}`)); } yield exports.asyncForEach(inputs.ssh, (ssh) => __awaiter(this, void 0, void 0, function* () { args.push('--ssh', ssh); diff --git a/src/buildx.ts b/src/buildx.ts index 36fbc14..327604a 100644 --- a/src/buildx.ts +++ b/src/buildx.ts @@ -18,17 +18,34 @@ export async function getImageID(): Promise { return fs.readFileSync(iidFile, {encoding: 'utf-8'}); } -export async function getSecret(kvp: string): Promise { +export async function getSecretString(kvp: string): Promise { + return getSecret(kvp, false); +} + +export async function getSecretFile(kvp: string): Promise { + return getSecret(kvp, true); +} + +export async function getSecret(kvp: string, file: boolean): Promise { const delimiterIndex = kvp.indexOf('='); const key = kvp.substring(0, delimiterIndex); - const value = kvp.substring(delimiterIndex + 1); + 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 = context.tmpNameSync({ tmpdir: context.tmpDir() }); - await fs.writeFileSync(secretFile, value); + fs.writeFileSync(secretFile, value); + return `id=${key},src=${secretFile}`; } diff --git a/src/context.ts b/src/context.ts index 4608354..9b81fdd 100644 --- a/src/context.ts +++ b/src/context.ts @@ -30,6 +30,7 @@ export interface Inputs { cacheFrom: string[]; cacheTo: string[]; secrets: string[]; + secretFiles: string[]; githubToken: string; ssh: string[]; } @@ -73,6 +74,7 @@ export async function getInputs(defaultContext: string): Promise { cacheFrom: await getInputList('cache-from', true), cacheTo: await getInputList('cache-to', true), secrets: await getInputList('secrets', true), + secretFiles: await getInputList('secret-files', true), githubToken: core.getInput('github-token'), ssh: await getInputList('ssh') }; @@ -123,13 +125,20 @@ async function getBuildArgs(inputs: Inputs, defaultContext: string, buildxVersio }); await asyncForEach(inputs.secrets, async secret => { try { - args.push('--secret', await buildx.getSecret(secret)); + args.push('--secret', await buildx.getSecretString(secret)); + } catch (err) { + core.warning(err.message); + } + }); + await asyncForEach(inputs.secretFiles, async secretFile => { + try { + args.push('--secret', await buildx.getSecretFile(secretFile)); } catch (err) { core.warning(err.message); } }); if (inputs.githubToken && !buildx.hasGitAuthToken(inputs.secrets) && inputs.context == defaultContext) { - args.push('--secret', await buildx.getSecret(`GIT_AUTH_TOKEN=${inputs.githubToken}`)); + args.push('--secret', await buildx.getSecretString(`GIT_AUTH_TOKEN=${inputs.githubToken}`)); } await asyncForEach(inputs.ssh, async ssh => { args.push('--ssh', ssh); From 33eec1587d300657c3a29ed4b5d313014606de1f Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Tue, 16 Feb 2021 11:56:02 +0100 Subject: [PATCH 2/2] Update action.yml Signed-off-by: CrazyMax --- action.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/action.yml b/action.yml index 3ee80a0..1e9608a 100644 --- a/action.yml +++ b/action.yml @@ -60,15 +60,18 @@ inputs: description: "List of cache export destinations for buildx (eg. user/app:cache, type=local,dest=path/to/dir)" required: false secrets: - description: "List of secrets to expose to the build (eg. key=value, GIT_AUTH_TOKEN=mytoken)" + description: "List of secrets to expose to the build (eg. key=string, GIT_AUTH_TOKEN=mytoken)" + required: false + secret-files: + description: "List of secret files to expose to the build (eg. key=filename, MY_SECRET=./secret.txt)" + required: false + ssh: + description: "List of SSH agent socket or keys to expose to the build" required: false github-token: description: "GitHub Token used to authenticate against a repository for Git context" default: ${{ github.token }} required: false - ssh: - description: "List of SSH agent socket or keys to expose to the build" - required: false outputs: digest: