Skip to content

Commit 45dfb84

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

File tree

5 files changed

+157
-32
lines changed

5 files changed

+157
-32
lines changed

commands/history/export.go

+49-26
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ import (
1414
"github.com/docker/buildx/util/confutil"
1515
"github.com/docker/buildx/util/desktop/bundle"
1616
"github.com/docker/cli/cli/command"
17+
"github.com/moby/buildkit/client"
1718
"github.com/pkg/errors"
1819
"github.com/spf13/cobra"
1920
)
2021

2122
type exportOptions struct {
2223
builder string
23-
ref string
24+
refs []string
2425
output string
26+
all bool
2527
}
2628

2729
func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) error {
@@ -40,40 +42,60 @@ func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) e
4042
}
4143
}
4244

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

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

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

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

6579
ls, err := localstate.New(confutil.NewConfig(dockerCli))
6680
if err != nil {
6781
return err
6882
}
6983

70-
c, err := recs[0].node.Driver.Client(ctx)
71-
if err != nil {
72-
return err
84+
visited := map[*builder.Node]struct{}{}
85+
var clients []*client.Client
86+
for _, rec := range res {
87+
if _, ok := visited[rec.node]; ok {
88+
continue
89+
}
90+
c, err := rec.node.Driver.Client(ctx)
91+
if err != nil {
92+
return err
93+
}
94+
clients = append(clients, c)
7395
}
7496

75-
toExport := make([]*bundle.Record, 0, len(recs))
76-
for _, rec := range recs {
97+
toExport := make([]*bundle.Record, 0, len(res))
98+
for _, rec := range res {
7799
var defaultPlatform string
78100
if p := rec.node.Platforms; len(p) > 0 {
79101
defaultPlatform = platforms.FormatAll(platforms.Normalize(p[0]))
@@ -110,7 +132,7 @@ func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) e
110132
}
111133
}
112134

113-
return bundle.Export(ctx, c, w, toExport)
135+
return bundle.Export(ctx, clients, w, toExport)
114136
}
115137

116138
func exportCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
@@ -119,11 +141,11 @@ func exportCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
119141
cmd := &cobra.Command{
120142
Use: "export [OPTIONS] [REF]",
121143
Short: "Export a build into Docker Desktop bundle",
122-
Args: cobra.MaximumNArgs(1),
123144
RunE: func(cmd *cobra.Command, args []string) error {
124-
if len(args) > 0 {
125-
options.ref = args[0]
145+
if options.all && len(args) > 0 {
146+
return errors.New("cannot specify refs when using --all")
126147
}
148+
options.refs = args
127149
options.builder = *rootOpts.Builder
128150
return runExport(cmd.Context(), dockerCli, options)
129151
},
@@ -132,6 +154,7 @@ func exportCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
132154

133155
flags := cmd.Flags()
134156
flags.StringVarP(&options.output, "output", "o", "", "Output file path")
157+
flags.BoolVar(&options.all, "all", false, "Export all records for the builder")
135158

136159
return cmd
137160
}

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 |

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

+17-2
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,23 @@ 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+
var store content.Store
37+
for _, c := range c {
38+
s := proxy.NewContentStore(c.ContentClient())
39+
if store == nil {
40+
store = s
41+
break
42+
}
43+
store = &nsFallbackStore{
44+
main: store,
45+
fb: s,
46+
}
47+
}
48+
if store == nil {
49+
return errors.New("no buildkit client found")
50+
}
51+
3752
mp := contentutil.NewMultiProvider(store)
3853

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

util/desktop/bundle/trace.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"regexp"
99
"strings"
10+
"sync"
1011

1112
"github.com/containerd/containerd/v2/core/content"
1213
"github.com/docker/buildx/util/otelutil"
@@ -17,15 +18,22 @@ import (
1718
)
1819

1920
var (
20-
sensitiveKeys = []string{"ghtoken", "token", "access_key_id", "secret_access_key", "session_token"}
21-
reAttrs = regexp.MustCompile(`(?i)(` + strings.Join(sensitiveKeys, "|") + `)=[^ ,]+`)
22-
reGhs = regexp.MustCompile(`ghs_[A-Za-z0-9]{36}`)
21+
reOnce sync.Once
22+
reAttrs, reGhs, reGhpat *regexp.Regexp
2323
)
2424

2525
func sanitizeCommand(value string) string {
26+
reOnce.Do(func() {
27+
sensitiveKeys := []string{"ghtoken", "token", "access_key_id", "secret_access_key", "session_token"}
28+
reAttrs = regexp.MustCompile(`(?i)(` + strings.Join(sensitiveKeys, "|") + `)=[^ ,]+`)
29+
reGhs = regexp.MustCompile(`(?:ghu|ghs)_[A-Za-z0-9]{36}`)
30+
reGhpat = regexp.MustCompile(`github_pat_\w{82}`)
31+
})
32+
2633
value = reAttrs.ReplaceAllString(value, "${1}=xxxxx")
27-
// reGhs is just double proofing. Not really needed.
34+
// reGhs/reGhpat is just double proofing. Not really needed.
2835
value = reGhs.ReplaceAllString(value, "xxxxx")
36+
value = reGhpat.ReplaceAllString(value, "xxxxx")
2937
return value
3038
}
3139

0 commit comments

Comments
 (0)