1
1
package history
2
2
3
3
import (
4
+ "bytes"
4
5
"context"
6
+ "encoding/csv"
5
7
"fmt"
6
8
"io"
7
9
"path/filepath"
@@ -15,10 +17,13 @@ import (
15
17
"github.com/docker/buildx/builder"
16
18
"github.com/docker/buildx/localstate"
17
19
controlapi "github.com/moby/buildkit/api/services/control"
20
+ "github.com/moby/buildkit/util/gitutil"
18
21
"github.com/pkg/errors"
19
22
"golang.org/x/sync/errgroup"
20
23
)
21
24
25
+ const recordsLimit = 50
26
+
22
27
func buildName (fattrs map [string ]string , ls * localstate.State ) string {
23
28
var res string
24
29
@@ -110,6 +115,7 @@ type historyRecord struct {
110
115
111
116
type queryOptions struct {
112
117
CompletedOnly bool
118
+ Filters []string
113
119
}
114
120
115
121
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
126
132
ref = ""
127
133
}
128
134
135
+ var filters []string
136
+ if opts != nil {
137
+ filters = opts .Filters
138
+ }
139
+
129
140
eg , ctx := errgroup .WithContext (ctx )
130
141
for _ , node := range nodes {
131
142
node := node
@@ -138,9 +149,25 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q
138
149
if err != nil {
139
150
return err
140
151
}
152
+
153
+ var matchers []matchFunc
154
+ if len (filters ) > 0 {
155
+ filters , matchers , err = dockerFiltersToBuildkit (filters )
156
+ if err != nil {
157
+ return err
158
+ }
159
+ sb := bytes .NewBuffer (nil )
160
+ w := csv .NewWriter (sb )
161
+ w .Write (filters )
162
+ w .Flush ()
163
+ filters = []string {strings .TrimSuffix (sb .String (), "\n " )}
164
+ }
165
+
141
166
serv , err := c .ControlClient ().ListenBuildHistory (ctx , & controlapi.BuildHistoryRequest {
142
167
EarlyExit : true ,
143
168
Ref : ref ,
169
+ Limit : recordsLimit ,
170
+ Filter : filters ,
144
171
})
145
172
if err != nil {
146
173
return err
@@ -158,6 +185,7 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q
158
185
ts = & t
159
186
}
160
187
defer serv .CloseSend ()
188
+ loop0:
161
189
for {
162
190
he , err := serv .Recv ()
163
191
if err != nil {
@@ -173,6 +201,13 @@ func queryRecords(ctx context.Context, ref string, nodes []builder.Node, opts *q
173
201
continue
174
202
}
175
203
204
+ // for older buildkit that don't support filters apply local filters
205
+ for _ , matcher := range matchers {
206
+ if ! matcher (he .Record ) {
207
+ continue loop0
208
+ }
209
+ }
210
+
176
211
records = append (records , historyRecord {
177
212
BuildHistoryRecord : he .Record ,
178
213
currentTimestamp : ts ,
@@ -219,3 +254,150 @@ func formatDuration(d time.Duration) string {
219
254
}
220
255
return fmt .Sprintf ("%dm %2ds" , int (d .Minutes ()), int (d .Seconds ())% 60 )
221
256
}
257
+
258
+ type matchFunc func (* controlapi.BuildHistoryRecord ) bool
259
+
260
+ func dockerFiltersToBuildkit (in []string ) ([]string , []matchFunc , error ) {
261
+ out := []string {}
262
+ matchers := []matchFunc {}
263
+ for _ , f := range in {
264
+ key , value , sep , found := cutAny (f , "!=" , "=" , "<=" , "<" , ">=" , ">" )
265
+ if ! found {
266
+ return nil , nil , errors .Errorf ("invalid filter %q" , f )
267
+ }
268
+ switch key {
269
+ case "ref" , "repository" , "status" :
270
+ if sep != "=" && sep != "!=" {
271
+ return nil , nil , errors .Errorf ("invalid separator for %q, expected = or !=" , f )
272
+ }
273
+ matchers = append (matchers , valueFiler (key , value , sep ))
274
+ if sep == "=" {
275
+ if key == "status" {
276
+ sep = "=="
277
+ } else {
278
+ sep = "~="
279
+ }
280
+ }
281
+ case "startedAt" , "completedAt" , "duration" :
282
+ if sep == "=" || sep == "!=" {
283
+ return nil , nil , errors .Errorf ("invalid separator for %q, expected <=, <, >= or >" , f )
284
+ }
285
+ matcher , err := timeBasedFilter (key , value , sep )
286
+ if err != nil {
287
+ return nil , nil , err
288
+ }
289
+ matchers = append (matchers , matcher )
290
+ default :
291
+ return nil , nil , errors .Errorf ("unsupported filter %q" , f )
292
+ }
293
+ out = append (out , key + sep + value )
294
+ }
295
+ return out , matchers , nil
296
+ }
297
+
298
+ func valueFiler (key , value , sep string ) matchFunc {
299
+ return func (rec * controlapi.BuildHistoryRecord ) bool {
300
+ var recValue string
301
+ switch key {
302
+ case "ref" :
303
+ recValue = rec .Ref
304
+ case "repository" :
305
+ v , ok := rec .FrontendAttrs ["vcs:source" ]
306
+ if ok {
307
+ recValue = v
308
+ } else {
309
+ if context , ok := rec .FrontendAttrs ["context" ]; ok {
310
+ if ref , err := gitutil .ParseGitRef (context ); err == nil {
311
+ recValue = ref .Remote
312
+ }
313
+ }
314
+ }
315
+ case "status" :
316
+ if rec .CompletedAt != nil {
317
+ if rec .Error != nil {
318
+ if strings .Contains (rec .Error .Message , "context canceled" ) {
319
+ recValue = "canceled"
320
+ } else {
321
+ recValue = "error"
322
+ }
323
+ } else {
324
+ recValue = "completed"
325
+ }
326
+ } else {
327
+ recValue = "running"
328
+ }
329
+ }
330
+ switch sep {
331
+ case "=" :
332
+ if key == "status" {
333
+ return recValue == value
334
+ }
335
+ return strings .Contains (recValue , value )
336
+ case "!=" :
337
+ return recValue != value
338
+ default :
339
+ return false
340
+ }
341
+ }
342
+ }
343
+
344
+ func timeBasedFilter (key , value , sep string ) (matchFunc , error ) {
345
+ var cmp int64
346
+ switch key {
347
+ case "startedAt" , "completedAt" :
348
+ v , err := time .ParseDuration (value )
349
+ if err == nil {
350
+ tm := time .Now ().Add (- v )
351
+ cmp = tm .Unix ()
352
+ } else {
353
+ tm , err := time .Parse (time .RFC3339 , value )
354
+ if err != nil {
355
+ return nil , errors .Errorf ("invalid time %s" , value )
356
+ }
357
+ cmp = tm .Unix ()
358
+ }
359
+ case "duration" :
360
+ v , err := time .ParseDuration (value )
361
+ if err != nil {
362
+ return nil , errors .Errorf ("invalid duration %s" , value )
363
+ }
364
+ cmp = int64 (v )
365
+ default :
366
+ return nil , nil
367
+ }
368
+
369
+ return func (rec * controlapi.BuildHistoryRecord ) bool {
370
+ var val int64
371
+ switch key {
372
+ case "startedAt" :
373
+ val = rec .CreatedAt .AsTime ().Unix ()
374
+ case "completedAt" :
375
+ if rec .CompletedAt != nil {
376
+ val = rec .CompletedAt .AsTime ().Unix ()
377
+ }
378
+ case "duration" :
379
+ if rec .CompletedAt != nil {
380
+ val = int64 (rec .CompletedAt .AsTime ().Sub (rec .CreatedAt .AsTime ()))
381
+ }
382
+ }
383
+ switch sep {
384
+ case ">=" :
385
+ return val >= cmp
386
+ case "<=" :
387
+ return val <= cmp
388
+ case ">" :
389
+ return val > cmp
390
+ default :
391
+ return val < cmp
392
+ }
393
+ }, nil
394
+ }
395
+
396
+ func cutAny (s string , seps ... string ) (before , after , sep string , found bool ) {
397
+ for _ , sep := range seps {
398
+ if idx := strings .Index (s , sep ); idx != - 1 {
399
+ return s [:idx ], s [idx + len (sep ):], sep , true
400
+ }
401
+ }
402
+ return s , "" , "" , false
403
+ }
0 commit comments