diff --git a/commands/history/inspect.go b/commands/history/inspect.go index b79eb401a96f..beed753bf11c 100644 --- a/commands/history/inspect.go +++ b/commands/history/inspect.go @@ -25,6 +25,7 @@ import ( "github.com/docker/buildx/util/confutil" "github.com/docker/buildx/util/desktop" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/debug" slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2" @@ -48,6 +49,7 @@ import ( type inspectOptions struct { builder string ref string + format string } func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) error { @@ -92,7 +94,135 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) } st, _ := ls.ReadRef(rec.node.Builder, rec.node.Name, rec.Ref) - tw := tabwriter.NewWriter(dockerCli.Out(), 1, 8, 1, '\t', 0) + var sg *localstate.StateGroup + if st != nil && st.GroupRef != "" { + sg, _ = ls.ReadGroup(st.GroupRef) + } + + switch opts.format { + case formatter.JSONFormatKey: + return inspectPrintJSON(ctx, rec, st, sg, dockerCli.Out()) + case formatter.RawFormatKey: + return inspectPrintRaw(ctx, rec, st, dockerCli.Out()) + default: + return errors.Errorf("unsupported format %q", opts.format) + } +} + +func inspectPrintJSON(ctx context.Context, rec *historyRecord, ls *localstate.State, lsg *localstate.StateGroup, w io.Writer) error { + type buildError struct { + Name string `json:"name,omitempty"` + Sources string `json:"sources,omitempty"` + Logs string `json:"logs,omitempty"` + } + + out := struct { + Record *controlapi.BuildHistoryRecord `json:"record"` + LocalState *localstate.State `json:"localState,omitempty"` + LocalStateGroup *localstate.StateGroup `json:"localStateGroup,omitempty"` + Name string `json:"name,omitempty"` + DefaultPlatform string `json:"defaultPlatform,omitempty"` + Status string `json:"status,omitempty"` + Duration time.Duration `json:"duration,omitempty"` + Error *buildError `json:"error,omitempty"` + }{ + Record: rec.BuildHistoryRecord, + Name: buildName(rec.FrontendAttrs, ls), + LocalState: ls, + LocalStateGroup: lsg, + Status: "completed", + } + + if rec.CompletedAt != nil { + out.Duration = rec.CompletedAt.AsTime().Sub(rec.CreatedAt.AsTime()) + } else { + out.Duration = rec.currentTimestamp.Sub(rec.CreatedAt.AsTime()) + out.Status = "running" + } + + c, err := rec.node.Driver.Client(ctx) + if err != nil { + return err + } + + workers, err := c.ListWorkers(ctx) + if err != nil { + return errors.Wrap(err, "failed to list workers") + } +workers0: + for _, w := range workers { + for _, p := range w.Platforms { + out.DefaultPlatform = platforms.FormatAll(platforms.Normalize(p)) + break workers0 + } + } + + store := proxy.NewContentStore(c.ContentClient()) + + if rec.Error != nil || rec.ExternalError != nil { + out.Error = new(buildError) + if rec.Error != nil { + if rec.Error.Code == int32(codes.Canceled) { + out.Status = "canceled" + } else { + out.Status = "error" + out.Error.Logs = rec.Error.Message + } + } + if rec.ExternalError != nil { + dt, err := content.ReadBlob(ctx, store, ociDesc(rec.ExternalError)) + if err != nil { + return errors.Wrapf(err, "failed to read external error %s", rec.ExternalError.Digest) + } + var st spb.Status + if err := proto.Unmarshal(dt, &st); err != nil { + return errors.Wrapf(err, "failed to unmarshal external error %s", rec.ExternalError.Digest) + } + + if st.Code == int32(codes.Canceled) { + out.Status = "canceled" + } else { + out.Status = "error" + } + + retErr := grpcerrors.FromGRPC(status.ErrorProto(&st)) + + var bsources bytes.Buffer + for _, s := range errdefs.Sources(retErr) { + s.Print(&bsources) + bsources.WriteString("\n") + } + out.Error.Sources = bsources.String() + + var ve *errdefs.VertexError + if errors.As(retErr, &ve) { + dgst, err := digest.Parse(ve.Vertex.Digest) + if err != nil { + return errors.Wrapf(err, "failed to parse vertex digest %s", ve.Vertex.Digest) + } + name, logs, err := loadVertexLogs(ctx, c, rec.Ref, dgst, -1) + if err != nil { + return errors.Wrapf(err, "failed to load vertex logs %s", dgst) + } + out.Error.Name = name + if len(logs) > 0 { + var blogs bytes.Buffer + for _, l := range logs { + fmt.Fprintln(&blogs, l) + } + out.Error.Logs = blogs.String() + } + } + } + } + + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + return enc.Encode(out) +} + +func inspectPrintRaw(ctx context.Context, rec *historyRecord, ls *localstate.State, w io.Writer) error { + tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0) attrs := rec.FrontendAttrs delete(attrs, "frontend.caps") @@ -111,9 +241,9 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) var context string var dockerfile string - if st != nil { - context = st.LocalPath - dockerfile = st.DockerfilePath + if ls != nil { + context = ls.LocalPath + dockerfile = ls.DockerfilePath wd, _ := os.Getwd() if dockerfile != "" && dockerfile != "-" { @@ -187,11 +317,11 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) tw.Flush() - fmt.Fprintln(dockerCli.Out()) + fmt.Fprintln(w) - printTable(dockerCli.Out(), attrs, "context:", "Named Context") + printTable(w, attrs, "context:", "Named Context") - tw = tabwriter.NewWriter(dockerCli.Out(), 1, 8, 1, '\t', 0) + tw = tabwriter.NewWriter(w, 1, 8, 1, '\t', 0) fmt.Fprintf(tw, "Started:\t%s\n", rec.CreatedAt.AsTime().Local().Format("2006-01-02 15:04:05")) var duration time.Duration @@ -213,9 +343,9 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) fmt.Fprintf(tw, "Build Steps:\t%d/%d (%.0f%% cached)\n", rec.NumCompletedSteps, rec.NumTotalSteps, float64(rec.NumCachedSteps)/float64(rec.NumTotalSteps)*100) tw.Flush() - fmt.Fprintln(dockerCli.Out()) + fmt.Fprintln(w) - tw = tabwriter.NewWriter(dockerCli.Out(), 1, 8, 1, '\t', 0) + tw = tabwriter.NewWriter(w, 1, 8, 1, '\t', 0) writeAttr("force-network-mode", "Network", nil) writeAttr("hostname", "Hostname", nil) @@ -260,10 +390,10 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) tw.Flush() - fmt.Fprintln(dockerCli.Out()) + fmt.Fprintln(w) - printTable(dockerCli.Out(), attrs, "build-arg:", "Build Arg") - printTable(dockerCli.Out(), attrs, "label:", "Label") + printTable(w, attrs, "build-arg:", "Build Arg") + printTable(w, attrs, "label:", "Label") c, err := rec.node.Driver.Client(ctx) if err != nil { @@ -293,19 +423,19 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) return errors.Errorf("failed to unmarshal provenance %s: %v", prov.descr.Digest, err) } - fmt.Fprintln(dockerCli.Out(), "Materials:") - tw = tabwriter.NewWriter(dockerCli.Out(), 1, 8, 1, '\t', 0) + fmt.Fprintln(w, "Materials:") + tw = tabwriter.NewWriter(w, 1, 8, 1, '\t', 0) fmt.Fprintf(tw, "URI\tDIGEST\n") for _, m := range pred.Materials { fmt.Fprintf(tw, "%s\t%s\n", m.URI, strings.Join(digestSetToDigests(m.Digest), ", ")) } tw.Flush() - fmt.Fprintln(dockerCli.Out()) + fmt.Fprintln(w) } if len(attachments) > 0 { fmt.Fprintf(tw, "Attachments:\n") - tw = tabwriter.NewWriter(dockerCli.Out(), 1, 8, 1, '\t', 0) + tw = tabwriter.NewWriter(w, 1, 8, 1, '\t', 0) fmt.Fprintf(tw, "DIGEST\tPLATFORM\tTYPE\n") for _, a := range attachments { p := "" @@ -315,7 +445,7 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) fmt.Fprintf(tw, "%s\t%s\t%s\n", a.descr.Digest, p, descrType(a.descr)) } tw.Flush() - fmt.Fprintln(dockerCli.Out()) + fmt.Fprintln(w) } if rec.ExternalError != nil { @@ -329,9 +459,9 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) } retErr := grpcerrors.FromGRPC(status.ErrorProto(&st)) for _, s := range errdefs.Sources(retErr) { - s.Print(dockerCli.Out()) + s.Print(w) } - fmt.Fprintln(dockerCli.Out()) + fmt.Fprintln(w) var ve *errdefs.VertexError if errors.As(retErr, &ve) { @@ -344,25 +474,25 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) return errors.Wrapf(err, "failed to load vertex logs %s", dgst) } if len(logs) > 0 { - fmt.Fprintln(dockerCli.Out(), "Logs:") - fmt.Fprintf(dockerCli.Out(), "> => %s:\n", name) + fmt.Fprintln(w, "Logs:") + fmt.Fprintf(w, "> => %s:\n", name) for _, l := range logs { - fmt.Fprintln(dockerCli.Out(), "> "+l) + fmt.Fprintln(w, "> "+l) } - fmt.Fprintln(dockerCli.Out()) + fmt.Fprintln(w) } } if debug.IsEnabled() { - fmt.Fprintf(dockerCli.Out(), "\n%+v\n", stack.Formatter(retErr)) + fmt.Fprintf(w, "\n%+v\n", stack.Formatter(retErr)) } else if len(stack.Traces(retErr)) > 0 { - fmt.Fprintf(dockerCli.Out(), "Enable --debug to see stack traces for error\n") + fmt.Fprintf(w, "Enable --debug to see stack traces for error\n") } } - fmt.Fprintf(dockerCli.Out(), "Print build logs: docker buildx history logs %s\n", rec.Ref) + fmt.Fprintf(w, "Print build logs: docker buildx history logs %s\n", rec.Ref) - fmt.Fprintf(dockerCli.Out(), "View build in Docker Desktop: %s\n", desktop.BuildURL(fmt.Sprintf("%s/%s/%s", rec.node.Builder, rec.node.Name, rec.Ref))) + fmt.Fprintf(w, "View build in Docker Desktop: %s\n", desktop.BuildURL(fmt.Sprintf("%s/%s/%s", rec.node.Builder, rec.node.Name, rec.Ref))) return nil } @@ -388,7 +518,8 @@ func inspectCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command { attachmentCmd(dockerCli, rootOpts), ) - // flags := cmd.Flags() + flags := cmd.Flags() + flags.StringVar(&options.format, "format", formatter.RawFormatKey, "Format the output") return cmd } diff --git a/docs/reference/buildx_history_inspect.md b/docs/reference/buildx_history_inspect.md index d3d6637aed1c..e03effc4dad1 100644 --- a/docs/reference/buildx_history_inspect.md +++ b/docs/reference/buildx_history_inspect.md @@ -12,11 +12,128 @@ Inspect a build ### Options -| Name | Type | Default | Description | -|:----------------|:---------|:--------|:-----------------------------------------| -| `--builder` | `string` | | Override the configured builder instance | -| `-D`, `--debug` | `bool` | | Enable debug logging | +| Name | Type | Default | Description | +|:----------------------|:---------|:--------|:-----------------------------------------| +| `--builder` | `string` | | Override the configured builder instance | +| `-D`, `--debug` | `bool` | | Enable debug logging | +| [`--format`](#format) | `string` | `raw` | Format the output | +## Examples + +### Format the output (--format) + +Output format can be one of `raw`, `json`. + +```console +$ docker buildx history inspect --format raw +Context: . +Dockerfile: Dockerfile +VCS Repository: https://github.com/crazy-max/buildx.git +VCS Revision: b066ee111062d8079485ec225f6e677f48a9b3ad +Target: binaries +Platform: linux/amd64 +Keep Git Dir: true + +Started: 2025-02-03 14:21:33 +Duration: 1m 3s +Build Steps: 16/16 (25% cached) + + +Materials: +URI DIGEST +pkg:docker/docker/dockerfile@1 sha256:93bfd3b68c109427185cd78b4779fc82b484b0b7618e36d0f104d4d801e66d25 +pkg:docker/golang@1.23-alpine3.21?platform=linux%2Famd64 sha256:47d337594bd9e667d35514b241569f95fb6d95727c24b19468813d596d5ae596 +pkg:docker/tonistiigi/xx@1.6.1?platform=linux%2Famd64 sha256:923441d7c25f1e2eb5789f82d987693c47b8ed987c4ab3b075d6ed2b5d6779a3 + +Attachments: +DIGEST PLATFORM TYPE +sha256:7413a92fdb7cab09b0724626c9c15b0d762822d9d108927a04cf5b26d34d3092 https://slsa.dev/provenance/v0.2 + +Print build logs: docker buildx history logs pzsupdzicme4l4fpj6hqj2g5u +``` + +```console +$ docker buildx history inspect --format json +{ + "record": { + "Ref": "pzsupdzicme4l4fpj6hqj2g5u", + "FrontendAttrs": { + "attest:provenance": "mode=min,inline-only=true", + "build-arg:BUILDKIT_CONTEXT_KEEP_GIT_DIR": "1", + "filename": "Dockerfile", + "platform": "linux/amd64", + "target": "binaries", + "vcs:localdir:context": ".", + "vcs:localdir:dockerfile": ".", + "vcs:revision": "b066ee111062d8079485ec225f6e677f48a9b3ad", + "vcs:source": "https://github.com/crazy-max/buildx.git" + }, + "Exporters": [ + { + "Type": "local" + } + ], + "CreatedAt": { + "seconds": 1738588893, + "nanos": 241021211 + }, + "CompletedAt": { + "seconds": 1738588956, + "nanos": 385422791 + }, + "logs": { + "media_type": "application/vnd.buildkit.status.v0", + "digest": "sha256:23d3dae6be61eb4bb080dd64493337ea523328805f9cd7c51f1c92245d9d2bf9", + "size": 9512 + }, + "ExporterResponse": { + "containerimage.config": "{\"architecture\":\"amd64\",\"os\":\"linux\",\"rootfs\":{\"type\":\"layers\",\"diff_ids\":null},\"history\":[{\"created_by\":\"COPY /usr/bin/docker-buildx /buildx # buildkit\",\"comment\":\"buildkit.dockerfile.v0\"},{\"created_by\":\"ARG BUILDKIT_SBOM_SCAN_STAGE=true\",\"comment\":\"buildkit.dockerfile.v0\",\"empty_layer\":true}],\"config\":{\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"WorkingDir\":\"/\"}}", + "refs.platforms": "{\"Platforms\":[{\"ID\":\"linux/amd64\",\"Platform\":{\"architecture\":\"amd64\",\"os\":\"linux\"}}]}", + "verifier.requestopts": "{\"Platforms\":[\"linux/amd64\"],\"Labels\":{},\"Request\":\"\"}" + }, + "Result": { + "Attestations": [ + { + "media_type": "application/vnd.in-toto+json", + "digest": "sha256:7413a92fdb7cab09b0724626c9c15b0d762822d9d108927a04cf5b26d34d3092", + "size": 42469, + "annotations": { + "in-toto.io/predicate-type": "https://slsa.dev/provenance/v0.2" + } + } + ] + }, + "Generation": 1, + "trace": { + "media_type": "application/vnd.buildkit.otlp.json.v0", + "digest": "sha256:3379891c9526e5f563d405f54e3f764aa46c89a66bb625f85ecec705602edb8f", + "size": 217689 + }, + "numCachedSteps": 4, + "numTotalSteps": 16, + "numCompletedSteps": 16 + }, + "localState": { + "Target": "binaries", + "LocalPath": "/home/foo/github/docker/buildx", + "DockerfilePath": "/home/foo/github/docker/buildx/Dockerfile", + "GroupRef": "w6p3fj7agisc1i1e72ehikggo" + }, + "localStateGroup": { + "Definition": "ewogICJncm91cCI6IHsKICAgICJkZWZhdWx0IjogewogICAgICAidGFyZ2V0cyI6IFsKICAgICAgICAiYmluYXJpZXMiCiAgICAgIF0KICAgIH0KICB9LAogICJ0YXJnZXQiOiB7CiAgICAiYmluYXJpZXMiOiB7CiAgICAgICJjb250ZXh0IjogIi4iLAogICAgICAiZG9ja2VyZmlsZSI6ICJEb2NrZXJmaWxlIiwKICAgICAgImFyZ3MiOiB7CiAgICAgICAgIkJVSUxES0lUX0NPTlRFWFRfS0VFUF9HSVRfRElSIjogIjEiCiAgICAgIH0sCiAgICAgICJ0YXJnZXQiOiAiYmluYXJpZXMiLAogICAgICAicGxhdGZvcm1zIjogWwogICAgICAgICJsb2NhbCIKICAgICAgXSwKICAgICAgIm91dHB1dCI6IFsKICAgICAgICB7CiAgICAgICAgICAiZGVzdCI6ICIuL2Jpbi9idWlsZCIsCiAgICAgICAgICAidHlwZSI6ICJsb2NhbCIKICAgICAgICB9CiAgICAgIF0KICAgIH0KICB9Cn0=", + "Targets": [ + "binaries" + ], + "Refs": [ + "pzsupdzicme4l4fpj6hqj2g5u" + ] + }, + "name": "buildx (binaries)", + "defaultPlatform": "linux/amd64", + "status": "completed", + "duration": 63144401580 +} +```