Skip to content

Commit 37dc0f0

Browse files
committed
history: add support for exporting multiple and all records
Signed-off-by: Tonis Tiigi <[email protected]>
1 parent dbcbaa0 commit 37dc0f0

File tree

5 files changed

+138
-29
lines changed

5 files changed

+138
-29
lines changed

commands/history/export.go

+46-26
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package history
33
import (
44
"context"
55
"io"
6+
"maps"
67
"os"
78
"slices"
89

@@ -14,14 +15,16 @@ import (
1415
"github.com/docker/buildx/util/confutil"
1516
"github.com/docker/buildx/util/desktop/bundle"
1617
"github.com/docker/cli/cli/command"
18+
"github.com/moby/buildkit/client"
1719
"github.com/pkg/errors"
1820
"github.com/spf13/cobra"
1921
)
2022

2123
type exportOptions struct {
2224
builder string
23-
ref string
25+
refs []string
2426
output string
27+
all bool
2528
}
2629

2730
func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) error {
@@ -40,40 +43,56 @@ func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) e
4043
}
4144
}
4245

43-
recs, err := queryRecords(ctx, opts.ref, nodes, &queryOptions{
44-
CompletedOnly: true,
45-
})
46-
if err != nil {
47-
return err
46+
if len(opts.refs) == 0 {
47+
opts.refs = []string{""}
4848
}
4949

50-
if len(recs) == 0 {
51-
if opts.ref == "" {
52-
return errors.New("no records found")
50+
var res []historyRecord
51+
for _, ref := range opts.refs {
52+
recs, err := queryRecords(ctx, ref, nodes, &queryOptions{
53+
CompletedOnly: true,
54+
})
55+
if err != nil {
56+
return err
5357
}
54-
return errors.Errorf("no record found for ref %q", opts.ref)
55-
}
5658

57-
if opts.ref == "" {
58-
slices.SortFunc(recs, func(a, b historyRecord) int {
59-
return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime())
60-
})
61-
}
59+
if len(recs) == 0 {
60+
if ref == "" {
61+
return errors.New("no records found")
62+
}
63+
return errors.Errorf("no record found for ref %q", ref)
64+
}
6265

63-
recs = recs[:1]
66+
if ref == "" {
67+
slices.SortFunc(recs, func(a, b historyRecord) int {
68+
return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime())
69+
})
70+
}
71+
72+
if opts.all {
73+
res = append(res, recs...)
74+
break
75+
} else {
76+
res = append(res, recs[0])
77+
}
78+
}
6479

6580
ls, err := localstate.New(confutil.NewConfig(dockerCli))
6681
if err != nil {
6782
return err
6883
}
6984

70-
c, err := recs[0].node.Driver.Client(ctx)
71-
if err != nil {
72-
return err
85+
clients := map[*client.Client]struct{}{}
86+
for _, rec := range res {
87+
c, err := rec.node.Driver.Client(ctx)
88+
if err != nil {
89+
return err
90+
}
91+
clients[c] = struct{}{}
7392
}
7493

75-
toExport := make([]*bundle.Record, 0, len(recs))
76-
for _, rec := range recs {
94+
toExport := make([]*bundle.Record, 0, len(res))
95+
for _, rec := range res {
7796
var defaultPlatform string
7897
if p := rec.node.Platforms; len(p) > 0 {
7998
defaultPlatform = platforms.FormatAll(platforms.Normalize(p[0]))
@@ -110,7 +129,7 @@ func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) e
110129
}
111130
}
112131

113-
return bundle.Export(ctx, c, w, toExport)
132+
return bundle.Export(ctx, maps.Keys(clients), w, toExport)
114133
}
115134

