diff --git a/commands/history/export.go b/commands/history/export.go index 5ccf25f32..af8149a7f 100644 --- a/commands/history/export.go +++ b/commands/history/export.go @@ -20,10 +20,11 @@ import ( ) type exportOptions struct { - builder string - refs []string - output string - all bool + builder string + refs []string + output string + all bool + finalize bool } func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) error { @@ -62,6 +63,26 @@ func runExport(ctx context.Context, dockerCli command.Cli, opts exportOptions) e return errors.Errorf("no record found for ref %q", ref) } + if opts.finalize { + var finalized bool + for _, rec := range recs { + if rec.Trace == nil { + finalized = true + if err := finalizeRecord(ctx, rec.Ref, nodes); err != nil { + return err + } + } + } + if finalized { + recs, err = queryRecords(ctx, ref, nodes, &queryOptions{ + CompletedOnly: true, + }) + if err != nil { + return err + } + } + } + if ref == "" { slices.SortFunc(recs, func(a, b historyRecord) int { return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime()) @@ -154,7 +175,8 @@ func exportCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command { flags := cmd.Flags() flags.StringVarP(&options.output, "output", "o", "", "Output file path") - flags.BoolVar(&options.all, "all", false, "Export all records for the builder") + flags.BoolVar(&options.all, "all", false, "Export all build records for the builder") + flags.BoolVar(&options.finalize, "finalize", false, "Ensure build records are finalized before exporting") return cmd } diff --git a/commands/history/trace.go b/commands/history/trace.go index 1e2b0f1ba..b841ce357 100644 --- a/commands/history/trace.go +++ b/commands/history/trace.go @@ -17,7 +17,6 @@ import ( "github.com/docker/buildx/util/otelutil" "github.com/docker/buildx/util/otelutil/jaeger" "github.com/docker/cli/cli/command" - controlapi "github.com/moby/buildkit/api/services/control" "github.com/opencontainers/go-digest" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/browser" @@ -57,14 +56,7 @@ func loadTrace(ctx context.Context, ref string, nodes []builder.Node) (string, [ // build is complete but no trace yet. try to finalize the trace time.Sleep(1 * time.Second) // give some extra time for last parts of trace to be written - c, err := rec.node.Driver.Client(ctx) - if err != nil { - return "", nil, err - } - _, err = c.ControlClient().UpdateBuildHistory(ctx, &controlapi.UpdateBuildHistoryRequest{ - Ref: rec.Ref, - Finalize: true, - }) + err := finalizeRecord(ctx, rec.Ref, []builder.Node{*rec.node}) if err != nil { return "", nil, err } diff --git a/commands/history/utils.go b/commands/history/utils.go index 95c8702e1..fa070eaab 100644 --- a/commands/history/utils.go +++ b/commands/history/utils.go @@ -248,6 +248,28 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q return out, nil } +func finalizeRecord(ctx context.Context, ref string, nodes []builder.Node) error { + eg, ctx := errgroup.WithContext(ctx) + for _, node := range nodes { + node := node + eg.Go(func() error { + if node.Driver == nil { + return nil + } + c, err := node.Driver.Client(ctx) + if err != nil { + return err + } + _, err = c.ControlClient().UpdateBuildHistory(ctx, &controlapi.UpdateBuildHistoryRequest{ + Ref: ref, + Finalize: true, + }) + return err + }) + } + return eg.Wait() +} + func formatDuration(d time.Duration) string { if d < time.Minute { return fmt.Sprintf("%.1fs", d.Seconds()) diff --git a/docs/reference/buildx_history_export.md b/docs/reference/buildx_history_export.md index ae3940777..edb99ee8f 100644 --- a/docs/reference/buildx_history_export.md +++ b/docs/reference/buildx_history_export.md @@ -5,12 +5,13 @@ Export build records into Docker Desktop bundle ### Options -| Name | Type | Default | Description | -|:---------------------------------------|:---------|:--------|:-----------------------------------------| -| [`--all`](#all) | `bool` | | Export all records for the builder | -| [`--builder`](#builder) | `string` | | Override the configured builder instance | -| [`-D`](#debug), [`--debug`](#debug) | `bool` | | Enable debug logging | -| [`-o`](#output), [`--output`](#output) | `string` | | Output file path | +| Name | Type | Default | Description | +|:---------------------------------------|:---------|:--------|:----------------------------------------------------| +| [`--all`](#all) | `bool` | | Export all build records for the builder | +| [`--builder`](#builder) | `string` | | Override the configured builder instance | +| [`-D`](#debug), [`--debug`](#debug) | `bool` | | Enable debug logging | +| [`--finalize`](#finalize) | `bool` | | Ensure build records are finalized before exporting | +| [`-o`](#output), [`--output`](#output) | `string` | | Output file path | @@ -49,6 +50,16 @@ docker buildx history export --builder builder0 ^1 -o builder0-build.dockerbuild docker buildx history export --debug qu2gsuo8ejqrwdfii23xkkckt -o debug-build.dockerbuild ``` +### Ensure build records are finalized before exporting (--finalize) + +Clients can report their own traces concurrently, and not all traces may be +saved yet by the time of the export. Use the `--finalize` flag to ensure all +traces are finalized before exporting. + +```console +docker buildx history export --finalize qu2gsuo8ejqrwdfii23xkkckt -o finalized-build.dockerbuild +``` + ### Export a single build to a custom file (--output) ```console