Skip to content

Commit 0543877

Browse files
committed
bake: use secret source overrides
Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
1 parent 959dbe2 commit 0543877

5 files changed

Lines changed: 37 additions & 76 deletions

File tree

.github/workflows/.test-bake.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ jobs:
480480
fixture_plain: |
481481
alpha-line
482482
beta-line
483-
secret.fixture_json: ${{ toJSON(format('gamma-line{0}delta-line{0}', fromJSON('"\n"'))) }}
483+
foosec.fixture_json: ${{ toJSON(format('gamma-line{0}delta-line{0}', fromJSON('"\n"'))) }}
484484
485485
bake-set-runner:
486486
uses: ./.github/workflows/bake.yml

.github/workflows/bake.yml

Lines changed: 24 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ jobs:
212212
metaImages: ${{ steps.set.outputs.metaImages }}
213213
sign: ${{ steps.set.outputs.sign }}
214214
privateRepo: ${{ steps.set.outputs.privateRepo }}
215-
secretIds: ${{ steps.set.outputs.secretIds }}
215+
targets: ${{ steps.set.outputs.targets }}
216216
ghaCacheSign: ${{ steps.set.outputs.ghaCacheSign }}
217217
steps:
218218
-
@@ -523,16 +523,6 @@ jobs:
523523
const match = value.match(/^target:(.+)$/);
524524
return match ? match[1] : undefined;
525525
};
526-
const parseSecretId = secret => {
527-
if (typeof secret === 'string') {
528-
const idAttr = secret.split(',').map(attr => attr.trim()).find(attr => attr.startsWith('id='));
529-
return idAttr ? idAttr.substring(3) : undefined;
530-
}
531-
if (secret && typeof secret === 'object' && typeof secret.id === 'string') {
532-
return secret.id;
533-
}
534-
return undefined;
535-
};
536526
const resolveTarget = () => {
537527
if (targetDefs[inpTarget]) {
538528
return inpTarget;
@@ -561,11 +551,7 @@ jobs:
561551
if (unsupportedTargets.length > 0) {
562552
throw new Error(`Only one target can be built at once, found unsupported targets: ${unsupportedTargets.join(', ')}`);
563553
}
564-
const secretIds = {};
565-
for (const name of allowedTargets) {
566-
secretIds[name] = (targetDefs[name]?.secret || []).map(parseSecretId).filter(Boolean);
567-
}
568-
core.setOutput('secretIds', JSON.stringify(secretIds));
554+
core.setOutput('targets', JSON.stringify([...allowedTargets]));
569555
});
570556
} catch (error) {
571557
core.setFailed(error);
@@ -852,7 +838,7 @@ jobs:
852838
INPUT_CACHE-SCOPE: ${{ inputs.cache-scope }}
853839
INPUT_CACHE-MODE: ${{ inputs.cache-mode }}
854840
INPUT_BUILD-SECRETS: ${{ secrets.build-secrets }}
855-
INPUT_SECRET-IDS: ${{ needs.prepare.outputs.secretIds }}
841+
INPUT_TARGETS: ${{ needs.prepare.outputs.targets }}
856842
INPUT_CONTEXT: ${{ inputs.context }}
857843
INPUT_FILES: ${{ inputs.files }}
858844
INPUT_OUTPUT: ${{ inputs.output }}
@@ -895,7 +881,7 @@ jobs:
895881
const inpCacheScope = core.getInput('cache-scope');
896882
const inpCacheMode = core.getInput('cache-mode');
897883
const inpBuildSecrets = core.getInput('build-secrets');
898-
const inpSecretIds = core.getInput('secret-ids');
884+
const inpTargets = core.getInput('targets');
899885
const inpContext = core.getInput('context');
900886
const inpFiles = Util.getInputList('files');
901887
const inpOutput = core.getInput('output');
@@ -920,6 +906,11 @@ jobs:
920906
};
921907
const renderTemplate = value => Util.compileHandlebars(value, {noEscape: true}, {meta});
922908
909+
const isInputKeySafe = value => value && !/[\r\n=]/.test(value);
910+
const parseBuildSecretKey = key => {
911+
const separator = key.lastIndexOf('.');
912+
return separator === -1 ? {target: inpTarget, id: key} : {target: key.substring(0, separator), id: key.substring(separator + 1)};
913+
};
923914
const parseBuildSecrets = value => {
924915
const normalized = value.trim();
925916
if (!normalized) {
@@ -938,50 +929,18 @@ jobs:
938929
if (Array.isArray(parsed) || typeof parsed !== 'object') {
939930
throw new Error('build-secrets must be a YAML object');
940931
}
941-
const secrets = [];
942-
const seen = new Set();
943-
for (const [key, secret] of Object.entries(parsed)) {
944-
const separator = key.lastIndexOf('.');
945-
const target = separator === -1 ? inpTarget : key.substring(0, separator);
946-
const id = separator === -1 ? key : key.substring(separator + 1);
947-
if (!target) {
948-
throw new Error(`Invalid build secret target for "${key}": target must not be empty`);
949-
}
950-
if (!/^[A-Za-z0-9_.-]+$/.test(target)) {
951-
throw new Error(`Invalid build secret target "${target}": use letters, digits, dots, underscores or dashes`);
952-
}
953-
if (!/^[A-Za-z0-9_-]+$/.test(id)) {
954-
throw new Error(`Invalid build secret id "${id}": use letters, digits, underscores or dashes`);
932+
return Object.entries(parsed).map(([key, secret]) => {
933+
const {target, id} = parseBuildSecretKey(key);
934+
if (!isInputKeySafe(target) || !isInputKeySafe(id)) {
935+
throw new Error(`Invalid build-secrets key "${key}": use "secret_id" or "target.secret_id" without empty names, line breaks or "="`);
955936
}
956937
if (typeof secret !== 'string') {
957938
throw new Error(`build-secrets value for "${key}" must be a string`);
958939
}
959-
if (secret.length === 0) {
960-
throw new Error(`build-secrets value for "${key}" must not be empty`);
961-
}
962-
const ref = `${target}\0${id}`;
963-
if (seen.has(ref)) {
964-
throw new Error(`Build secret id "${id}" is defined more than once for target "${target}"`);
965-
}
966-
seen.add(ref);
967940
core.setSecret(secret);
968-
secrets.push({target, id, secret});
969-
}
970-
return secrets;
971-
};
972-
973-
const validateBuildSecrets = (secrets, secretIds) => {
974-
for (const {target, id} of secrets) {
975-
const targetSecretIds = secretIds[target];
976-
if (!targetSecretIds) {
977-
throw new Error(`Build secret target "${target}" is not part of the resolved Bake definition`);
978-
}
979-
if (!Array.isArray(targetSecretIds) || !targetSecretIds.includes(id)) {
980-
throw new Error(`Build secret "${id}" must be declared in Bake target "${target}" before it can be provided through build-secrets`);
981-
}
982-
}
941+
return {target, id, secret};
942+
});
983943
};
984-
985944
const toBuildSecretEnvName = (id, index) => `BUILD_SECRET_${index}_${id.toUpperCase().replace(/[^A-Z0-9_]/g, '_')}`;
986945
987946
const gitContextAttrs = GitHub.context.ref.startsWith('refs/tags/') ? {checksum: GitHub.context.sha} : {'fetch-by-commit': 'true'};
@@ -1010,10 +969,15 @@ jobs:
1010969
return;
1011970
}
1012971
1013-
let secretIds;
972+
let targets;
1014973
try {
1015-
secretIds = JSON.parse(inpSecretIds || '{}');
1016-
validateBuildSecrets(buildSecrets, secretIds);
974+
targets = JSON.parse(inpTargets || '[]');
975+
const allowedTargets = new Set(targets);
976+
for (const {target} of buildSecrets) {
977+
if (!allowedTargets.has(target)) {
978+
throw new Error(`Build secret target "${target}" is not part of the resolved Bake definition`);
979+
}
980+
}
1017981
} catch (err) {
1018982
core.setFailed(err.message);
1019983
return;
@@ -1036,7 +1000,7 @@ jobs:
10361000
buildSecrets.forEach(({target, id, secret}, index) => {
10371001
const envName = toBuildSecretEnvName(id, index);
10381002
envs[envName] = secret;
1039-
secretOverrides.push(`${target}.secrets+=id=${id},env=${envName}`);
1003+
secretOverrides.push(`${target}.secret.${id}=env=${envName}`);
10401004
});
10411005
await core.group(`Set envs`, async () => {
10421006
core.info(JSON.stringify(Object.keys(envs).sort(), null, 2));

.github/workflows/build.yml

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -800,9 +800,9 @@ jobs:
800800
version: inpMetaVersion,
801801
tags: inpMetaTags
802802
};
803-
804803
const renderTemplate = value => Util.compileHandlebars(value, {noEscape: true}, {meta});
805-
const toMultilineInput = value => value.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
804+
805+
const isInputKeySafe = value => value && !/[\r\n=]/.test(value);
806806
const parseBuildSecrets = value => {
807807
const normalized = value.trim();
808808
if (!normalized) {
@@ -821,24 +821,19 @@ jobs:
821821
if (Array.isArray(parsed) || typeof parsed !== 'object') {
822822
throw new Error('build-secrets must be a YAML object');
823823
}
824-
const secrets = {};
825824
for (const [id, secret] of Object.entries(parsed)) {
826-
if (!/^[A-Za-z0-9_-]+$/.test(id)) {
827-
throw new Error(`Invalid build secret id "${id}": use letters, digits, underscores or dashes`);
825+
if (!isInputKeySafe(id)) {
826+
throw new Error(`Invalid build secret id "${id}": must not be empty or contain line breaks or "="`);
828827
}
829828
if (id === 'GIT_AUTH_TOKEN') {
830829
throw new Error('Build secret id "GIT_AUTH_TOKEN" is reserved for Git context authentication');
831830
}
832831
if (typeof secret !== 'string') {
833832
throw new Error(`build-secrets value for "${id}" must be a string`);
834833
}
835-
if (secret.length === 0) {
836-
throw new Error(`build-secrets value for "${id}" must not be empty`);
837-
}
838834
core.setSecret(secret);
839-
secrets[id] = secret;
840835
}
841-
return secrets;
836+
return parsed;
842837
};
843838
const toBuildSecretEnvName = (id, index) => `BUILD_SECRET_${index}_${id.toUpperCase().replace(/[^A-Z0-9_]/g, '_')}`;
844839
@@ -879,6 +874,7 @@ jobs:
879874
let labels;
880875
let buildArgs;
881876
try {
877+
const toMultilineInput = value => value.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
882878
annotations = toMultilineInput(renderTemplate(inpAnnotations));
883879
labels = toMultilineInput(renderTemplate(inpLabels));
884880
buildArgs = renderTemplate(inpBuildArgs);

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ with `builder-outputs: ${{ toJSON(needs.<job_id>.outputs) }}`.
398398
The `build-secrets` secret is shared by the build and bake workflows. It must
399399
be a YAML object. For the build workflow, each key is the BuildKit secret ID and
400400
each value is the secret payload. The bake workflow accepts the same unqualified
401-
keys. Secret IDs may contain letters, digits, underscores, and dashes:
401+
keys.
402402

403403
```yaml
404404
secrets:
@@ -415,11 +415,12 @@ or YAML syntax are preserved as a single YAML scalar.
415415

416416
Each secret is exposed as an env-backed BuildKit secret. The build workflow
417417
passes these values through `docker/build-push-action` `secret-envs`, and the
418-
bake workflow appends matching `target.secrets+=id=...,env=...` overrides. For
418+
bake workflow sets matching `target.secret.<id>=env=...` source overrides. For
419419
the bake workflow, an unqualified key applies to the workflow `target` input. A
420420
key written as `target.secret_id` applies only to that Bake target. This
421-
target-scoped key form is only accepted by the bake workflow. The target must
422-
already declare a matching secret ID in the Bake definition:
421+
target-scoped key form is only accepted by the bake workflow. The target must be
422+
part of the resolved Bake build, and Buildx requires the target to already
423+
declare a matching secret ID in the Bake definition:
423424

424425
```yaml
425426
secrets:

test/docker-bake.hcl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ target "hello-cross" {
3838
platforms = ["linux/amd64", "linux/arm64"]
3939
}
4040

41-
target "secret" {
41+
target "foosec" {
4242
dockerfile = "secret.Dockerfile"
4343
secret = [
4444
"id=fixture_plain,env=FIXTURE_PLAIN",

0 commit comments

Comments
 (0)