github.com/moby/docker@v26.1.3+incompatible/daemon/containerd/progress.go (about)

     1  package containerd
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/containerd/containerd/content"
    10  	cerrdefs "github.com/containerd/containerd/errdefs"
    11  	"github.com/containerd/containerd/images"
    12  	"github.com/containerd/containerd/remotes"
    13  	"github.com/containerd/containerd/remotes/docker"
    14  	"github.com/containerd/log"
    15  	"github.com/distribution/reference"
    16  	"github.com/docker/docker/internal/compatcontext"
    17  	"github.com/docker/docker/pkg/progress"
    18  	"github.com/docker/docker/pkg/stringid"
    19  	"github.com/opencontainers/go-digest"
    20  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    21  )
    22  
    23  type progressUpdater interface {
    24  	UpdateProgress(context.Context, *jobs, progress.Output, time.Time) error
    25  }
    26  
    27  type jobs struct {
    28  	descs map[digest.Digest]ocispec.Descriptor
    29  	mu    sync.Mutex
    30  }
    31  
    32  // newJobs creates a new instance of the job status tracker
    33  func newJobs() *jobs {
    34  	return &jobs{
    35  		descs: map[digest.Digest]ocispec.Descriptor{},
    36  	}
    37  }
    38  
    39  func (j *jobs) showProgress(ctx context.Context, out progress.Output, updater progressUpdater) func() {
    40  	ctx, cancelProgress := context.WithCancel(ctx)
    41  
    42  	start := time.Now()
    43  	lastUpdate := make(chan struct{})
    44  
    45  	go func() {
    46  		ticker := time.NewTicker(100 * time.Millisecond)
    47  		defer ticker.Stop()
    48  
    49  		for {
    50  			select {
    51  			case <-ticker.C:
    52  				if err := updater.UpdateProgress(ctx, j, out, start); err != nil {
    53  					if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
    54  						log.G(ctx).WithError(err).Error("Updating progress failed")
    55  					}
    56  				}
    57  			case <-ctx.Done():
    58  				ctx, cancel := context.WithTimeout(compatcontext.WithoutCancel(ctx), time.Millisecond*500)
    59  				defer cancel()
    60  				updater.UpdateProgress(ctx, j, out, start)
    61  				close(lastUpdate)
    62  				return
    63  			}
    64  		}
    65  	}()
    66  
    67  	return func() {
    68  		cancelProgress()
    69  		// Wait for the last update to finish.
    70  		// UpdateProgress may still write progress to output and we need
    71  		// to keep the caller from closing it before we finish.
    72  		<-lastUpdate
    73  	}
    74  }
    75  
    76  // Add adds a descriptor to be tracked
    77  func (j *jobs) Add(desc ...ocispec.Descriptor) {
    78  	j.mu.Lock()
    79  	defer j.mu.Unlock()
    80  
    81  	for _, d := range desc {
    82  		if _, ok := j.descs[d.Digest]; ok {
    83  			continue
    84  		}
    85  		j.descs[d.Digest] = d
    86  	}
    87  }
    88  
    89  // Remove removes a descriptor
    90  func (j *jobs) Remove(desc ocispec.Descriptor) {
    91  	j.mu.Lock()
    92  	defer j.mu.Unlock()
    93  
    94  	delete(j.descs, desc.Digest)
    95  }
    96  
    97  // Jobs returns a list of all tracked descriptors
    98  func (j *jobs) Jobs() []ocispec.Descriptor {
    99  	j.mu.Lock()
   100  	defer j.mu.Unlock()
   101  
   102  	descs := make([]ocispec.Descriptor, 0, len(j.descs))
   103  	for _, d := range j.descs {
   104  		descs = append(descs, d)
   105  	}
   106  	return descs
   107  }
   108  
   109  type pullProgress struct {
   110  	store      content.Store
   111  	showExists bool
   112  	hideLayers bool
   113  }
   114  
   115  func (p pullProgress) UpdateProgress(ctx context.Context, ongoing *jobs, out progress.Output, start time.Time) error {
   116  	actives, err := p.store.ListStatuses(ctx, "")
   117  	if err != nil {
   118  		if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
   119  			return err
   120  		}
   121  		log.G(ctx).WithError(err).Error("status check failed")
   122  		return nil
   123  	}
   124  	pulling := make(map[string]content.Status, len(actives))
   125  
   126  	// update status of status entries!
   127  	for _, status := range actives {
   128  		pulling[status.Ref] = status
   129  	}
   130  
   131  	for _, j := range ongoing.Jobs() {
   132  		if p.hideLayers {
   133  			ongoing.Remove(j)
   134  			continue
   135  		}
   136  		key := remotes.MakeRefKey(ctx, j)
   137  		if info, ok := pulling[key]; ok {
   138  			if info.Offset == 0 {
   139  				continue
   140  			}
   141  			out.WriteProgress(progress.Progress{
   142  				ID:      stringid.TruncateID(j.Digest.Encoded()),
   143  				Action:  "Downloading",
   144  				Current: info.Offset,
   145  				Total:   info.Total,
   146  			})
   147  			continue
   148  		}
   149  
   150  		info, err := p.store.Info(ctx, j.Digest)
   151  		if err != nil {
   152  			if !cerrdefs.IsNotFound(err) {
   153  				return err
   154  			}
   155  		} else if info.CreatedAt.After(start) {
   156  			out.WriteProgress(progress.Progress{
   157  				ID:         stringid.TruncateID(j.Digest.Encoded()),
   158  				Action:     "Download complete",
   159  				HideCounts: true,
   160  				LastUpdate: true,
   161  			})
   162  			ongoing.Remove(j)
   163  		} else if p.showExists {
   164  			out.WriteProgress(progress.Progress{
   165  				ID:         stringid.TruncateID(j.Digest.Encoded()),
   166  				Action:     "Already exists",
   167  				HideCounts: true,
   168  				LastUpdate: true,
   169  			})
   170  			ongoing.Remove(j)
   171  		}
   172  	}
   173  	return nil
   174  }
   175  
   176  type pushProgress struct {
   177  	Tracker docker.StatusTracker
   178  }
   179  
   180  func (p *pushProgress) UpdateProgress(ctx context.Context, ongoing *jobs, out progress.Output, start time.Time) error {
   181  	for _, j := range ongoing.Jobs() {
   182  		key := remotes.MakeRefKey(ctx, j)
   183  		id := stringid.TruncateID(j.Digest.Encoded())
   184  
   185  		status, err := p.Tracker.GetStatus(key)
   186  		if err != nil {
   187  			if cerrdefs.IsNotFound(err) {
   188  				progress.Update(out, id, "Waiting")
   189  				continue
   190  			}
   191  		}
   192  
   193  		if status.Committed && status.Offset >= status.Total {
   194  			if status.MountedFrom != "" {
   195  				from := status.MountedFrom
   196  				if ref, err := reference.ParseNormalizedNamed(from); err == nil {
   197  					from = reference.Path(ref)
   198  				}
   199  				progress.Update(out, id, "Mounted from "+from)
   200  			} else if status.Exists {
   201  				if images.IsLayerType(j.MediaType) {
   202  					progress.Update(out, id, "Layer already exists")
   203  				} else {
   204  					progress.Update(out, id, "Already exists")
   205  				}
   206  			} else {
   207  				progress.Update(out, id, "Pushed")
   208  			}
   209  			ongoing.Remove(j)
   210  			continue
   211  		}
   212  
   213  		out.WriteProgress(progress.Progress{
   214  			ID:      id,
   215  			Action:  "Pushing",
   216  			Current: status.Offset,
   217  			Total:   status.Total,
   218  		})
   219  	}
   220  
   221  	return nil
   222  }
   223  
   224  type combinedProgress []progressUpdater
   225  
   226  func (combined combinedProgress) UpdateProgress(ctx context.Context, ongoing *jobs, out progress.Output, start time.Time) error {
   227  	for _, p := range combined {
   228  		err := p.UpdateProgress(ctx, ongoing, out, start)
   229  		if err != nil {
   230  			return err
   231  		}
   232  	}
   233  	return nil
   234  }