-
Notifications
You must be signed in to change notification settings - Fork 18
build/bake: BuildKit secrets support #248
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -130,6 +130,9 @@ on: | |
| registry-auths: | ||
| description: "Raw authentication to registries, defined as YAML objects (for image output)" | ||
| required: false | ||
| build-secrets: | ||
| description: "YAML object mapping BuildKit secret IDs to secret values" | ||
| required: false | ||
| github-token: | ||
| description: "GitHub Token used to authenticate against the repository for Git context" | ||
| required: false | ||
|
|
@@ -455,7 +458,7 @@ jobs: | |
| } | ||
| ); | ||
| await core.group(`Set envs`, async () => { | ||
| core.info(JSON.stringify(envs, null, 2)); | ||
| core.info(JSON.stringify(Object.keys(envs).sort(), null, 2)); | ||
| }); | ||
|
|
||
| const metaImages = inpMetaImages.map(image => image.toLowerCase()); | ||
|
|
@@ -797,6 +800,7 @@ jobs: | |
| INPUT_CACHE: ${{ inputs.cache }} | ||
| INPUT_CACHE-SCOPE: ${{ inputs.cache-scope }} | ||
| INPUT_CACHE-MODE: ${{ inputs.cache-mode }} | ||
| INPUT_BUILD-SECRETS: ${{ secrets.build-secrets }} | ||
| INPUT_CONTEXT: ${{ inputs.context }} | ||
| INPUT_FILES: ${{ inputs.files }} | ||
| INPUT_OUTPUT: ${{ inputs.output }} | ||
|
|
@@ -820,7 +824,14 @@ jobs: | |
| const { Build } = require('@docker/github-builder-runtime/lib/buildx/build'); | ||
| const { GitHub } = require('@docker/github-builder-runtime/lib/github/github'); | ||
| const { Util } = require('@docker/github-builder-runtime/lib/util'); | ||
|
|
||
|
|
||
| let yaml; | ||
| try { | ||
| yaml = require('js-yaml'); | ||
| } catch { | ||
| yaml = require('@docker/github-builder-runtime/node_modules/js-yaml'); | ||
| } | ||
|
|
||
| const inpPlatform = core.getInput('platform'); | ||
| const platformPairSuffix = inpPlatform ? `-${inpPlatform.replace(/\//g, '-')}` : ''; | ||
| core.setOutput('platform-pair-suffix', platformPairSuffix); | ||
|
|
@@ -831,6 +842,7 @@ jobs: | |
| const inpCache = core.getBooleanInput('cache'); | ||
| const inpCacheScope = core.getInput('cache-scope'); | ||
| const inpCacheMode = core.getInput('cache-mode'); | ||
| const inpBuildSecrets = core.getInput('build-secrets'); | ||
| const inpContext = core.getInput('context'); | ||
| const inpFiles = Util.getInputList('files'); | ||
| const inpOutput = core.getInput('output'); | ||
|
|
@@ -854,6 +866,40 @@ jobs: | |
| tags: inpMetaTags | ||
| }; | ||
| const renderTemplate = value => Util.compileHandlebars(value, {noEscape: true}, {meta}); | ||
| const parseBuildSecrets = value => { | ||
| const normalized = value.trim(); | ||
| if (!normalized) { | ||
| return {}; | ||
| } | ||
| let parsed; | ||
| try { | ||
| parsed = yaml.load(normalized, {schema: yaml.FAILSAFE_SCHEMA}); | ||
| } catch (err) { | ||
| throw new Error(`Failed to parse build-secrets YAML: ${err.message}`); | ||
| } | ||
| if (!parsed) { | ||
| return {}; | ||
| } | ||
| if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') { | ||
| throw new Error('build-secrets must be a YAML object'); | ||
| } | ||
| const secrets = {}; | ||
| for (const [id, secret] of Object.entries(parsed)) { | ||
| if (!/^[A-Za-z0-9_.-]+$/.test(id)) { | ||
| throw new Error(`Invalid build secret id "${id}": use letters, digits, dots, underscores or dashes`); | ||
| } | ||
| if (typeof secret !== 'string') { | ||
| throw new Error(`build-secrets value for "${id}" must be a string`); | ||
| } | ||
| if (secret.length === 0) { | ||
| throw new Error(`build-secrets value for "${id}" must not be empty`); | ||
| } | ||
| core.setSecret(secret); | ||
| secrets[id] = secret; | ||
| } | ||
| return secrets; | ||
| }; | ||
| const toBuildSecretEnvName = (id, index) => `BUILD_SECRET_${index}_${id.toUpperCase().replace(/[^A-Z0-9_]/g, '_')}`; | ||
|
|
||
| const gitContextAttrs = GitHub.context.ref.startsWith('refs/tags/') ? {checksum: GitHub.context.sha} : {'fetch-by-commit': 'true'}; | ||
| const bakeSource = await new Build().gitContext({subdir: inpContext, attrs: gitContextAttrs}); | ||
|
|
@@ -872,6 +918,14 @@ jobs: | |
| core.info(sbom); | ||
| core.setOutput('sbom', sbom); | ||
| }); | ||
|
|
||
| let buildSecrets; | ||
| try { | ||
| buildSecrets = parseBuildSecrets(inpBuildSecrets); | ||
| } catch (err) { | ||
| core.setFailed(err.message); | ||
| return; | ||
| } | ||
|
|
||
| const envs = Object.assign({}, | ||
| inpVars ? inpVars.reduce((acc, curr) => { | ||
|
|
@@ -886,8 +940,14 @@ jobs: | |
| BUILDX_BAKE_GIT_AUTH_TOKEN: inpGitHubToken | ||
| } | ||
| ); | ||
| const secretOverrides = []; | ||
| Object.entries(buildSecrets).forEach(([id, secret], index) => { | ||
| const envName = toBuildSecretEnvName(id, index); | ||
| envs[envName] = secret; | ||
| secretOverrides.push(`*.secrets+=id=${id},env=${envName}`); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this adding secrets to the definition? I would think secrets need to be defined already and just values are loaded here. |
||
| }); | ||
| await core.group(`Set envs`, async () => { | ||
| core.info(JSON.stringify(envs, null, 2)); | ||
| core.info(JSON.stringify(Object.keys(envs).sort(), null, 2)); | ||
| core.setOutput('envs', JSON.stringify(envs)); | ||
| }); | ||
|
|
||
|
|
@@ -950,6 +1010,7 @@ jobs: | |
| bakeOverrides.push(`*.cache-from=type=gha,scope=${inpCacheScope || inpTarget}${platformPairSuffix}`); | ||
| bakeOverrides.push(`*.cache-to=type=gha,ignore-error=true,scope=${inpCacheScope || inpTarget}${platformPairSuffix},mode=${inpCacheMode}`); | ||
| } | ||
| bakeOverrides.push(...secretOverrides); | ||
| core.info(JSON.stringify(bakeOverrides, null, 2)); | ||
| core.setOutput('overrides', bakeOverrides.join(os.EOL)); | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -133,6 +133,9 @@ on: | |
| registry-auths: | ||
| description: "Raw authentication to registries, defined as YAML objects (for image output)" | ||
| required: false | ||
| build-secrets: | ||
| description: "YAML object mapping BuildKit secret IDs to secret values" | ||
| required: false | ||
| github-token: | ||
| description: "GitHub Token used to authenticate against the repository for Git context" | ||
| required: false | ||
|
|
@@ -691,6 +694,7 @@ jobs: | |
| INPUT_CACHE: ${{ inputs.cache }} | ||
| INPUT_CACHE-SCOPE: ${{ inputs.cache-scope }} | ||
| INPUT_CACHE-MODE: ${{ inputs.cache-mode }} | ||
| INPUT_BUILD-SECRETS: ${{ secrets.build-secrets }} | ||
| INPUT_LABELS: ${{ inputs.labels }} | ||
| INPUT_CONTEXT: ${{ inputs.context }} | ||
| INPUT_OUTPUT: ${{ inputs.output }} | ||
|
|
@@ -705,12 +709,20 @@ jobs: | |
| INPUT_META-ANNOTATIONS: ${{ steps.meta.outputs.annotations }} | ||
| INPUT_SET-META-LABELS: ${{ inputs.set-meta-labels }} | ||
| INPUT_META-LABELS: ${{ steps.meta.outputs.labels }} | ||
| INPUT_GITHUB-TOKEN: ${{ secrets.github-token || github.token }} | ||
| with: | ||
| script: | | ||
| const { Build } = require('@docker/github-builder-runtime/lib/buildx/build'); | ||
| const { GitHub } = require('@docker/github-builder-runtime/lib/github/github'); | ||
| const { Util } = require('@docker/github-builder-runtime/lib/util'); | ||
|
|
||
|
|
||
| let yaml; | ||
| try { | ||
| yaml = require('js-yaml'); | ||
| } catch { | ||
| yaml = require('@docker/github-builder-runtime/node_modules/js-yaml'); | ||
| } | ||
|
|
||
| const inpPlatform = core.getInput('platform'); | ||
| const platformPairSuffix = inpPlatform ? `-${inpPlatform.replace(/\//g, '-')}` : ''; | ||
| core.setOutput('platform-pair-suffix', platformPairSuffix); | ||
|
|
@@ -724,6 +736,7 @@ jobs: | |
| const inpCache = core.getBooleanInput('cache'); | ||
| const inpCacheScope = core.getInput('cache-scope'); | ||
| const inpCacheMode = core.getInput('cache-mode'); | ||
| const inpBuildSecrets = core.getInput('build-secrets'); | ||
| const inpContext = core.getInput('context'); | ||
| const inpLabels = core.getInput('labels'); | ||
| const inpOutput = core.getInput('output'); | ||
|
|
@@ -739,6 +752,7 @@ jobs: | |
| const inpMetaAnnotations = core.getMultilineInput('meta-annotations'); | ||
| const inpSetMetaLabels = core.getBooleanInput('set-meta-labels'); | ||
| const inpMetaLabels = core.getMultilineInput('meta-labels'); | ||
| const inpGitHubToken = core.getInput('github-token'); | ||
|
|
||
| const meta = { | ||
| version: inpMetaVersion, | ||
|
|
@@ -747,6 +761,40 @@ jobs: | |
|
|
||
| const renderTemplate = value => Util.compileHandlebars(value, {noEscape: true}, {meta}); | ||
| const toMultilineInput = value => value.split(/\r?\n/).map(line => line.trim()).filter(Boolean); | ||
| const parseBuildSecrets = value => { | ||
| const normalized = value.trim(); | ||
| if (!normalized) { | ||
| return {}; | ||
| } | ||
| let parsed; | ||
| try { | ||
| parsed = yaml.load(normalized, {schema: yaml.FAILSAFE_SCHEMA}); | ||
| } catch (err) { | ||
| throw new Error(`Failed to parse build-secrets YAML: ${err.message}`); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can't this leak secret value? Same in bake.yml. |
||
| } | ||
| if (!parsed) { | ||
| return {}; | ||
| } | ||
| if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') { | ||
| throw new Error('build-secrets must be a YAML object'); | ||
| } | ||
| const secrets = {}; | ||
| for (const [id, secret] of Object.entries(parsed)) { | ||
| if (!/^[A-Za-z0-9_.-]+$/.test(id)) { | ||
| throw new Error(`Invalid build secret id "${id}": use letters, digits, dots, underscores or dashes`); | ||
| } | ||
| if (typeof secret !== 'string') { | ||
| throw new Error(`build-secrets value for "${id}" must be a string`); | ||
| } | ||
| if (secret.length === 0) { | ||
| throw new Error(`build-secrets value for "${id}" must not be empty`); | ||
| } | ||
| core.setSecret(secret); | ||
| secrets[id] = secret; | ||
| } | ||
| return secrets; | ||
| }; | ||
| const toBuildSecretEnvName = (id, index) => `BUILD_SECRET_${index}_${id.toUpperCase().replace(/[^A-Z0-9_]/g, '_')}`; | ||
|
|
||
| const gitContextAttrs = GitHub.context.ref.startsWith('refs/tags/') ? {checksum: GitHub.context.sha} : {'fetch-by-commit': 'true'}; | ||
| const buildContext = await new Build().gitContext({subdir: inpContext, attrs: gitContextAttrs}); | ||
|
|
@@ -803,6 +851,26 @@ jobs: | |
| } | ||
| core.setOutput('labels', labels.join('\n')); | ||
| core.setOutput('build-args', buildArgs); | ||
|
|
||
| let buildSecrets; | ||
| try { | ||
| buildSecrets = parseBuildSecrets(inpBuildSecrets); | ||
| } catch (err) { | ||
| core.setFailed(err.message); | ||
| return; | ||
| } | ||
| const envs = { | ||
| BUILDKIT_MULTI_PLATFORM: '1', | ||
| GIT_AUTH_TOKEN: inpGitHubToken | ||
| }; | ||
| const secretEnvs = ['GIT_AUTH_TOKEN=GIT_AUTH_TOKEN']; | ||
| Object.entries(buildSecrets).forEach(([id, secret], index) => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to verify that |
||
| const envName = toBuildSecretEnvName(id, index); | ||
| envs[envName] = secret; | ||
| secretEnvs.push(`${id}=${envName}`); | ||
| }); | ||
| core.setOutput('envs', JSON.stringify(envs)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these |
||
| core.setOutput('secret-envs', secretEnvs.join('\n')); | ||
|
|
||
| if (GitHub.context.payload.repository?.private ?? false) { | ||
| // if this is a private repository, we set min provenance mode | ||
|
|
@@ -833,13 +901,11 @@ jobs: | |
| platforms: ${{ steps.prepare.outputs.platform }} | ||
| provenance: ${{ steps.prepare.outputs.provenance }} | ||
| sbom: ${{ steps.prepare.outputs.sbom }} | ||
| secret-envs: GIT_AUTH_TOKEN=GIT_AUTH_TOKEN | ||
| secret-envs: ${{ steps.prepare.outputs.secret-envs }} | ||
| shm-size: ${{ inputs.shm-size }} | ||
| target: ${{ inputs.target }} | ||
| ulimit: ${{ inputs.ulimit }} | ||
| env: | ||
| BUILDKIT_MULTI_PLATFORM: 1 | ||
| GIT_AUTH_TOKEN: ${{ secrets.github-token || github.token }} | ||
| env: ${{ fromJson(steps.prepare.outputs.envs || '{}') }} | ||
| - | ||
| name: Login to registry for signing | ||
| if: ${{ needs.prepare.outputs.sign == 'true' && inputs.output == 'image' }} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # syntax=docker/dockerfile:1 | ||
|
|
||
| FROM alpine | ||
| RUN --mount=type=secret,id=fixture_plain,env=fixture_plain \ | ||
| --mount=type=secret,id=fixture_json,env=fixture_json \ | ||
| printf 'fixture_plain=%s\n' "$fixture_plain" && \ | ||
| printf 'fixture_json=%s\n' "$fixture_json" && \ | ||
| printf 'alpha-line\nbeta-line\n' > /tmp/expected && \ | ||
| printf '%s' "$fixture_plain" | cmp - /tmp/expected && \ | ||
| printf 'gamma-line\ndelta-line\n' > /tmp/expected-json && \ | ||
| printf '%s' "$fixture_json" | cmp - /tmp/expected-json |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
second
!parsedis dead code here