mirror of
				https://gitea.com/Lydanne/buildx.git
				synced 2025-11-04 18:13:42 +08:00 
			
		
		
		
	Merge pull request #3091 from tonistiigi/history-filters
history: add filters to ls
This commit is contained in:
		@@ -5,6 +5,7 @@ import (
 | 
				
			|||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"path"
 | 
				
			||||||
	"slices"
 | 
						"slices"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,10 +15,12 @@ import (
 | 
				
			|||||||
	"github.com/docker/buildx/util/cobrautil/completion"
 | 
						"github.com/docker/buildx/util/cobrautil/completion"
 | 
				
			||||||
	"github.com/docker/buildx/util/confutil"
 | 
						"github.com/docker/buildx/util/confutil"
 | 
				
			||||||
	"github.com/docker/buildx/util/desktop"
 | 
						"github.com/docker/buildx/util/desktop"
 | 
				
			||||||
 | 
						"github.com/docker/buildx/util/gitutil"
 | 
				
			||||||
	"github.com/docker/cli/cli"
 | 
						"github.com/docker/cli/cli"
 | 
				
			||||||
	"github.com/docker/cli/cli/command"
 | 
						"github.com/docker/cli/cli/command"
 | 
				
			||||||
	"github.com/docker/cli/cli/command/formatter"
 | 
						"github.com/docker/cli/cli/command/formatter"
 | 
				
			||||||
	"github.com/docker/go-units"
 | 
						"github.com/docker/go-units"
 | 
				
			||||||
 | 
						"github.com/pkg/errors"
 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -38,6 +41,9 @@ type lsOptions struct {
 | 
				
			|||||||
	builder string
 | 
						builder string
 | 
				
			||||||
	format  string
 | 
						format  string
 | 
				
			||||||
	noTrunc bool
 | 
						noTrunc bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filters []string
 | 
				
			||||||
 | 
						local   bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func runLs(ctx context.Context, dockerCli command.Cli, opts lsOptions) error {
 | 
					func runLs(ctx context.Context, dockerCli command.Cli, opts lsOptions) error {
 | 
				
			||||||
@@ -56,7 +62,29 @@ func runLs(ctx context.Context, dockerCli command.Cli, opts lsOptions) error {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	out, err := queryRecords(ctx, "", nodes, nil)
 | 
						queryOptions := &queryOptions{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if opts.local {
 | 
				
			||||||
 | 
							wd, err := os.Getwd()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							gitc, err := gitutil.New(gitutil.WithContext(ctx), gitutil.WithWorkingDir(wd))
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if st, err1 := os.Stat(path.Join(wd, ".git")); err1 == nil && st.IsDir() {
 | 
				
			||||||
 | 
									return errors.Wrap(err, "git was not found in the system")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return errors.Wrapf(err, "could not find git repository for local filter")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							remote, err := gitc.RemoteURL()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return errors.Wrapf(err, "could not get remote URL for local filter")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							queryOptions.Filters = append(queryOptions.Filters, fmt.Sprintf("repository=%s", remote))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						queryOptions.Filters = append(queryOptions.Filters, opts.filters...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						out, err := queryRecords(ctx, "", nodes, queryOptions)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -92,6 +120,8 @@ func lsCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
 | 
				
			|||||||
	flags := cmd.Flags()
 | 
						flags := cmd.Flags()
 | 
				
			||||||
	flags.StringVar(&options.format, "format", formatter.TableFormatKey, "Format the output")
 | 
						flags.StringVar(&options.format, "format", formatter.TableFormatKey, "Format the output")
 | 
				
			||||||
	flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output")
 | 
						flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output")
 | 
				
			||||||
 | 
						flags.StringArrayVar(&options.filters, "filter", nil, `Provide filter values (e.g., "status=error")`)
 | 
				
			||||||
 | 
						flags.BoolVar(&options.local, "local", false, "List records for current repository only")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return cmd
 | 
						return cmd
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,9 @@
 | 
				
			|||||||
package history
 | 
					package history
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
	"context"
 | 
						"context"
 | 
				
			||||||
 | 
						"encoding/csv"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"path/filepath"
 | 
						"path/filepath"
 | 
				
			||||||
@@ -15,10 +17,13 @@ import (
 | 
				
			|||||||
	"github.com/docker/buildx/builder"
 | 
						"github.com/docker/buildx/builder"
 | 
				
			||||||
	"github.com/docker/buildx/localstate"
 | 
						"github.com/docker/buildx/localstate"
 | 
				
			||||||
	controlapi "github.com/moby/buildkit/api/services/control"
 | 
						controlapi "github.com/moby/buildkit/api/services/control"
 | 
				
			||||||
 | 
						"github.com/moby/buildkit/util/gitutil"
 | 
				
			||||||
	"github.com/pkg/errors"
 | 
						"github.com/pkg/errors"
 | 
				
			||||||
	"golang.org/x/sync/errgroup"
 | 
						"golang.org/x/sync/errgroup"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const recordsLimit = 50
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func buildName(fattrs map[string]string, ls *localstate.State) string {
 | 
					func buildName(fattrs map[string]string, ls *localstate.State) string {
 | 
				
			||||||
	var res string
 | 
						var res string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -110,6 +115,7 @@ type historyRecord struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type queryOptions struct {
 | 
					type queryOptions struct {
 | 
				
			||||||
	CompletedOnly bool
 | 
						CompletedOnly bool
 | 
				
			||||||
 | 
						Filters       []string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *queryOptions) ([]historyRecord, error) {
 | 
					func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *queryOptions) ([]historyRecord, error) {
 | 
				
			||||||
@@ -126,6 +132,11 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q
 | 
				
			|||||||
		ref = ""
 | 
							ref = ""
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var filters []string
 | 
				
			||||||
 | 
						if opts != nil {
 | 
				
			||||||
 | 
							filters = opts.Filters
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	eg, ctx := errgroup.WithContext(ctx)
 | 
						eg, ctx := errgroup.WithContext(ctx)
 | 
				
			||||||
	for _, node := range nodes {
 | 
						for _, node := range nodes {
 | 
				
			||||||
		node := node
 | 
							node := node
 | 
				
			||||||
@@ -138,9 +149,25 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q
 | 
				
			|||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				return err
 | 
									return err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var matchers []matchFunc
 | 
				
			||||||
 | 
								if len(filters) > 0 {
 | 
				
			||||||
 | 
									filters, matchers, err = dockerFiltersToBuildkit(filters)
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										return err
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									sb := bytes.NewBuffer(nil)
 | 
				
			||||||
 | 
									w := csv.NewWriter(sb)
 | 
				
			||||||
 | 
									w.Write(filters)
 | 
				
			||||||
 | 
									w.Flush()
 | 
				
			||||||
 | 
									filters = []string{strings.TrimSuffix(sb.String(), "\n")}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			serv, err := c.ControlClient().ListenBuildHistory(ctx, &controlapi.BuildHistoryRequest{
 | 
								serv, err := c.ControlClient().ListenBuildHistory(ctx, &controlapi.BuildHistoryRequest{
 | 
				
			||||||
				EarlyExit: true,
 | 
									EarlyExit: true,
 | 
				
			||||||
				Ref:       ref,
 | 
									Ref:       ref,
 | 
				
			||||||
 | 
									Limit:     recordsLimit,
 | 
				
			||||||
 | 
									Filter:    filters,
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				return err
 | 
									return err
 | 
				
			||||||
@@ -158,6 +185,7 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q
 | 
				
			|||||||
				ts = &t
 | 
									ts = &t
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			defer serv.CloseSend()
 | 
								defer serv.CloseSend()
 | 
				
			||||||
 | 
							loop0:
 | 
				
			||||||
			for {
 | 
								for {
 | 
				
			||||||
				he, err := serv.Recv()
 | 
									he, err := serv.Recv()
 | 
				
			||||||
				if err != nil {
 | 
									if err != nil {
 | 
				
			||||||
@@ -173,6 +201,13 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q
 | 
				
			|||||||
					continue
 | 
										continue
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// for older buildkit that don't support filters apply local filters
 | 
				
			||||||
 | 
									for _, matcher := range matchers {
 | 
				
			||||||
 | 
										if !matcher(he.Record) {
 | 
				
			||||||
 | 
											continue loop0
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				records = append(records, historyRecord{
 | 
									records = append(records, historyRecord{
 | 
				
			||||||
					BuildHistoryRecord: he.Record,
 | 
										BuildHistoryRecord: he.Record,
 | 
				
			||||||
					currentTimestamp:   ts,
 | 
										currentTimestamp:   ts,
 | 
				
			||||||
@@ -219,3 +254,150 @@ func formatDuration(d time.Duration) string {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	return fmt.Sprintf("%dm %2ds", int(d.Minutes()), int(d.Seconds())%60)
 | 
						return fmt.Sprintf("%dm %2ds", int(d.Minutes()), int(d.Seconds())%60)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type matchFunc func(*controlapi.BuildHistoryRecord) bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func dockerFiltersToBuildkit(in []string) ([]string, []matchFunc, error) {
 | 
				
			||||||
 | 
						out := []string{}
 | 
				
			||||||
 | 
						matchers := []matchFunc{}
 | 
				
			||||||
 | 
						for _, f := range in {
 | 
				
			||||||
 | 
							key, value, sep, found := cutAny(f, "!=", "=", "<=", "<", ">=", ">")
 | 
				
			||||||
 | 
							if !found {
 | 
				
			||||||
 | 
								return nil, nil, errors.Errorf("invalid filter %q", f)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							switch key {
 | 
				
			||||||
 | 
							case "ref", "repository", "status":
 | 
				
			||||||
 | 
								if sep != "=" && sep != "!=" {
 | 
				
			||||||
 | 
									return nil, nil, errors.Errorf("invalid separator for %q, expected = or !=", f)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								matchers = append(matchers, valueFiler(key, value, sep))
 | 
				
			||||||
 | 
								if sep == "=" {
 | 
				
			||||||
 | 
									if key == "status" {
 | 
				
			||||||
 | 
										sep = "=="
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										sep = "~="
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							case "startedAt", "completedAt", "duration":
 | 
				
			||||||
 | 
								if sep == "=" || sep == "!=" {
 | 
				
			||||||
 | 
									return nil, nil, errors.Errorf("invalid separator for %q, expected <=, <, >= or >", f)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								matcher, err := timeBasedFilter(key, value, sep)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, nil, err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								matchers = append(matchers, matcher)
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								return nil, nil, errors.Errorf("unsupported filter %q", f)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							out = append(out, key+sep+value)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return out, matchers, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func valueFiler(key, value, sep string) matchFunc {
 | 
				
			||||||
 | 
						return func(rec *controlapi.BuildHistoryRecord) bool {
 | 
				
			||||||
 | 
							var recValue string
 | 
				
			||||||
 | 
							switch key {
 | 
				
			||||||
 | 
							case "ref":
 | 
				
			||||||
 | 
								recValue = rec.Ref
 | 
				
			||||||
 | 
							case "repository":
 | 
				
			||||||
 | 
								v, ok := rec.FrontendAttrs["vcs:source"]
 | 
				
			||||||
 | 
								if ok {
 | 
				
			||||||
 | 
									recValue = v
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									if context, ok := rec.FrontendAttrs["context"]; ok {
 | 
				
			||||||
 | 
										if ref, err := gitutil.ParseGitRef(context); err == nil {
 | 
				
			||||||
 | 
											recValue = ref.Remote
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							case "status":
 | 
				
			||||||
 | 
								if rec.CompletedAt != nil {
 | 
				
			||||||
 | 
									if rec.Error != nil {
 | 
				
			||||||
 | 
										if strings.Contains(rec.Error.Message, "context canceled") {
 | 
				
			||||||
 | 
											recValue = "canceled"
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											recValue = "error"
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										recValue = "completed"
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									recValue = "running"
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							switch sep {
 | 
				
			||||||
 | 
							case "=":
 | 
				
			||||||
 | 
								if key == "status" {
 | 
				
			||||||
 | 
									return recValue == value
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return strings.Contains(recValue, value)
 | 
				
			||||||
 | 
							case "!=":
 | 
				
			||||||
 | 
								return recValue != value
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func timeBasedFilter(key, value, sep string) (matchFunc, error) {
 | 
				
			||||||
 | 
						var cmp int64
 | 
				
			||||||
 | 
						switch key {
 | 
				
			||||||
 | 
						case "startedAt", "completedAt":
 | 
				
			||||||
 | 
							v, err := time.ParseDuration(value)
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								tm := time.Now().Add(-v)
 | 
				
			||||||
 | 
								cmp = tm.Unix()
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								tm, err := time.Parse(time.RFC3339, value)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return nil, errors.Errorf("invalid time %s", value)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								cmp = tm.Unix()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						case "duration":
 | 
				
			||||||
 | 
							v, err := time.ParseDuration(value)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return nil, errors.Errorf("invalid duration %s", value)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							cmp = int64(v)
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return nil, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return func(rec *controlapi.BuildHistoryRecord) bool {
 | 
				
			||||||
 | 
							var val int64
 | 
				
			||||||
 | 
							switch key {
 | 
				
			||||||
 | 
							case "startedAt":
 | 
				
			||||||
 | 
								val = rec.CreatedAt.AsTime().Unix()
 | 
				
			||||||
 | 
							case "completedAt":
 | 
				
			||||||
 | 
								if rec.CompletedAt != nil {
 | 
				
			||||||
 | 
									val = rec.CompletedAt.AsTime().Unix()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							case "duration":
 | 
				
			||||||
 | 
								if rec.CompletedAt != nil {
 | 
				
			||||||
 | 
									val = int64(rec.CompletedAt.AsTime().Sub(rec.CreatedAt.AsTime()))
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							switch sep {
 | 
				
			||||||
 | 
							case ">=":
 | 
				
			||||||
 | 
								return val >= cmp
 | 
				
			||||||
 | 
							case "<=":
 | 
				
			||||||
 | 
								return val <= cmp
 | 
				
			||||||
 | 
							case ">":
 | 
				
			||||||
 | 
								return val > cmp
 | 
				
			||||||
 | 
							default:
 | 
				
			||||||
 | 
								return val < cmp
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func cutAny(s string, seps ...string) (before, after, sep string, found bool) {
 | 
				
			||||||
 | 
						for _, sep := range seps {
 | 
				
			||||||
 | 
							if idx := strings.Index(s, sep); idx != -1 {
 | 
				
			||||||
 | 
								return s[:idx], s[idx+len(sep):], sep, true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return s, "", "", false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,12 +5,14 @@ List build records
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### Options
 | 
					### Options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| Name            | Type     | Default | Description                              |
 | 
					| Name            | Type          | Default | Description                                  |
 | 
				
			||||||
|:----------------|:---------|:--------|:-----------------------------------------|
 | 
					|:----------------|:--------------|:--------|:---------------------------------------------|
 | 
				
			||||||
| `--builder`     | `string` |         | Override the configured builder instance |
 | 
					| `--builder`     | `string`      |         | Override the configured builder instance     |
 | 
				
			||||||
| `-D`, `--debug` | `bool`   |         | Enable debug logging                     |
 | 
					| `-D`, `--debug` | `bool`        |         | Enable debug logging                         |
 | 
				
			||||||
| `--format`      | `string` | `table` | Format the output                        |
 | 
					| `--filter`      | `stringArray` |         | Provide filter values (e.g., `status=error`) |
 | 
				
			||||||
| `--no-trunc`    | `bool`   |         | Don't truncate output                    |
 | 
					| `--format`      | `string`      | `table` | Format the output                            |
 | 
				
			||||||
 | 
					| `--local`       | `bool`        |         | List records for current repository only     |
 | 
				
			||||||
 | 
					| `--no-trunc`    | `bool`        |         | Don't truncate output                        |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<!---MARKER_GEN_END-->
 | 
					<!---MARKER_GEN_END-->
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user