-
Notifications
You must be signed in to change notification settings - Fork 617
history: add export command #3073
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,160 @@ | ||||||||||||||||
| package history | ||||||||||||||||
|
|
||||||||||||||||
| import ( | ||||||||||||||||
| "context" | ||||||||||||||||
| "io" | ||||||||||||||||
| "os" | ||||||||||||||||
| "slices" | ||||||||||||||||
|
|
||||||||||||||||
| "github.com/containerd/console" | ||||||||||||||||
| "github.com/containerd/platforms" | ||||||||||||||||
| "github.com/docker/buildx/builder" | ||||||||||||||||
| "github.com/docker/buildx/localstate" | ||||||||||||||||
| "github.com/docker/buildx/util/cobrautil/completion" | ||||||||||||||||
| "github.com/docker/buildx/util/confutil" | ||||||||||||||||
| "github.com/docker/buildx/util/desktop/bundle" | ||||||||||||||||
| "github.com/docker/cli/cli/command" | ||||||||||||||||
| "github.com/moby/buildkit/client" | ||||||||||||||||
| "github.com/pkg/errors" | ||||||||||||||||
| "github.com/spf13/cobra" | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| type exportOptions struct { | ||||||||||||||||
| builder string | ||||||||||||||||
| refs []string | ||||||||||||||||
| output string | ||||||||||||||||
| all bool | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) error { | ||||||||||||||||
| b, err := builder.New(dockerCli, builder.WithName(opts.builder)) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return err | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| nodes, err := b.LoadNodes(ctx, builder.WithData()) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return err | ||||||||||||||||
| } | ||||||||||||||||
| for _, node := range nodes { | ||||||||||||||||
| if node.Err != nil { | ||||||||||||||||
| return node.Err | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if len(opts.refs) == 0 { | ||||||||||||||||
| opts.refs = []string{""} | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| var res []historyRecord | ||||||||||||||||
| for _, ref := range opts.refs { | ||||||||||||||||
| recs, err := queryRecords(ctx, ref, nodes, &queryOptions{ | ||||||||||||||||
| CompletedOnly: true, | ||||||||||||||||
| }) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return err | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if len(recs) == 0 { | ||||||||||||||||
| if ref == "" { | ||||||||||||||||
| return errors.New("no records found") | ||||||||||||||||
| } | ||||||||||||||||
| return errors.Errorf("no record found for ref %q", ref) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if ref == "" { | ||||||||||||||||
| slices.SortFunc(recs, func(a, b historyRecord) int { | ||||||||||||||||
| return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime()) | ||||||||||||||||
| }) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| if opts.all { | ||||||||||||||||
| res = append(res, recs...) | ||||||||||||||||
| break | ||||||||||||||||
| } else { | ||||||||||||||||
| res = append(res, recs[0]) | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| ls, err := localstate.New(confutil.NewConfig(dockerCli)) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return err | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| visited := map[*builder.Node]struct{}{} | ||||||||||||||||
| var clients []*client.Client | ||||||||||||||||
| for _, rec := range res { | ||||||||||||||||
| if _, ok := visited[rec.node]; ok { | ||||||||||||||||
| continue | ||||||||||||||||
| } | ||||||||||||||||
| c, err := rec.node.Driver.Client(ctx) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return err | ||||||||||||||||
| } | ||||||||||||||||
| clients = append(clients, c) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| toExport := make([]*bundle.Record, 0, len(res)) | ||||||||||||||||
| for _, rec := range res { | ||||||||||||||||
| var defaultPlatform string | ||||||||||||||||
| if p := rec.node.Platforms; len(p) > 0 { | ||||||||||||||||
| defaultPlatform = platforms.FormatAll(platforms.Normalize(p[0])) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| var stg *localstate.StateGroup | ||||||||||||||||
| st, _ := ls.ReadRef(rec.node.Builder, rec.node.Name, rec.Ref) | ||||||||||||||||
| if st != nil && st.GroupRef != "" { | ||||||||||||||||
| stg, err = ls.ReadGroup(st.GroupRef) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return err | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| toExport = append(toExport, &bundle.Record{ | ||||||||||||||||
| BuildHistoryRecord: rec.BuildHistoryRecord, | ||||||||||||||||
| DefaultPlatform: defaultPlatform, | ||||||||||||||||
| LocalState: st, | ||||||||||||||||
| StateGroup: stg, | ||||||||||||||||
| }) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| var w io.Writer = os.Stdout | ||||||||||||||||
| if opts.output != "" { | ||||||||||||||||
| f, err := os.Create(opts.output) | ||||||||||||||||
| if err != nil { | ||||||||||||||||
| return errors.Wrapf(err, "failed to create output file %q", opts.output) | ||||||||||||||||
| } | ||||||||||||||||
| defer f.Close() | ||||||||||||||||
| w = f | ||||||||||||||||
| } else { | ||||||||||||||||
| if _, err := console.ConsoleFromFile(os.Stdout); err == nil { | ||||||||||||||||
| return errors.Errorf("refusing to write to console, use --output to specify a file") | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| return bundle.Export(ctx, clients, w, toExport) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| func exportCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command { | ||||||||||||||||
| var options exportOptions | ||||||||||||||||
|
|
||||||||||||||||
| cmd := &cobra.Command{ | ||||||||||||||||
| Use: "export [OPTIONS] [REF]", | ||||||||||||||||
| Short: "Export a build into Docker Desktop bundle", | ||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if "Docker Desktop" should be stipulated here. I think the following is enough:
Suggested change
or
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also there are some places where we talk about build and other build record. I think we should be consistent: buildx/commands/history/import.go Line 123 in 18ccba0
buildx/commands/history/logs.go Line 101 in 18ccba0
buildx/commands/history/open.go Line 60 in 18ccba0
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It kind of shows what you can do with the bundle file. |
||||||||||||||||
| RunE: func(cmd *cobra.Command, args []string) error { | ||||||||||||||||
| if options.all && len(args) > 0 { | ||||||||||||||||
| return errors.New("cannot specify refs when using --all") | ||||||||||||||||
| } | ||||||||||||||||
| options.refs = args | ||||||||||||||||
| options.builder = *rootOpts.Builder | ||||||||||||||||
| return runExport(cmd.Context(), dockerCli, options) | ||||||||||||||||
| }, | ||||||||||||||||
| ValidArgsFunction: completion.Disable, | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| flags := cmd.Flags() | ||||||||||||||||
| flags.StringVarP(&options.output, "output", "o", "", "Output file path") | ||||||||||||||||
| flags.BoolVar(&options.all, "all", false, "Export all records for the builder") | ||||||||||||||||
|
|
||||||||||||||||
| return cmd | ||||||||||||||||
| } | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # docker buildx history export | ||
|
|
||
| <!---MARKER_GEN_START--> | ||
| Export a build into Docker Desktop bundle | ||
|
|
||
| ### Options | ||
|
|
||
| | Name | Type | Default | Description | | ||
| |:-----------------|:---------|:--------|:-----------------------------------------| | ||
| | `--all` | `bool` | | Export all records for the builder | | ||
| | `--builder` | `string` | | Override the configured builder instance | | ||
| | `-D`, `--debug` | `bool` | | Enable debug logging | | ||
| | `-o`, `--output` | `string` | | Output file path | | ||
|
|
||
|
|
||
| <!---MARKER_GEN_END--> | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| package bundle | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/containerd/containerd/v2/core/content" | ||
| cerrdefs "github.com/containerd/errdefs" | ||
| digest "github.com/opencontainers/go-digest" | ||
| ocispecs "github.com/opencontainers/image-spec/specs-go/v1" | ||
| ) | ||
|
|
||
| type nsFallbackStore struct { | ||
| main content.Store | ||
| fb content.Store | ||
| } | ||
|
|
||
| var _ content.Store = &nsFallbackStore{} | ||
|
|
||
| func (c *nsFallbackStore) Info(ctx context.Context, dgst digest.Digest) (content.Info, error) { | ||
| info, err := c.main.Info(ctx, dgst) | ||
| if err != nil { | ||
| if cerrdefs.IsNotFound(err) { | ||
| return c.fb.Info(ctx, dgst) | ||
| } | ||
| } | ||
| return info, err | ||
| } | ||
|
|
||
| func (c *nsFallbackStore) Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error) { | ||
| return c.main.Update(ctx, info, fieldpaths...) | ||
| } | ||
|
|
||
| func (c *nsFallbackStore) Walk(ctx context.Context, fn content.WalkFunc, filters ...string) error { | ||
| seen := make(map[digest.Digest]struct{}) | ||
| err := c.main.Walk(ctx, func(i content.Info) error { | ||
| seen[i.Digest] = struct{}{} | ||
| return fn(i) | ||
| }, filters...) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return c.fb.Walk(ctx, func(i content.Info) error { | ||
| if _, ok := seen[i.Digest]; ok { | ||
| return nil | ||
| } | ||
| return fn(i) | ||
| }, filters...) | ||
| } | ||
|
|
||
| func (c *nsFallbackStore) Delete(ctx context.Context, dgst digest.Digest) error { | ||
| return c.main.Delete(ctx, dgst) | ||
| } | ||
|
|
||
| func (c *nsFallbackStore) Status(ctx context.Context, ref string) (content.Status, error) { | ||
| return c.main.Status(ctx, ref) | ||
| } | ||
|
|
||
| func (c *nsFallbackStore) ListStatuses(ctx context.Context, filters ...string) ([]content.Status, error) { | ||
| return c.main.ListStatuses(ctx, filters...) | ||
| } | ||
|
|
||
| func (c *nsFallbackStore) Abort(ctx context.Context, ref string) error { | ||
| return c.main.Abort(ctx, ref) | ||
| } | ||
|
|
||
| func (c *nsFallbackStore) ReaderAt(ctx context.Context, desc ocispecs.Descriptor) (content.ReaderAt, error) { | ||
| ra, err := c.main.ReaderAt(ctx, desc) | ||
| if err != nil { | ||
| if cerrdefs.IsNotFound(err) { | ||
| return c.fb.ReaderAt(ctx, desc) | ||
| } | ||
| } | ||
| return ra, err | ||
| } | ||
|
|
||
| func (c *nsFallbackStore) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) { | ||
| return c.main.Writer(ctx, opts...) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.