Skip to content

Commit b0ef2cd

Browse files
committed
feat: add support for Docker Buildx Bake mode with multi-registry push
1 parent 340c486 commit b0ef2cd

File tree

4 files changed

+239
-11
lines changed

4 files changed

+239
-11
lines changed

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,58 @@ 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+
192+
Bake with metadata output
193+
```yaml
194+
envVariables:
195+
PLUGIN_BAKE_FILE: docker-bake.hcl
196+
PLUGIN_BAKE_OPTIONS: "web"
197+
PLUGIN_METADATA_FILE: "/tmp/metadata.json"
198+
```
199+
148200
## Developer Notes
149201
150202
- 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: 82 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,18 @@ 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+
// If PLUGIN_CONFIG starts with '/' or './', treat it as a file path
207+
if strings.HasPrefix(p.Login.Config, "/") || strings.HasPrefix(p.Login.Config, "./") {
208+
data, err := os.ReadFile(p.Login.Config)
209+
if err != nil {
210+
return fmt.Errorf("Error reading docker config file %s: %s", p.Login.Config, err)
211+
}
212+
content = data
213+
} else {
214+
content = []byte(p.Login.Config)
215+
}
216+
if err := os.WriteFile(path, content, 0600); err != nil {
204217
return fmt.Errorf("Error writing config.json: %s", err)
205218
}
206219
}
@@ -257,7 +270,8 @@ func (p Plugin) Exec() error {
257270
}
258271

259272
// 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) {
273+
// NOTE: skip this auto-switch when Bake mode is active
274+
if p.Build.BakeFile == "" && len(p.Build.CacheTo) > 0 && (p.Builder.Driver == "" || p.Builder.Driver == defaultDriver) {
261275
p.Builder.Driver = dockerContainerDriver
262276
}
263277

@@ -359,21 +373,38 @@ func (p Plugin) Exec() error {
359373
}()
360374
}
361375

376+
// Enforce mutual exclusivity: Bake mode and Push-only mode cannot be used together
377+
if p.Build.BakeFile != "" && p.PushOnly {
378+
return fmt.Errorf("conflict: Bake mode (PLUGIN_BAKE_FILE) and Push-only mode (PLUGIN_PUSH_ONLY) cannot be used together")
379+
}
380+
362381
// Handle push-only mode if requested
363382
if p.PushOnly {
364383
return p.pushOnly()
365384
}
366385

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

372388
cmds = append(cmds, commandVersion()) // docker version
373389
cmds = append(cmds, commandInfo()) // docker info
374390

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

378409
// execute all commands in batch mode.
379410
for _, cmd := range cmds {
@@ -425,7 +456,7 @@ func (p Plugin) Exec() error {
425456
}
426457
}
427458

428-
if p.TarPath != "" && p.Dryrun {
459+
if p.Build.BakeFile == "" && p.TarPath != "" && p.Dryrun {
429460
if len(p.Build.Tags) > 0 {
430461
tag := p.Build.Tags[0]
431462
fullImageName := fmt.Sprintf("%s:%s", p.Build.Repo, tag)
@@ -457,11 +488,13 @@ func (p Plugin) Exec() error {
457488
}
458489
}
459490

460-
// output the adaptive card
461-
if p.Builder.Driver == defaultDriver {
491+
// output the adaptive card (skipped in Bake mode)
492+
if p.Build.BakeFile == "" && p.Builder.Driver == defaultDriver {
462493
if err := p.writeCard(); err != nil {
463494
fmt.Printf("Could not create adaptive card. %s\n", err)
464495
}
496+
} else if p.Build.BakeFile != "" {
497+
fmt.Println("Bake mode: skipping adaptive card output.")
465498
}
466499

467500
// write to artifact file
@@ -729,6 +762,44 @@ func commandBuildx(build Build, builder Builder, dryrun bool, metadataFile strin
729762
return exec.Command(dockerExe, args...)
730763
}
731764

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

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)