diff --git a/commands/history/inspect.go b/commands/history/inspect.go index 8463754f..b79eb401 100644 --- a/commands/history/inspect.go +++ b/commands/history/inspect.go @@ -1,6 +1,7 @@ package history import ( + "bytes" "cmp" "context" "encoding/json" @@ -24,16 +25,24 @@ import ( "github.com/docker/buildx/util/confutil" "github.com/docker/buildx/util/desktop" "github.com/docker/cli/cli/command" + "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" controlapi "github.com/moby/buildkit/api/services/control" + "github.com/moby/buildkit/client" + "github.com/moby/buildkit/solver/errdefs" provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types" + "github.com/moby/buildkit/util/grpcerrors" + "github.com/moby/buildkit/util/stack" "github.com/opencontainers/go-digest" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/tonistiigi/go-csvvalue" + spb "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + proto "google.golang.org/protobuf/proto" ) type inspectOptions struct { @@ -184,16 +193,16 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) tw = tabwriter.NewWriter(dockerCli.Out(), 1, 8, 1, '\t', 0) - fmt.Fprintf(tw, "Started:\t%s\n", rec.CreatedAt.AsTime().Format("2006-01-02 15:04:05")) + fmt.Fprintf(tw, "Started:\t%s\n", rec.CreatedAt.AsTime().Local().Format("2006-01-02 15:04:05")) var duration time.Duration - var status string + var statusStr string if rec.CompletedAt != nil { duration = rec.CompletedAt.AsTime().Sub(rec.CreatedAt.AsTime()) } else { duration = rec.currentTimestamp.Sub(rec.CreatedAt.AsTime()) - status = " (running)" + statusStr = " (running)" } - fmt.Fprintf(tw, "Duration:\t%s%s\n", formatDuration(duration), status) + fmt.Fprintf(tw, "Duration:\t%s%s\n", formatDuration(duration), statusStr) if rec.Error != nil { if codes.Code(rec.Error.Code) == codes.Canceled { fmt.Fprintf(tw, "Status:\tCanceled\n") @@ -309,9 +318,51 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) fmt.Fprintln(dockerCli.Out()) } + 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) + } + retErr := grpcerrors.FromGRPC(status.ErrorProto(&st)) + for _, s := range errdefs.Sources(retErr) { + s.Print(dockerCli.Out()) + } + fmt.Fprintln(dockerCli.Out()) + + 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, 16) + if err != nil { + 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) + for _, l := range logs { + fmt.Fprintln(dockerCli.Out(), "> "+l) + } + fmt.Fprintln(dockerCli.Out()) + } + } + + if debug.IsEnabled() { + fmt.Fprintf(dockerCli.Out(), "\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(dockerCli.Out(), "Print build logs: docker buildx history logs %s\n", rec.Ref) - fmt.Fprintf(dockerCli.Out(), "View build in Docker Desktop: %s\n", desktop.BuildURL(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))) return nil } @@ -342,6 +393,73 @@ func inspectCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command { return cmd } +func loadVertexLogs(ctx context.Context, c *client.Client, ref string, dgst digest.Digest, limit int) (string, []string, error) { + st, err := c.ControlClient().Status(ctx, &controlapi.StatusRequest{ + Ref: ref, + }) + if err != nil { + return "", nil, err + } + + var name string + var logs []string + lastState := map[int]int{} + +loop0: + for { + select { + case <-ctx.Done(): + st.CloseSend() + return "", nil, context.Cause(ctx) + default: + ev, err := st.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + break loop0 + } + return "", nil, err + } + ss := client.NewSolveStatus(ev) + for _, v := range ss.Vertexes { + if v.Digest == dgst { + name = v.Name + break + } + } + for _, l := range ss.Logs { + if l.Vertex == dgst { + parts := bytes.Split(l.Data, []byte("\n")) + for i, p := range parts { + var wrote bool + if i == 0 { + idx, ok := lastState[l.Stream] + if ok && idx != -1 { + logs[idx] = logs[idx] + string(p) + wrote = true + } + } + if !wrote { + if len(p) > 0 { + logs = append(logs, string(p)) + } + lastState[l.Stream] = len(logs) - 1 + } + if i == len(parts)-1 && len(p) == 0 { + lastState[l.Stream] = -1 + } + } + } + } + } + } + + if limit > 0 && len(logs) > limit { + logs = logs[len(logs)-limit:] + } + + return name, logs, nil +} + type attachment struct { platform *ocispecs.Platform descr ocispecs.Descriptor diff --git a/go.mod b/go.mod index 50f6db21..4fa5e98a 100644 --- a/go.mod +++ b/go.mod @@ -57,6 +57,7 @@ require ( golang.org/x/sys v0.28.0 golang.org/x/term v0.27.0 golang.org/x/text v0.21.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 google.golang.org/grpc v1.68.1 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 google.golang.org/protobuf v1.35.2 @@ -173,7 +174,6 @@ require ( golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.25.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect