github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/imgutil/push/push.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  // Package push derived from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/images/push.go
    18  package push
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"io"
    24  	"sync"
    25  	"text/tabwriter"
    26  	"time"
    27  
    28  	"github.com/containerd/containerd"
    29  	"github.com/containerd/containerd/images"
    30  	"github.com/containerd/containerd/pkg/progress"
    31  	"github.com/containerd/containerd/remotes"
    32  	"github.com/containerd/containerd/remotes/docker"
    33  	"github.com/containerd/log"
    34  	"github.com/containerd/nerdctl/v2/pkg/imgutil/jobs"
    35  	"github.com/containerd/platforms"
    36  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    37  
    38  	"golang.org/x/sync/errgroup"
    39  )
    40  
    41  // Push pushes an image to a remote registry.
    42  func Push(ctx context.Context, client *containerd.Client, resolver remotes.Resolver, pushTracker docker.StatusTracker, stdout io.Writer,
    43  	localRef, remoteRef string, platform platforms.MatchComparer, allowNonDist, quiet bool) error {
    44  	img, err := client.ImageService().Get(ctx, localRef)
    45  	if err != nil {
    46  		return fmt.Errorf("unable to resolve image to manifest: %w", err)
    47  	}
    48  	desc := img.Target
    49  
    50  	ongoing := newPushJobs(pushTracker)
    51  
    52  	eg, ctx := errgroup.WithContext(ctx)
    53  
    54  	// used to notify the progress writer
    55  	doneCh := make(chan struct{})
    56  
    57  	eg.Go(func() error {
    58  		defer close(doneCh)
    59  
    60  		log.G(ctx).WithField("image", remoteRef).WithField("digest", desc.Digest).Debug("pushing")
    61  
    62  		jobHandler := images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
    63  			if allowNonDist || !images.IsNonDistributable(desc.MediaType) {
    64  				ongoing.add(remotes.MakeRefKey(ctx, desc))
    65  			}
    66  			return nil, nil
    67  		})
    68  
    69  		if !allowNonDist {
    70  			jobHandler = remotes.SkipNonDistributableBlobs(jobHandler)
    71  		}
    72  
    73  		return client.Push(ctx, remoteRef, desc,
    74  			containerd.WithResolver(resolver),
    75  			containerd.WithImageHandler(jobHandler),
    76  			containerd.WithPlatformMatcher(platform),
    77  		)
    78  	})
    79  
    80  	if !quiet {
    81  		eg.Go(func() error {
    82  			var (
    83  				ticker = time.NewTicker(100 * time.Millisecond)
    84  				fw     = progress.NewWriter(stdout)
    85  				start  = time.Now()
    86  				done   bool
    87  			)
    88  
    89  			defer ticker.Stop()
    90  
    91  			for {
    92  				select {
    93  				case <-ticker.C:
    94  					fw.Flush()
    95  
    96  					tw := tabwriter.NewWriter(fw, 1, 8, 1, ' ', 0)
    97  
    98  					jobs.Display(tw, ongoing.status(), start)
    99  					tw.Flush()
   100  
   101  					if done {
   102  						fw.Flush()
   103  						return nil
   104  					}
   105  				case <-doneCh:
   106  					done = true
   107  				case <-ctx.Done():
   108  					done = true // allow ui to update once more
   109  				}
   110  			}
   111  		})
   112  	}
   113  	return eg.Wait()
   114  }
   115  
   116  type pushjobs struct {
   117  	jobs    map[string]struct{}
   118  	ordered []string
   119  	tracker docker.StatusTracker
   120  	mu      sync.Mutex
   121  }
   122  
   123  func newPushJobs(tracker docker.StatusTracker) *pushjobs {
   124  	return &pushjobs{
   125  		jobs:    make(map[string]struct{}),
   126  		tracker: tracker,
   127  	}
   128  }
   129  
   130  func (j *pushjobs) add(ref string) {
   131  	j.mu.Lock()
   132  	defer j.mu.Unlock()
   133  
   134  	if _, ok := j.jobs[ref]; ok {
   135  		return
   136  	}
   137  	j.ordered = append(j.ordered, ref)
   138  	j.jobs[ref] = struct{}{}
   139  }
   140  
   141  func (j *pushjobs) status() []jobs.StatusInfo {
   142  	j.mu.Lock()
   143  	defer j.mu.Unlock()
   144  
   145  	statuses := make([]jobs.StatusInfo, 0, len(j.jobs))
   146  	for _, name := range j.ordered {
   147  		si := jobs.StatusInfo{
   148  			Ref: name,
   149  		}
   150  
   151  		status, err := j.tracker.GetStatus(name)
   152  		if err != nil {
   153  			si.Status = "waiting"
   154  		} else {
   155  			si.Offset = status.Offset
   156  			si.Total = status.Total
   157  			si.StartedAt = status.StartedAt
   158  			si.UpdatedAt = status.UpdatedAt
   159  			if status.Offset >= status.Total {
   160  				if status.UploadUUID == "" {
   161  					si.Status = "done"
   162  				} else {
   163  					si.Status = "committing"
   164  				}
   165  			} else {
   166  				si.Status = "uploading"
   167  			}
   168  		}
   169  		statuses = append(statuses, si)
   170  	}
   171  
   172  	return statuses
   173  }