diff --git a/commands/history/inspect.go b/commands/history/inspect.go index abbc08e4..3e7ac710 100644 --- a/commands/history/inspect.go +++ b/commands/history/inspect.go @@ -1,10 +1,11 @@ package history import ( + "cmp" "context" + "encoding/json" "fmt" "io" - "log" "os" "path/filepath" "slices" @@ -13,12 +14,22 @@ import ( "text/tabwriter" "time" + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/content/proxy" + "github.com/containerd/containerd/images" "github.com/containerd/platforms" "github.com/docker/buildx/builder" "github.com/docker/buildx/localstate" "github.com/docker/buildx/util/cobrautil/completion" "github.com/docker/buildx/util/confutil" + "github.com/docker/buildx/util/desktop" "github.com/docker/cli/cli/command" + 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" + provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types" + "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" @@ -72,9 +83,6 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) } st, _ := ls.ReadRef(rec.node.Builder, rec.node.Name, rec.Ref) - log.Printf("rec %+v", rec) - log.Printf("st %+v", st) - tw := tabwriter.NewWriter(dockerCli.Out(), 1, 8, 1, '\t', 0) attrs := rec.FrontendAttrs @@ -101,12 +109,16 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) if dockerfile != "" && dockerfile != "-" { if rel, err := filepath.Rel(context, dockerfile); err == nil { - dockerfile = rel + if !strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + dockerfile = rel + } } } if context != "" { if rel, err := filepath.Rel(wd, context); err == nil { - context = rel + if !strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + context = rel + } } } } @@ -226,7 +238,7 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) var unusedAttrs []string for k := range attrs { - if strings.HasPrefix(k, "vcs:") || strings.HasPrefix(k, "build-arg:") || strings.HasPrefix(k, "label:") || strings.HasPrefix(k, "context:") { + if strings.HasPrefix(k, "vcs:") || strings.HasPrefix(k, "build-arg:") || strings.HasPrefix(k, "label:") || strings.HasPrefix(k, "context:") || strings.HasPrefix(k, "attest:") { continue } unusedAttrs = append(unusedAttrs, k) @@ -244,10 +256,62 @@ func runInspect(ctx context.Context, dockerCli command.Cli, opts inspectOptions) printTable(dockerCli.Out(), attrs, "build-arg:", "Build Arg") printTable(dockerCli.Out(), attrs, "label:", "Label") - // exporters (image) - // commands - // error - // materials + c, err := rec.node.Driver.Client(ctx) + if err != nil { + return err + } + + store := proxy.NewContentStore(c.ContentClient()) + + attachments, err := allAttachments(ctx, store, *rec) + if err != nil { + return err + } + + provIndex := slices.IndexFunc(attachments, func(a attachment) bool { + return descrType(a.descr) == slsa02.PredicateSLSAProvenance + }) + if provIndex != -1 { + prov := attachments[provIndex] + + dt, err := content.ReadBlob(ctx, store, prov.descr) + if err != nil { + return errors.Errorf("failed to read provenance %s: %v", prov.descr.Digest, err) + } + + var pred provenancetypes.ProvenancePredicate + if err := json.Unmarshal(dt, &pred); err != nil { + 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.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()) + } + + if len(attachments) > 0 { + fmt.Fprintf(tw, "Attachments:\n") + tw = tabwriter.NewWriter(dockerCli.Out(), 1, 8, 1, '\t', 0) + fmt.Fprintf(tw, "DIGEST\tPLATFORM\tTYPE\n") + for _, a := range attachments { + p := "" + if a.platform != nil { + p = platforms.FormatAll(*a.platform) + } + fmt.Fprintf(tw, "%s\t%s\t%s\n", a.descr.Digest, p, descrType(a.descr)) + } + tw.Flush() + fmt.Fprintln(dockerCli.Out()) + } + + 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)) return nil } @@ -274,6 +338,111 @@ func inspectCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command { return cmd } +type attachment struct { + platform *ocispecs.Platform + descr ocispecs.Descriptor +} + +func allAttachments(ctx context.Context, store content.Store, rec historyRecord) ([]attachment, error) { + var attachments []attachment + + if rec.Result != nil { + for _, a := range rec.Result.Attestations { + attachments = append(attachments, attachment{ + descr: ociDesc(a), + }) + } + for _, r := range rec.Result.Results { + attachments = append(attachments, walkAttachments(ctx, store, ociDesc(r), nil)...) + } + } + + for key, ri := range rec.Results { + p, err := platforms.Parse(key) + if err != nil { + return nil, err + } + for _, a := range ri.Attestations { + attachments = append(attachments, attachment{ + platform: &p, + descr: ociDesc(a), + }) + } + for _, r := range ri.Results { + attachments = append(attachments, walkAttachments(ctx, store, ociDesc(r), &p)...) + } + } + + slices.SortFunc(attachments, func(a, b attachment) int { + pCmp := 0 + if a.platform == nil && b.platform != nil { + return -1 + } else if a.platform != nil && b.platform == nil { + return 1 + } else if a.platform != nil && b.platform != nil { + pCmp = cmp.Compare(platforms.FormatAll(*a.platform), platforms.FormatAll(*b.platform)) + } + return cmp.Or( + pCmp, + cmp.Compare(descrType(a.descr), descrType(b.descr)), + ) + }) + + return attachments, nil +} + +func walkAttachments(ctx context.Context, store content.Store, desc ocispecs.Descriptor, platform *ocispecs.Platform) []attachment { + _, err := store.Info(ctx, desc.Digest) + if err != nil { + return nil + } + + var out []attachment + + if desc.Annotations["vnd.docker.reference.type"] != "attestation-manifest" { + out = append(out, attachment{platform: platform, descr: desc}) + } + + if desc.MediaType != ocispecs.MediaTypeImageIndex && desc.MediaType != images.MediaTypeDockerSchema2ManifestList { + return out + } + + dt, err := content.ReadBlob(ctx, store, desc) + if err != nil { + return out + } + + var idx ocispecs.Index + if err := json.Unmarshal(dt, &idx); err != nil { + return out + } + + for _, d := range idx.Manifests { + p := platform + if d.Platform != nil { + p = d.Platform + } + out = append(out, walkAttachments(ctx, store, d, p)...) + } + + return out +} + +func ociDesc(in *controlapi.Descriptor) ocispecs.Descriptor { + return ocispecs.Descriptor{ + MediaType: in.MediaType, + Digest: digest.Digest(in.Digest), + Size: in.Size, + Annotations: in.Annotations, + } +} +func descrType(desc ocispecs.Descriptor) string { + if typ, ok := desc.Annotations["in-toto.io/predicate-type"]; ok { + return typ + } + return desc.MediaType +} + func tryParseValue(s string, f func(string) (string, error)) string { v, err := f(s) if err != nil { @@ -303,3 +472,11 @@ func printTable(w io.Writer, attrs map[string]string, prefix, title string) { tw.Flush() fmt.Fprintln(w) } + +func digestSetToDigests(ds slsa.DigestSet) []string { + var out []string + for k, v := range ds { + out = append(out, fmt.Sprintf("%s:%s", k, v)) + } + return out +}