-
Notifications
You must be signed in to change notification settings - Fork 545
history: add filters to ls #3091
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,9 @@ | ||
package history | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/csv" | ||
"fmt" | ||
"io" | ||
"path/filepath" | ||
|
@@ -15,10 +17,13 @@ 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" | ||
) | ||
|
||
const recordsLimit = 50 | ||
|
||
func buildName(fattrs map[string]string, ls *localstate.State) string { | ||
var res string | ||
|
||
|
@@ -110,6 +115,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 +132,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 +149,25 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q | |
if err != nil { | ||
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{ | ||
EarlyExit: true, | ||
Ref: ref, | ||
Limit: recordsLimit, | ||
Filter: filters, | ||
}) | ||
if err != nil { | ||
return err | ||
|
@@ -158,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 { | ||
|
@@ -173,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, | ||
|
@@ -219,3 +254,150 @@ func formatDuration(d time.Duration) string { | |
} | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm surprised we don't have an operator for "contains" with docker filters like |
||
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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`) | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As follow-up we should document the available filters with some examples similar to https://docs.docker.com/reference/cli/docker/container/ls/#filter There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah found pr description we could grab in the docs: moby/buildkit#5705 (comment) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that separators are different. BuildKit uses containerd style while buildx uses docker style. |
||
| `--format` | `string` | `table` | Format the output | | ||
| `--local` | `bool` | | List records for current repository only | | ||
| `--no-trunc` | `bool` | | Don't truncate output | | ||
|
||
|
||
<!---MARKER_GEN_END--> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok I see we set it here after
valueFiler
matching moby/buildkit#5705 (comment)