From f118749cdc5f08a0fcae3ee3aca3ad4e5485598b Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 16 Jan 2025 20:44:30 -0800 Subject: [PATCH 1/4] 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 From f7594d484b27a403c3781df78e6ce516ade0841e Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 16 Jan 2025 21:10:00 -0800 Subject: [PATCH 2/4] history: fix printing desktop URL Signed-off-by: Tonis Tiigi --- commands/history/inspect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/history/inspect.go b/commands/history/inspect.go index b1a97d8c..350ee8ad 100644 --- a/commands/history/inspect.go +++ b/commands/history/inspect.go @@ -360,7 +360,7 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) 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 } From 8c27b5c5458e053c2cc6d3ef01c10df62c91e86f Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 16 Jan 2025 21:07:56 -0800 Subject: [PATCH 3/4] history: make sure started time is shown in current timezone Signed-off-by: Tonis Tiigi --- commands/history/inspect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/history/inspect.go b/commands/history/inspect.go index 350ee8ad..bdae6123 100644 --- a/commands/history/inspect.go +++ b/commands/history/inspect.go @@ -193,7 +193,7 @@ 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 statusStr string if rec.CompletedAt != nil { From 1335264c9d9aa986fb9307c0b36145c960e261f4 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Fri, 17 Jan 2025 08:54:38 -0800 Subject: [PATCH 4/4] history: update formatting of error logs Signed-off-by: Tonis Tiigi --- commands/history/inspect.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/commands/history/inspect.go b/commands/history/inspect.go index bdae6123..b79eb401 100644 --- a/commands/history/inspect.go +++ b/commands/history/inspect.go @@ -331,6 +331,7 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) for _, s := range errdefs.Sources(retErr) { s.Print(dockerCli.Out()) } + fmt.Fprintln(dockerCli.Out()) var ve *errdefs.VertexError if errors.As(retErr, &ve) { @@ -343,9 +344,10 @@ 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.Fprintf(dockerCli.Out(), "\n => %s:\n", name) + 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(), "> "+l) } fmt.Fprintln(dockerCli.Out()) }