116135
func exportCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
@@ -119,11 +138,11 @@ func exportCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
119138
cmd := &cobra.Command{
120139
Use: "export [OPTIONS] [REF]",
121140
Short: "Export a build into Docker Desktop bundle",
122-
Args: cobra.MaximumNArgs(1),
123141
RunE: func(cmd *cobra.Command, args []string) error {
124-
if len(args) > 0 {
125-
options.ref = args[0]
142+
if options.all && len(args) > 0 {
143+
return errors.New("cannot specify refs when using --all")
126144
}
145+
options.refs = args
127146
options.builder = *rootOpts.Builder
128147
return runExport(cmd.Context(), dockerCli, options)
129148
},
@@ -132,6 +151,7 @@ func exportCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
132151

133152
flags := cmd.Flags()
134153
flags.StringVarP(&options.output, "output", "o", "", "Output file path")
154+
flags.BoolVar(&options.all, "all", false, "Export all records for the builder")
135155

136156
return cmd
137157
}

docs/reference/buildx_history_export.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Export a build into Docker Desktop bundle
77

88
| Name | Type | Default | Description |
99
|:-----------------|:---------|:--------|:-----------------------------------------|
10+
| `--all` | `bool` | | Export all records for the builder |
1011
| `--builder` | `string` | | Override the configured builder instance |
1112
| `-D`, `--debug` | `bool` | | Enable debug logging |
1213
| `-o`, `--output` | `string` | | Output file path |

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ require (
5353
go.opentelemetry.io/otel/metric v1.31.0
5454
go.opentelemetry.io/otel/sdk v1.31.0
5555
go.opentelemetry.io/otel/trace v1.31.0
56+
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
5657
golang.org/x/mod v0.22.0
5758
golang.org/x/sync v0.10.0
5859
golang.org/x/sys v0.29.0
@@ -170,7 +171,6 @@ require (
170171
go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect
171172
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
172173
golang.org/x/crypto v0.31.0 // indirect
173-
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
174174
golang.org/x/net v0.33.0 // indirect
175175
golang.org/x/oauth2 v0.23.0 // indirect
176176
golang.org/x/time v0.6.0 // indirect

util/desktop/bundle/content.go

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package bundle
2+
3+
import (
4+
"context"
5+
6+
"github.com/containerd/containerd/v2/core/content"
7+
cerrdefs "github.com/containerd/errdefs"
8+
digest "github.com/opencontainers/go-digest"
9+
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
10+
)
11+
12+
type nsFallbackStore struct {
13+
main content.Store
14+
fb content.Store
15+
}
16+
17+
var _ content.Store = &nsFallbackStore{}
18+
19+
func (c *nsFallbackStore) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) {
20+
info, err := c.main.Info(ctx, dgst)
21+
if err != nil {
22+
if cerrdefs.IsNotFound(err) {
23+
return c.fb.Info(ctx, dgst)
24+
}
25+
}
26+
return info, err
27+
}
28+
29+
func (c *nsFallbackStore) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) {
30+
return c.main.Update(ctx, info, fieldpaths...)
31+
}
32+
33+
func (c *nsFallbackStore) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error {
34+
seen := make(map[digest.Digest]struct{})
35+
err := c.main.Walk(ctx, func(i content.Info) error {
36+
seen[i.Digest] = struct{}{}
37+
return fn(i)
38+
}, filters...)
39+
if err != nil {
40+
return err
41+
}
42+
return c.fb.Walk(ctx, func(i content.Info) error {
43+
if _, ok := seen[i.Digest]; ok {
44+
return nil
45+
}
46+
return fn(i)
47+
}, filters...)
48+
}
49+
50+
func (c *nsFallbackStore) Delete(ctx context.Context, dgst digest.Digest) error {
51+
return c.main.Delete(ctx, dgst)
52+
}
53+
54+
func (c *nsFallbackStore) Status(ctx context.Context, ref string) (content.Status, error) {
55+
return c.main.Status(ctx, ref)
56+
}
57+
58+
func (c *nsFallbackStore) ListStatuses(ctx context.Context, filters ...string) ([]content.Status, error) {
59+
return c.main.ListStatuses(ctx, filters...)
60+
}
61+
62+
func (c *nsFallbackStore) Abort(ctx context.Context, ref string) error {
63+
return c.main.Abort(ctx, ref)
64+
}
65+
66+
func (c *nsFallbackStore) ReaderAt(ctx context.Context, desc ocispecs.Descriptor) (content.ReaderAt, error) {
67+
ra, err := c.main.ReaderAt(ctx, desc)
68+
if err != nil {
69+
if cerrdefs.IsNotFound(err) {
70+
return c.fb.ReaderAt(ctx, desc)
71+
}
72+
}
73+
return ra, err
74+
}
75+
76+
func (c *nsFallbackStore) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
77+
return c.main.Writer(ctx, opts...)
78+
}

util/desktop/bundle/export.go

+12-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,18 @@ type Record struct {
3232
StateGroup *localstate.StateGroup `json:"stateGroup,omitempty"`
3333
}
3434

35-
func Export(ctx context.Context, c *client.Client, w io.Writer, records []*Record) error {
36-
store := proxy.NewContentStore(c.ContentClient())
35+
func Export(ctx context.Context, c []*client.Client, w io.Writer, records []*Record) error {
36+
if len(c) == 0 {
37+
return errors.New("no buildkit client found")
38+
}
39+
store := proxy.NewContentStore(c[0].ContentClient())
40+
for _, c := range c[1:] {
41+
store = &nsFallbackStore{
42+
main: store,
43+
fb: proxy.NewContentStore(c.ContentClient()),
44+
}
45+
}
46+
3747
mp := contentutil.NewMultiProvider(store)
3848

3949
desc, err := export(ctx, mp, records)

0 commit comments

Comments
 (0)