Skip to content

Commit a18ff4d

Browse files
authored
Merge pull request #2891 from tonistiigi/history-command-initial
Add buildx history command
2 parents cde0e98 + b035a04 commit a18ff4d

31 files changed

+1808
-9
lines changed

commands/history/inspect.go

Lines changed: 486 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package history
2+
3+
import (
4+
"context"
5+
"io"
6+
"slices"
7+
8+
"github.com/containerd/containerd/v2/core/content/proxy"
9+
"github.com/containerd/platforms"
10+
"github.com/docker/buildx/builder"
11+
"github.com/docker/buildx/util/cobrautil/completion"
12+
"github.com/docker/cli/cli/command"
13+
intoto "github.com/in-toto/in-toto-golang/in_toto"
14+
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
15+
"github.com/opencontainers/go-digest"
16+
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
17+
"github.com/pkg/errors"
18+
"github.com/spf13/cobra"
19+
)
20+
21+
type attachmentOptions struct {
22+
builder string
23+
typ string
24+
platform string
25+
ref string
26+
digest digest.Digest
27+
}
28+
29+
func runAttachment(ctx context.Context, dockerCli command.Cli, opts attachmentOptions) error {
30+
b, err := builder.New(dockerCli, builder.WithName(opts.builder))
31+
if err != nil {
32+
return err
33+
}
34+
35+
nodes, err := b.LoadNodes(ctx)
36+
if err != nil {
37+
return err
38+
}
39+
for _, node := range nodes {
40+
if node.Err != nil {
41+
return node.Err
42+
}
43+
}
44+
45+
recs, err := queryRecords(ctx, opts.ref, nodes)
46+
if err != nil {
47+
return err
48+
}
49+
50+
if len(recs) == 0 {
51+
if opts.ref == "" {
52+
return errors.New("no records found")
53+
}
54+
return errors.Errorf("no record found for ref %q", opts.ref)
55+
}
56+
57+
if opts.ref == "" {
58+
slices.SortFunc(recs, func(a, b historyRecord) int {
59+
return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime())
60+
})
61+
}
62+
63+
rec := &recs[0]
64+
65+
c, err := rec.node.Driver.Client(ctx)
66+
if err != nil {
67+
return err
68+
}
69+
70+
store := proxy.NewContentStore(c.ContentClient())
71+
72+
if opts.digest != "" {
73+
ra, err := store.ReaderAt(ctx, ocispecs.Descriptor{Digest: opts.digest})
74+
if err != nil {
75+
return err
76+
}
77+
_, err = io.Copy(dockerCli.Out(), io.NewSectionReader(ra, 0, ra.Size()))
78+
return err
79+
}
80+
81+
attachments, err := allAttachments(ctx, store, *rec)
82+
if err != nil {
83+
return err
84+
}
85+
86+
typ := opts.typ
87+
switch typ {
88+
case "index":
89+
typ = ocispecs.MediaTypeImageIndex
90+
case "manifest":
91+
typ = ocispecs.MediaTypeImageManifest
92+
case "image":
93+
typ = ocispecs.MediaTypeImageConfig
94+
case "provenance":
95+
typ = slsa02.PredicateSLSAProvenance
96+
case "sbom":
97+
typ = intoto.PredicateSPDX
98+
}
99+
100+
for _, a := range attachments {
101+
if opts.platform != "" && (a.platform == nil || platforms.FormatAll(*a.platform) != opts.platform) {
102+
continue
103+
}
104+
if typ != "" && descrType(a.descr) != typ {
105+
continue
106+
}
107+
ra, err := store.ReaderAt(ctx, a.descr)
108+
if err != nil {
109+
return err
110+
}
111+
_, err = io.Copy(dockerCli.Out(), io.NewSectionReader(ra, 0, ra.Size()))
112+
return err
113+
}
114+
115+
return errors.Errorf("no matching attachment found for ref %q", opts.ref)
116+
}
117+
118+
func attachmentCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
119+
var options attachmentOptions
120+
121+
cmd := &cobra.Command{
122+
Use: "attachment [OPTIONS] REF [DIGEST]",
123+
Short: "Inspect a build attachment",
124+
Args: cobra.RangeArgs(1, 2),
125+
RunE: func(cmd *cobra.Command, args []string) error {
126+
if len(args) > 0 {
127+
options.ref = args[0]
128+
}
129+
if len(args) > 1 {
130+
dgst, err := digest.Parse(args[1])
131+
if err != nil {
132+
return errors.Wrapf(err, "invalid digest %q", args[1])
133+
}
134+
options.digest = dgst
135+
}
136+
137+
if options.digest == "" && options.platform == "" && options.typ == "" {
138+
return errors.New("at least one of --type, --platform or DIGEST must be specified")
139+
}
140+
141+
options.builder = *rootOpts.Builder
142+
return runAttachment(cmd.Context(), dockerCli, options)
143+
},
144+
ValidArgsFunction: completion.Disable,
145+
}
146+
147+
flags := cmd.Flags()
148+
flags.StringVar(&options.typ, "type", "", "Type of attachment")
149+
flags.StringVar(&options.platform, "platform", "", "Platform of attachment")
150+
151+
return cmd
152+
}

