From f118749cdc5f08a0fcae3ee3aca3ad4e5485598b Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 16 Jan 2025 20:44:30 -0800 Subject: [PATCH] history: add error details to history inspect command For failed builds, show the source with error location and last logs for vertex that caused the error. When debug mode is on, stacktrace is printed. Signed-off-by: Tonis Tiigi --- commands/history/inspect.go | 122 +++++++++++++++++++++++++++++++++++- go.mod | 2 +- 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/commands/history/inspect.go b/commands/history/inspect.go index 8463754f..b1a97d8c 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 { @@ -186,14 +195,14 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) fmt.Fprintf(tw, "Started:\t%s\n", rec.CreatedAt.AsTime().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,6 +318,46 @@ 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()) + } + + 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.Fprintf(dockerCli.Out(), "\n => %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)) @@ -342,6 +391,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