From 900502b1398010a45992efc1d059ed44c903fe1a Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Mon, 7 Apr 2025 21:32:43 -0700 Subject: [PATCH] history: add filters to ls Signed-off-by: Tonis Tiigi --- commands/history/ls.go | 32 +++++++++++++++++++- commands/history/utils.go | 63 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/commands/history/ls.go b/commands/history/ls.go index 5a6e9d4e..c902a343 100644 --- a/commands/history/ls.go +++ b/commands/history/ls.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "path" "slices" "time" @@ -14,10 +15,12 @@ import ( "github.com/docker/buildx/util/cobrautil/completion" "github.com/docker/buildx/util/confutil" "github.com/docker/buildx/util/desktop" + "github.com/docker/buildx/util/gitutil" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" "github.com/docker/go-units" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -38,6 +41,9 @@ type lsOptions struct { builder string format string noTrunc bool + + filters []string + local bool } 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 { return err } @@ -92,6 +120,8 @@ func lsCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command { flags := cmd.Flags() flags.StringVar(&options.format, "format", formatter.TableFormatKey, "Format the 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 } diff --git a/commands/history/utils.go b/commands/history/utils.go index c74b7c7c..2d24eef0 100644 --- a/commands/history/utils.go +++ b/commands/history/utils.go @@ -1,7 +1,9 @@ package history import ( + "bytes" "context" + "encoding/csv" "fmt" "io" "path/filepath" @@ -19,6 +21,8 @@ import ( "golang.org/x/sync/errgroup" ) +const recordsLimit = 50 + func buildName(fattrs map[string]string, ls *localstate.State) string { var res string @@ -110,6 +114,7 @@ type historyRecord struct { type queryOptions struct { CompletedOnly bool + Filters []string } func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *queryOptions) ([]historyRecord, error) { @@ -126,6 +131,11 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q ref = "" } + var filters []string + if opts != nil { + filters = opts.Filters + } + eg, ctx := errgroup.WithContext(ctx) for _, node := range nodes { node := node @@ -138,9 +148,24 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q if err != nil { return err } + + if len(filters) > 0 { + filters, 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{ EarlyExit: true, Ref: ref, + Limit: recordsLimit, + Filter: filters, }) if err != nil { return err @@ -219,3 +244,41 @@ func formatDuration(d time.Duration) string { } return fmt.Sprintf("%dm %2ds", int(d.Minutes()), int(d.Seconds())%60) } + +func dockerFiltersToBuildkit(in []string) ([]string, error) { + out := []string{} + for _, f := range in { + key, value, sep, found := multiCut(f, "=", "!=", "<=", "<", ">=", ">") + if !found { + return nil, errors.Errorf("invalid filter %q", f) + } + switch key { + case "ref", "repository", "status": + if sep != "=" && sep != "!=" { + return nil, errors.Errorf("invalid separator for %q, expected = or !=", f) + } + if sep == "=" { + if key == "status" { + sep = "==" + } else { + sep = "~=" + } + } + case "createdAt", "completedAt", "duration": + if sep == "=" || sep == "!=" { + return nil, errors.Errorf("invalid separator for %q, expected <=, <, >= or >", f) + } + } + out = append(out, key+sep+value) + } + return out, nil +} + +func multiCut(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 +}