From f1b895196c7449f1664448cfb04e3594bb3eaf23 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Mon, 7 Apr 2025 22:14:39 -0700 Subject: [PATCH] history: add local filters for older buildkit versions Signed-off-by: Tonis Tiigi --- commands/history/utils.go | 137 ++++++++++++++++++++++++++-- docs/reference/buildx_history_ls.md | 14 +-- 2 files changed, 136 insertions(+), 15 deletions(-) diff --git a/commands/history/utils.go b/commands/history/utils.go index 2d24eef0..95c8702e 100644 --- a/commands/history/utils.go +++ b/commands/history/utils.go @@ -17,6 +17,7 @@ import ( "github.com/docker/buildx/builder" "github.com/docker/buildx/localstate" controlapi "github.com/moby/buildkit/api/services/control" + "github.com/moby/buildkit/util/gitutil" "github.com/pkg/errors" "golang.org/x/sync/errgroup" ) @@ -149,8 +150,9 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q return err } + var matchers []matchFunc if len(filters) > 0 { - filters, err = dockerFiltersToBuildkit(filters) + filters, matchers, err = dockerFiltersToBuildkit(filters) if err != nil { return err } @@ -183,6 +185,7 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q ts = &t } defer serv.CloseSend() + loop0: for { he, err := serv.Recv() if err != nil { @@ -198,6 +201,13 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q 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{ BuildHistoryRecord: he.Record, currentTimestamp: ts, @@ -245,18 +255,22 @@ func formatDuration(d time.Duration) string { return fmt.Sprintf("%dm %2ds", int(d.Minutes()), int(d.Seconds())%60) } -func dockerFiltersToBuildkit(in []string) ([]string, error) { +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 := multiCut(f, "=", "!=", "<=", "<", ">=", ">") + key, value, sep, found := cutAny(f, "!=", "=", "<=", "<", ">=", ">") if !found { - return nil, errors.Errorf("invalid filter %q", f) + return nil, 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) + 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 = "==" @@ -264,17 +278,122 @@ func dockerFiltersToBuildkit(in []string) ([]string, error) { sep = "~=" } } - case "createdAt", "completedAt", "duration": + case "startedAt", "completedAt", "duration": if sep == "=" || sep == "!=" { - return nil, errors.Errorf("invalid separator for %q, expected <=, <, >= or >", f) + 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, nil + return out, matchers, nil } -func multiCut(s string, seps ...string) (before, after, sep string, found bool) { +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 diff --git a/docs/reference/buildx_history_ls.md b/docs/reference/buildx_history_ls.md index e03b64e4..e63224b1 100644 --- a/docs/reference/buildx_history_ls.md +++ b/docs/reference/buildx_history_ls.md @@ -5,12 +5,14 @@ List build records ### Options -| Name | Type | Default | Description | -|:----------------|:---------|:--------|:-----------------------------------------| -| `--builder` | `string` | | Override the configured builder instance | -| `-D`, `--debug` | `bool` | | Enable debug logging | -| `--format` | `string` | `table` | Format the output | -| `--no-trunc` | `bool` | | Don't truncate output | +| Name | Type | Default | Description | +|:----------------|:--------------|:--------|:---------------------------------------------| +| `--builder` | `string` | | Override the configured builder instance | +| `-D`, `--debug` | `bool` | | Enable debug logging | +| `--filter` | `stringArray` | | Provide filter values (e.g., `status=error`) | +| `--format` | `string` | `table` | Format the output | +| `--local` | `bool` | | List records for current repository only | +| `--no-trunc` | `bool` | | Don't truncate output |