Andrey Smirnov f2dd08594e
feat: report image pull progress in the console
This reports image pull progress in the console for images pulled by
Talos:

* etcd
* kubelet
* installer

This work was mostly done by @laurazard, I just wrapped it for the
console with Laura's help. (see #12932)

Co-authored-by: Laura Brehm <laurabrehm@hey.com>
Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
2026-01-19 18:00:13 +04:00

139 lines
2.8 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package console provides a console-based implementation of image pull progress reporting.
package console
import (
"log"
"sync"
"time"
"github.com/dustin/go-humanize"
"github.com/siderolabs/talos/internal/pkg/containers/image"
"github.com/siderolabs/talos/internal/pkg/containers/image/progress"
)
// ReportInterval is the interval between progress reports.
const ReportInterval = 15 * time.Second
type layerProgress struct {
status progress.LayerPullStatus
offset int64
total int64
}
// ProgressReporter reports image pull progress to the console.
type ProgressReporter struct {
imageRef string
mu sync.Mutex
layers map[string]*layerProgress
stopCh chan struct{}
}
// NewProgressReporter creates a new ProgressReporter.
func NewProgressReporter(imageRef string) image.ProgressReporter {
return &ProgressReporter{
imageRef: imageRef,
}
}
// Update implements UpdateFn interface.
func (c *ProgressReporter) Update(upd progress.LayerPullProgress) {
c.mu.Lock()
defer c.mu.Unlock()
if c.layers == nil {
c.layers = make(map[string]*layerProgress)
}
lp, ok := c.layers[upd.LayerID]
if !ok {
lp = &layerProgress{}
c.layers[upd.LayerID] = lp
}
if upd.Status == progress.LayerPullStatusDownloading {
lp.total = upd.Total
lp.offset = upd.Offset
} else {
lp.offset = lp.total
}
lp.status = upd.Status
}
// Start implements ProgressReporter interface.
func (c *ProgressReporter) Start() {
c.stopCh = make(chan struct{})
go func() {
ticker := time.NewTicker(ReportInterval)
defer ticker.Stop()
c.reportProgress()
for {
select {
case <-ticker.C:
c.reportProgress()
case <-c.stopCh:
return
}
}
}()
}
// Stop implements ProgressReporter interface.
func (c *ProgressReporter) Stop() {
close(c.stopCh)
}
func (c *ProgressReporter) reportProgress() {
c.mu.Lock()
defer c.mu.Unlock()
if len(c.layers) == 0 {
log.Printf("pulling image %s: starting...", c.imageRef)
return
}
var (
anyDownloading bool
overallOffset int64
overallTotal int64
)
for _, l := range c.layers {
if l.status == progress.LayerPullStatusDownloading {
anyDownloading = true
}
overallOffset += l.offset
overallTotal += l.total
}
if !anyDownloading {
log.Printf("pulling image %s: extracting...", c.imageRef)
return
}
var percentage float64
if overallTotal > 0 {
percentage = float64(overallOffset) / float64(overallTotal) * 100.0
}
log.Printf("pulling image %s: downloading %d layers (%s/%s) (%.2f%%)...",
c.imageRef, len(c.layers),
humanize.IBytes(uint64(overallOffset)),
humanize.IBytes(uint64(overallTotal)),
percentage,
)
}