Skip to content

Commit a5ec4f7

Browse files
Ompragashclaude
andauthored
feat: [CI-18552]: add Docker Buildx Bake support with multi-registry capabilities (#74)
* feat: add support for Docker Buildx Bake mode with multi-registry push * Update BUILDX_URL for ARM64 architecture * Update docker.go * fix: improve PLUGIN_CONFIG handling with JSON-first validation - Use JSON validation to distinguish between config content and file paths - Prevents sensitive data exposure in error logs - Provides clearer, user-friendly error messages - Maintains full backward compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 340c486 commit a5ec4f7

File tree

5 files changed

+238
-12
lines changed

5 files changed

+238
-12
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,50 @@ envVariables:
145145
PLUGIN_BUILDX_OPTIONS_SEMICOLON: "--platform=linux/amd64,linux/arm64;--provenance=false;--output=type=tar,dest=image.tar"
146146
```
147147

148+
### Buildx Bake mode (opt-in)
149+
150+
Use Docker Buildx Bake when you have a bake file (HCL/JSON/Compose) and want build orchestration across multiple targets and registries.
151+
152+
Inputs:
153+
- PLUGIN_BAKE_FILE: Path to your bake file. When set, the plugin runs `docker buildx bake` instead of classic `buildx build`.
154+
- PLUGIN_BAKE_OPTIONS: Semicolon-delimited extra bake CLI args and/or target names. Example: `--progress=plain;web;api` or `--set=*.platform=linux/amd64`.
155+
156+
Behavior:
157+
- Do not include `--push` or `--load` in PLUGIN_BAKE_OPTIONS. The plugin adds these implicitly:
158+
- `--push` when PLUGIN_DRY_RUN=false (default).
159+
- `--load` when PLUGIN_DRY_RUN=true.
160+
- The existing `builder-name` is passed as `--builder` to bake if set.
161+
- The plugin does not auto-switch the builder driver in Bake mode. If your bake file uses `cache-to` (registry exports), set `PLUGIN_BUILDER_DRIVER=docker-container` explicitly.
162+
- If PLUGIN_METADATA_FILE is set, it is forwarded to bake as `--metadata-file`.
163+
- Bake mode ignores classic cache envs (PLUGIN_CACHE_FROM / PLUGIN_CACHE_TO / PLUGIN_NO_CACHE). Define cache in the bake file instead.
164+
- Bake mode and Push-only mode (PLUGIN_PUSH_ONLY) are mutually exclusive.
165+
- Classic tar export (PLUGIN_TAR_PATH) is not applied in Bake; define outputs in the bake file.
166+
167+
Examples:
168+
169+
Basic Bake with multi-registry push
170+
```yaml
171+
envVariables:
172+
PLUGIN_BAKE_FILE: docker-bake.hcl
173+
# Either pass a config file path or JSON content for multi-registry auth
174+
PLUGIN_CONFIG: /path/to/docker-config.json
175+
# PLUGIN_CONFIG: '{"auths":{"docker.io":{"auth":"..."},"ghcr.io":{"auth":"..."}}}'
176+
```
177+
178+
Bake with specific targets and progress
179+
```yaml
180+
envVariables:
181+
PLUGIN_BAKE_FILE: docker-bake.hcl
182+
PLUGIN_BAKE_OPTIONS: "--progress=plain;web;api"
183+
```
184+
185+
Bake with platform override
186+
```yaml
187+
envVariables:
188+
PLUGIN_BAKE_FILE: docker-bake.hcl
189+
PLUGIN_BAKE_OPTIONS: "--set=*.platform=linux/amd64"
190+
```
191+
148192
## Developer Notes
149193
150194
- When updating the base image, you will need to update for each architecture and OS.

app.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,16 @@ func Run() {
430430
Usage: "additional options to pass directly to the buildx command, separated by semicolons",
431431
EnvVar: "PLUGIN_BUILDX_OPTIONS_SEMICOLON",
432432
},
433+
cli.StringFlag{
434+
Name: "bake-file",
435+
Usage: "Buildx Bake definition file (HCL/JSON/Compose). When set, plugin runs docker buildx bake",
436+
EnvVar: "PLUGIN_BAKE_FILE",
437+
},
438+
cli.StringFlag{
439+
Name: "bake-options",
440+
Usage: "Semicolon-delimited extra bake CLI args and/or target names",
441+
EnvVar: "PLUGIN_BAKE_OPTIONS",
442+
},
433443
cli.BoolFlag{
434444
Name: "push-only",
435445
Usage: "skip build and only push images",
@@ -515,6 +525,8 @@ func run(c *cli.Context) error {
515525
HarnessSelfHostedGcpJsonKey: c.String("harness-self-hosted-gcp-json-key"),
516526
BuildxOptions: c.StringSlice("buildx-options"),
517527
BuildxOptionsSemicolon: c.String("buildx-options-semicolon"),
528+
BakeFile: c.String("bake-file"),
529+
BakeOptions: c.String("bake-options"),
518530
},
519531
Daemon: Daemon{
520532
Registry: c.String("docker.registry"),

docker.go

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ type (
9898
HarnessSelfHostedGcpJsonKey string // Harness self hosted gcp json region
9999
BuildxOptions []string // Generic buildx options passed directly to the buildx command
100100
BuildxOptionsSemicolon string // Buildx options separated by semicolons instead of commas
101+
// Buildx Bake (opt-in)
102+
BakeFile string // Buildx Bake definition file (HCL/JSON/Compose). If set, Bake mode is active
103+
BakeOptions string // Semicolon-delimited Bake options and/or target names
101104
}
102105

103106
// Plugin defines the Docker plugin parameters.
@@ -199,8 +202,24 @@ func (p Plugin) Exec() error {
199202
os.MkdirAll(dockerHome, 0600)
200203

201204
path := filepath.Join(dockerHome, "config.json")
202-
err := os.WriteFile(path, []byte(p.Login.Config), 0600)
203-
if err != nil {
205+
var content []byte
206+
// Try to parse as JSON first to determine if it's content or file path
207+
var jsonTest interface{}
208+
if json.Unmarshal([]byte(p.Login.Config), &jsonTest) == nil {
209+
// Valid JSON, treat as content
210+
content = []byte(p.Login.Config)
211+
} else if _, err := os.Stat(p.Login.Config); err == nil {
212+
// Not JSON but file exists, read it
213+
data, err := os.ReadFile(p.Login.Config)
214+
if err != nil {
215+
return fmt.Errorf("Error reading docker config file '%s': %s", p.Login.Config, err)
216+
}
217+
content = data
218+
} else {
219+
// Neither valid JSON nor existing file
220+
return fmt.Errorf("Docker config must be either valid JSON content or a path to an existing config file")
221+
}
222+
if err := os.WriteFile(path, content, 0600); err != nil {
204223
return fmt.Errorf("Error writing config.json: %s", err)
205224
}
206225
}
@@ -257,7 +276,8 @@ func (p Plugin) Exec() error {
257276
}
258277

259278
// cache export feature is currently not supported for docker driver hence we have to create docker-container driver
260-
if len(p.Build.CacheTo) > 0 && (p.Builder.Driver == "" || p.Builder.Driver == defaultDriver) {
279+
// NOTE: skip this auto-switch when Bake mode is active
280+
if p.Build.BakeFile == "" && len(p.Build.CacheTo) > 0 && (p.Builder.Driver == "" || p.Builder.Driver == defaultDriver) {
261281
p.Builder.Driver = dockerContainerDriver
262282
}
263283

@@ -359,21 +379,38 @@ func (p Plugin) Exec() error {
359379
}()
360380
}
361381

382+
// Enforce mutual exclusivity: Bake mode and Push-only mode cannot be used together
383+
if p.Build.BakeFile != "" && p.PushOnly {
384+
return fmt.Errorf("conflict: Bake mode (PLUGIN_BAKE_FILE) and Push-only mode (PLUGIN_PUSH_ONLY) cannot be used together")
385+
}
386+
362387
// Handle push-only mode if requested
363388
if p.PushOnly {
364389
return p.pushOnly()
365390
}
366391

367-
// add proxy build args
368-
addProxyBuildArgs(&p.Build)
369-
370392
var cmds []*exec.Cmd
371393

372394
cmds = append(cmds, commandVersion()) // docker version
373395
cmds = append(cmds, commandInfo()) // docker info
374396

375-
// Command to build, tag and push
376-
cmds = append(cmds, commandBuildx(p.Build, p.Builder, p.Dryrun, p.MetadataFile, p.TarPath)) // docker build
397+
// Determine execution path: Bake mode vs Classic buildx build
398+
if p.Build.BakeFile != "" {
399+
// Inform about ignored classic cache settings
400+
if len(p.Build.CacheFrom) > 0 || len(p.Build.CacheTo) > 0 || p.Build.NoCache {
401+
fmt.Println("Bake mode: ignoring PLUGIN_CACHE_*; define cache in the bake file.")
402+
}
403+
// Tar export is not applied in Bake mode
404+
if p.TarPath != "" {
405+
fmt.Println("Bake mode: ignoring PLUGIN_TAR_PATH; define outputs in the bake file.")
406+
}
407+
// Command to run buildx bake
408+
cmds = append(cmds, commandBuildxBake(p.Build, p.Builder, p.Dryrun, p.MetadataFile))
409+
} else {
410+
// Classic path: add proxy build args and run buildx build
411+
addProxyBuildArgs(&p.Build)
412+
cmds = append(cmds, commandBuildx(p.Build, p.Builder, p.Dryrun, p.MetadataFile, p.TarPath)) // docker build
413+
}
377414

378415
// execute all commands in batch mode.
379416
for _, cmd := range cmds {
@@ -425,7 +462,7 @@ func (p Plugin) Exec() error {
425462
}
426463
}
427464

428-
if p.TarPath != "" && p.Dryrun {
465+
if p.Build.BakeFile == "" && p.TarPath != "" && p.Dryrun {
429466
if len(p.Build.Tags) > 0 {
430467
tag := p.Build.Tags[0]
431468
fullImageName := fmt.Sprintf("%s:%s", p.Build.Repo, tag)
@@ -457,11 +494,13 @@ func (p Plugin) Exec() error {
457494
}
458495
}
459496

460-
// output the adaptive card
461-
if p.Builder.Driver == defaultDriver {
497+
// output the adaptive card (skipped in Bake mode)
498+
if p.Build.BakeFile == "" && p.Builder.Driver == defaultDriver {
462499
if err := p.writeCard(); err != nil {
463500
fmt.Printf("Could not create adaptive card. %s\n", err)
464501
}
502+
} else if p.Build.BakeFile != "" {
503+
fmt.Println("Bake mode: skipping adaptive card output.")
465504
}
466505

467506
// write to artifact file
@@ -729,6 +768,44 @@ func commandBuildx(build Build, builder Builder, dryrun bool, metadataFile strin
729768
return exec.Command(dockerExe, args...)
730769
}
731770

771+
// helper function to create the docker buildx bake command.
772+
func commandBuildxBake(build Build, builder Builder, dryrun bool, metadataFile string) *exec.Cmd {
773+
args := []string{"buildx", "bake"}
774+
775+
if build.BakeFile != "" {
776+
args = append(args, "-f", build.BakeFile)
777+
}
778+
if builder.Name != "" {
779+
args = append(args, "--builder", builder.Name)
780+
}
781+
782+
if dryrun {
783+
args = append(args, "--load")
784+
} else {
785+
args = append(args, "--push")
786+
}
787+
788+
if metadataFile != "" {
789+
args = append(args, "--metadata-file", metadataFile)
790+
}
791+
792+
if build.BakeOptions != "" {
793+
tokens := strings.Split(build.BakeOptions, ";")
794+
for _, t := range tokens {
795+
t = strings.TrimSpace(t)
796+
if t == "" {
797+
continue
798+
}
799+
if t == "--push" || t == "--load" {
800+
continue
801+
}
802+
args = append(args, t)
803+
}
804+
}
805+
806+
return exec.Command(dockerExe, args...)
807+
}
808+
732809
func sanitizeCacheCommand(build *Build) {
733810
// Helper function to sanitize cache arguments
734811
sanitizeCacheArgs := func(args []string) []string {

docker/docker/Dockerfile.linux.arm64

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ ENV BUILDKIT_PROGRESS=plain
77
ENV DOCKER_CLI_EXPERIMENTAL=enabled
88
ENV PLUGIN_BUILDKIT_ASSETS_DIR=/buildkit
99

10-
ARG BUILDX_URL=https://github.com/docker/buildx/releases/download/v0.23.0/buildx-v0.23.0.linux-amd64
10+
ARG BUILDX_URL=https://github.com/docker/buildx/releases/download/v0.23.0/buildx-v0.23.0.linux-arm64
1111

1212
RUN mkdir -p $HOME/.docker/cli-plugins && \
1313
wget -O $HOME/.docker/cli-plugins/docker-buildx $BUILDX_URL && \

docker_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,99 @@ func TestCommandBuildx(t *testing.T) {
247247
}
248248
}
249249

250+
func TestCommandBuildxBake(t *testing.T) {
251+
tcs := []struct {
252+
name string
253+
build Build
254+
builder Builder
255+
dryrun bool
256+
metadata string
257+
want *exec.Cmd
258+
}{
259+
{
260+
name: "basic bake with file, push by default",
261+
build: Build{
262+
BakeFile: "docker-bake.hcl",
263+
BakeOptions: "",
264+
},
265+
want: exec.Command(
266+
dockerExe,
267+
"buildx",
268+
"bake",
269+
"-f",
270+
"docker-bake.hcl",
271+
"--push",
272+
),
273+
},
274+
{
275+
name: "with builder and dryrun -> load",
276+
build: Build{
277+
BakeFile: "docker-bake.hcl",
278+
},
279+
builder: Builder{
280+
Name: "mybuilder",
281+
},
282+
dryrun: true,
283+
want: exec.Command(
284+
dockerExe,
285+
"buildx",
286+
"bake",
287+
"-f",
288+
"docker-bake.hcl",
289+
"--builder",
290+
"mybuilder",
291+
"--load",
292+
),
293+
},
294+
{
295+
name: "options and targets parsed; ignore push/load in options",
296+
build: Build{
297+
BakeFile: "docker-bake.hcl",
298+
BakeOptions: "--progress=plain;web;--push;api;--load",
299+
},
300+
// dryrun false -> plugin adds --push
301+
want: exec.Command(
302+
dockerExe,
303+
"buildx",
304+
"bake",
305+
"-f",
306+
"docker-bake.hcl",
307+
"--push",
308+
"--progress=plain",
309+
"web",
310+
"api",
311+
),
312+
},
313+
{
314+
name: "with metadata file",
315+
build: Build{
316+
BakeFile: "docker-bake.hcl",
317+
},
318+
metadata: "/tmp/meta.json",
319+
want: exec.Command(
320+
dockerExe,
321+
"buildx",
322+
"bake",
323+
"-f",
324+
"docker-bake.hcl",
325+
"--push",
326+
"--metadata-file",
327+
"/tmp/meta.json",
328+
),
329+
},
330+
}
331+
332+
for _, tc := range tcs {
333+
tc := tc
334+
t.Run(tc.name, func(t *testing.T) {
335+
cmd := commandBuildxBake(tc.build, tc.builder, tc.dryrun, tc.metadata)
336+
if !reflect.DeepEqual(cmd.String(), tc.want.String()) {
337+
t.Errorf("Got cmd %v, want %v", cmd, tc.want)
338+
}
339+
})
340+
}
341+
}
342+
250343
func TestSanitizeCacheCommand(t *testing.T) {
251344
tests := []struct {
252345
name string

0 commit comments

Comments
 (0)