@@ -20,9 +20,11 @@ import (
2020 "fmt"
2121 "log"
2222 "net/http"
23+ "os"
2324 "path"
2425 "runtime"
2526 "strings"
27+ "time"
2628
2729 "github.com/google/go-containerregistry/pkg/authn"
2830 "github.com/google/go-containerregistry/pkg/name"
@@ -33,6 +35,7 @@ import (
3335 ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
3436 "github.com/sigstore/cosign/v2/pkg/oci/walk"
3537 "golang.org/x/sync/errgroup"
38+ "golang.org/x/term"
3639
3740 "github.com/google/ko/pkg/build"
3841)
@@ -47,6 +50,7 @@ type defalt struct {
4750 jobs int
4851
4952 pusher * remote.Pusher
53+ ropt []remote.Option
5054 oopt []ociremote.Option
5155}
5256
@@ -115,6 +119,7 @@ func (do *defaultOpener) Open() (Interface, error) {
115119 insecure : do .insecure ,
116120 jobs : do .jobs ,
117121 pusher : pusher ,
122+ ropt : do .ropt ,
118123 oopt : oopt ,
119124 }, nil
120125}
@@ -156,7 +161,7 @@ func (d *defalt) pushResult(ctx context.Context, tag name.Tag, br build.Result)
156161 g .SetLimit (d .jobs )
157162
158163 g .Go (func () error {
159- return d .pusher . Push (ctx , tag , br )
164+ return d .pushWithProgress (ctx , tag , br )
160165 })
161166
162167 // writePeripherals implements walk.Fn
@@ -222,6 +227,139 @@ func (d *defalt) pushResult(ctx context.Context, tag name.Tag, br build.Result)
222227 return g .Wait ()
223228}
224229
230+ func (d * defalt ) pushWithProgress (ctx context.Context , ref name.Reference , target remote.Taggable ) error {
231+ updates := make (chan v1.Update , 128 )
232+ options := append ([]remote.Option {}, d .ropt ... )
233+ options = append (options , remote .WithProgress (updates ), remote .WithContext (ctx ))
234+
235+ errCh := make (chan error , 1 )
236+ go func () {
237+ errCh <- remote .Push (ref , target , options ... )
238+ }()
239+
240+ start := time .Now ()
241+ lastPrint := start
242+ lastComplete := int64 (0 )
243+ total := int64 (0 )
244+ complete := int64 (0 )
245+ var pushErr error
246+ updatesOpen := true
247+ pushDone := false
248+ ticker := time .NewTicker (time .Second )
249+ defer ticker .Stop ()
250+ inlineProgress := term .IsTerminal (int (os .Stderr .Fd ()))
251+ printedInline := false
252+ lastInlineLen := 0
253+
254+ for updatesOpen || ! pushDone {
255+ select {
256+ case <- ctx .Done ():
257+ if inlineProgress && printedInline {
258+ fmt .Fprintln (os .Stderr )
259+ }
260+ return ctx .Err ()
261+ case update , ok := <- updates :
262+ if ! ok {
263+ updatesOpen = false
264+ continue
265+ }
266+ if update .Total > 0 {
267+ total = update .Total
268+ }
269+ if update .Complete > complete {
270+ complete = update .Complete
271+ }
272+ case pushErr = <- errCh :
273+ pushDone = true
274+ case now := <- ticker .C :
275+ if complete == 0 || complete == lastComplete {
276+ continue
277+ }
278+ rate := bytesPerSecond (complete - lastComplete , now .Sub (lastPrint ))
279+ if inlineProgress {
280+ line := progressLineWithRef (inlineRefName (ref ), complete , total , rate )
281+ lastInlineLen = printInlineProgress (line , lastInlineLen )
282+ printedInline = true
283+ } else {
284+ logProgress (ref , complete , total , rate )
285+ }
286+ lastComplete = complete
287+ lastPrint = now
288+ }
289+ }
290+
291+ if complete > 0 || total > 0 {
292+ elapsedRate := bytesPerSecond (complete , time .Since (start ))
293+ if inlineProgress {
294+ line := progressLineWithRef (inlineRefName (ref ), complete , total , elapsedRate )
295+ printInlineProgress (line , lastInlineLen )
296+ fmt .Fprintln (os .Stderr )
297+ } else {
298+ logProgress (ref , complete , total , elapsedRate )
299+ }
300+ } else if inlineProgress && printedInline {
301+ fmt .Fprintln (os .Stderr )
302+ }
303+ return pushErr
304+ }
305+
306+ func logProgress (ref name.Reference , complete , total int64 , rate float64 ) {
307+ log .Printf ("%s" , progressLineWithRef (ref .Name (), complete , total , rate ))
308+ }
309+
310+ func progressLineWithRef (refName string , complete , total int64 , rate float64 ) string {
311+ if total > 0 {
312+ return fmt .Sprintf ("Push progress %s: %.1f%% (%s/%s) at %s" ,
313+ refName ,
314+ 100 * float64 (complete )/ float64 (total ),
315+ formatMiB (complete ),
316+ formatMiB (total ),
317+ formatRate (rate ))
318+ }
319+ return fmt .Sprintf ("Push progress %s: %s uploaded at %s" ,
320+ refName ,
321+ formatMiB (complete ),
322+ formatRate (rate ))
323+ }
324+
325+ func printInlineProgress (line string , previousLen int ) int {
326+ // Use CR + spaces to keep progress on a single line without ANSI reliance.
327+ padding := ""
328+ if previousLen > len (line ) {
329+ padding = strings .Repeat (" " , previousLen - len (line ))
330+ }
331+ fmt .Fprintf (os .Stderr , "\r %s%s" , line , padding )
332+ return len (line )
333+ }
334+
335+ func inlineRefName (ref name.Reference ) string {
336+ refName := ref .Name ()
337+ // Keep inline output short to avoid terminal line wrapping.
338+ if slash := strings .LastIndex (refName , "/" ); slash >= 0 && slash + 1 < len (refName ) {
339+ refName = refName [slash + 1 :]
340+ }
341+ const maxInlineRef = 64
342+ if len (refName ) > maxInlineRef {
343+ refName = "..." + refName [len (refName )- (maxInlineRef - 3 ):]
344+ }
345+ return refName
346+ }
347+
348+ func bytesPerSecond (bytes int64 , d time.Duration ) float64 {
349+ if d <= 0 {
350+ return 0
351+ }
352+ return float64 (bytes ) / d .Seconds ()
353+ }
354+
355+ func formatMiB (bytes int64 ) string {
356+ return fmt .Sprintf ("%.2fMiB" , float64 (bytes )/ (1024 * 1024 ))
357+ }
358+
359+ func formatRate (bytesPerSecond float64 ) string {
360+ return fmt .Sprintf ("%.2fMiB/s" , bytesPerSecond / (1024 * 1024 ))
361+ }
362+
225363// Publish implements publish.Interface
226364func (d * defalt ) Publish (ctx context.Context , br build.Result , s string ) (name.Reference , error ) {
227365 s = strings .TrimPrefix (s , build .StrictScheme )
0 commit comments