Skip to content

Commit 8593e03

Browse files
authored
Merge pull request #3073 from tonistiigi/add-trace-export-command
history: add export command
2 parents 0c0e8ee + 45dfb84 commit 8593e03

File tree

7 files changed

+623
-0
lines changed

7 files changed

+623
-0
lines changed

commands/history/export.go

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package history
2+
3+
import (
4+
"context"
5+
"io"
6+
"os"
7+
"slices"
8+
9+
"github.com/containerd/console"
10+
"github.com/containerd/platforms"
11+
"github.com/docker/buildx/builder"
12+
"github.com/docker/buildx/localstate"
13+
"github.com/docker/buildx/util/cobrautil/completion"
14+
"github.com/docker/buildx/util/confutil"
15+
"github.com/docker/buildx/util/desktop/bundle"
16+
"github.com/docker/cli/cli/command"
17+
"github.com/moby/buildkit/client"
18+
"github.com/pkg/errors"
19+
"github.com/spf13/cobra"
20+
)
21+
22+
type exportOptions struct {
23+
builder string
24+
refs []string
25+
output string
26+
all bool
27+
}
28+
29+
func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) error {
30+
b, err := builder.New(dockerCli, builder.WithName(opts.builder))
31+
if err != nil {
32+
return err
33+
}
34+
35+
nodes, err := b.LoadNodes(ctx, builder.WithData())
36+
if err != nil {
37+
return err
38+
}
39+
for _, node := range nodes {
40+
if node.Err != nil {
41+
return node.Err
42+
}
43+
}
44+
45+
if len(opts.refs) == 0 {
46+
opts.refs = []string{""}
47+
}
48+
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
56+
}
57+
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+
}
64+
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+
}
78+
79+
ls, err := localstate.New(confutil.NewConfig(dockerCli))
80+
if err != nil {
81+
return err
82+
}
83+
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)
95+
}
96+
97+
toExport := make([]*bundle.Record, 0, len(res))
98+
for _, rec := range res {
99+
var defaultPlatform string
100+
if p := rec.node.Platforms; len(p) > 0 {
101+
defaultPlatform = platforms.FormatAll(platforms.Normalize(p[0]))
102+
}
103+
104+
var stg *localstate.StateGroup
105+
st, _ := ls.ReadRef(rec.node.Builder, rec.node.Name, rec.Ref)
106+
if st != nil && st.GroupRef != "" {
107+
stg, err = ls.ReadGroup(st.GroupRef)
108+
if err != nil {
109+
return err
110+
}
111+
}
112+
113+
toExport = append(toExport, &bundle.Record{
114+
BuildHistoryRecord: rec.BuildHistoryRecord,
115+
DefaultPlatform: defaultPlatform,
116+
LocalState: st,
117+
StateGroup: stg,
118+
})
119+
}
120+
121+
var w io.Writer = os.Stdout
122+
if opts.output != "" {
123+
f, err := os.Create(opts.output)
124+
if err != nil {
125+
return errors.Wrapf(err, "failed to create output file %q", opts.output)
126+
}
127+
defer f.Close()
128+
w = f
129+
} else {
130+
if _, err := console.ConsoleFromFile(os.Stdout); err == nil {
131+
return errors.Errorf("refusing to write to console, use --output to specify a file")
132+
}
133+
}
134+
135+
return bundle.Export(ctx, clients, w, toExport)
136+
}
137+
138+
func exportCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
139+
var options exportOptions
140+
141+
cmd := &cobra.Command{
142+
Use: "export [OPTIONS] [REF]",
143+
Short: "Export a build into Docker Desktop bundle",
144+
RunE: func(cmd *cobra.Command, args []string) error {
145+
if options.all && len(args) > 0 {
146+
return errors.New("cannot specify refs when using --all")
147+
}
148+
options.refs = args
149+
options.builder = *rootOpts.Builder
150+
return runExport(cmd.Context(), dockerCli, options)
151+
},
152+
ValidArgsFunction: completion.Disable,
153+
}
154+
155+
flags := cmd.Flags()
156+
flags.StringVarP(&options.output, "output", "o", "", "Output file path")
157+
flags.BoolVar(&options.all, "all", false, "Export all records for the builder")
158+
159+
return cmd
160+
}

commands/history/root.go

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func RootCmd(rootcmd *cobra.Command, dockerCli command.Cli, opts RootOptions) *c
2626
openCmd(dockerCli, opts),
2727
traceCmd(dockerCli, opts),
2828
importCmd(dockerCli, opts),
29+
exportCmd(dockerCli, opts),
2930
)
3031

3132
return cmd

docs/reference/buildx_history.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Commands to work on build records
77

88
| Name | Description |
99
|:---------------------------------------|:-----------------------------------------------|
10+
| [`export`](buildx_history_export.md) | Export a build into Docker Desktop bundle |
1011
| [`import`](buildx_history_import.md) | Import a build into Docker Desktop |
1112
| [`inspect`](buildx_history_inspect.md) | Inspect a build |
1213
| [`logs`](buildx_history_logs.md) | Print the logs of a build |
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# docker buildx history export
2+
3+
<!---MARKER_GEN_START-->
4+
Export a build into Docker Desktop bundle
5+
6+
### Options
7+
8+
| Name | Type | Default | Description |
9+
|:-----------------|:---------|:--------|:-----------------------------------------|
10+
| `--all` | `bool` | | Export all records for the builder |
11+
| `--builder` | `string` | | Override the configured builder instance |
12+
| `-D`, `--debug` | `bool` | | Enable debug logging |
13+
| `-o`, `--output` | `string` | | Output file path |
14+
15+
16+
<!---MARKER_GEN_END-->
17+

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+
}

0 commit comments

Comments
 (0)