commands/history/logs.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package history
2+
3+
import (
4+
"context"
5+
"io"
6+
"os"
7+
"slices"
8+
9+
"github.com/docker/buildx/builder"
10+
"github.com/docker/buildx/util/cobrautil/completion"
11+
"github.com/docker/buildx/util/progress"
12+
"github.com/docker/cli/cli/command"
13+
controlapi "github.com/moby/buildkit/api/services/control"
14+
"github.com/moby/buildkit/client"
15+
"github.com/moby/buildkit/util/progress/progressui"
16+
"github.com/pkg/errors"
17+
"github.com/spf13/cobra"
18+
)
19+
20+
type logsOptions struct {
21+
builder string
22+
ref string
23+
progress string
24+
}
25+
26+
func runLogs(ctx context.Context, dockerCli command.Cli, opts logsOptions) error {
27+
b, err := builder.New(dockerCli, builder.WithName(opts.builder))
28+
if err != nil {
29+
return err
30+
}
31+
32+
nodes, err := b.LoadNodes(ctx)
33+
if err != nil {
34+
return err
35+
}
36+
for _, node := range nodes {
37+
if node.Err != nil {
38+
return node.Err
39+
}
40+
}
41+
42+
recs, err := queryRecords(ctx, opts.ref, nodes)
43+
if err != nil {
44+
return err
45+
}
46+
47+
if len(recs) == 0 {
48+
if opts.ref == "" {
49+
return errors.New("no records found")
50+
}
51+
return errors.Errorf("no record found for ref %q", opts.ref)
52+
}
53+
54+
if opts.ref == "" {
55+
slices.SortFunc(recs, func(a, b historyRecord) int {
56+
return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime())
57+
})
58+
}
59+
60+
rec := &recs[0]
61+
c, err := rec.node.Driver.Client(ctx)
62+
if err != nil {
63+
return err
64+
}
65+
66+
cl, err := c.ControlClient().Status(ctx, &controlapi.StatusRequest{
67+
Ref: rec.Ref,
68+
})
69+
if err != nil {
70+
return err
71+
}
72+
73+
var mode progressui.DisplayMode = progressui.DisplayMode(opts.progress)
74+
if mode == progressui.AutoMode {
75+
mode = progressui.PlainMode
76+
}
77+
printer, err := progress.NewPrinter(context.TODO(), os.Stderr, mode)
78+
if err != nil {
79+
return err
80+
}
81+
82+
loop0:
83+
for {
84+
select {
85+
case <-ctx.Done():
86+
cl.CloseSend()
87+
return context.Cause(ctx)
88+
default:
89+
ev, err := cl.Recv()
90+
if err != nil {
91+
if errors.Is(err, io.EOF) {
92+
break loop0
93+
}
94+
return err
95+
}
96+
printer.Write(client.NewSolveStatus(ev))
97+
}
98+
}
99+
100+
return printer.Wait()
101+
}
102+
103+
func logsCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
104+
var options logsOptions
105+
106+
cmd := &cobra.Command{
107+
Use: "logs [OPTIONS] [REF]",
108+
Short: "Print the logs of a build",
109+
Args: cobra.MaximumNArgs(1),
110+
RunE: func(cmd *cobra.Command, args []string) error {
111+
if len(args) > 0 {
112+
options.ref = args[0]
113+
}
114+
options.builder = *rootOpts.Builder
115+
return runLogs(cmd.Context(), dockerCli, options)
116+
},
117+
ValidArgsFunction: completion.Disable,
118+
}
119+
120+
flags := cmd.Flags()
121+
flags.StringVar(&options.progress, "progress", "plain", "Set type of progress output (plain, rawjson, tty)")
122+
123+
return cmd
124+
}

0 commit comments

Comments
 (0)