github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/cmd/ctr/commands/images/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 images
    18  
    19  import (
    20  	gocontext "context"
    21  	"os"
    22  	"sync"
    23  	"text/tabwriter"
    24  	"time"
    25  
    26  	"github.com/containerd/containerd"
    27  	"github.com/containerd/containerd/cmd/ctr/commands"
    28  	"github.com/containerd/containerd/cmd/ctr/commands/content"
    29  	"github.com/containerd/containerd/images"
    30  	"github.com/containerd/containerd/log"
    31  	"github.com/containerd/containerd/pkg/progress"
    32  	"github.com/containerd/containerd/platforms"
    33  	"github.com/containerd/containerd/remotes"
    34  	"github.com/containerd/containerd/remotes/docker"
    35  	digest "github.com/opencontainers/go-digest"
    36  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    37  	"github.com/pkg/errors"
    38  	"github.com/urfave/cli"
    39  	"golang.org/x/sync/errgroup"
    40  )
    41  
    42  var pushCommand = cli.Command{
    43  	Name:      "push",
    44  	Usage:     "push an image to a remote",
    45  	ArgsUsage: "[flags] <remote> [<local>]",
    46  	Description: `Pushes an image reference from containerd.
    47  
    48  	All resources associated with the manifest reference will be pushed.
    49  	The ref is used to resolve to a locally existing image manifest.
    50  	The image manifest must exist before push. Creating a new image
    51  	manifest can be done through calculating the diff for layers,
    52  	creating the associated configuration, and creating the manifest
    53  	which references those resources.
    54  `,
    55  	Flags: append(commands.RegistryFlags, cli.StringFlag{
    56  		Name:  "manifest",
    57  		Usage: "digest of manifest",
    58  	}, cli.StringFlag{
    59  		Name:  "manifest-type",
    60  		Usage: "media type of manifest digest",
    61  		Value: ocispec.MediaTypeImageManifest,
    62  	}, cli.StringSliceFlag{
    63  		Name:  "platform",
    64  		Usage: "push content from a specific platform",
    65  		Value: &cli.StringSlice{},
    66  	}),
    67  	Action: func(context *cli.Context) error {
    68  		var (
    69  			ref   = context.Args().First()
    70  			local = context.Args().Get(1)
    71  			debug = context.GlobalBool("debug")
    72  			desc  ocispec.Descriptor
    73  		)
    74  		if ref == "" {
    75  			return errors.New("please provide a remote image reference to push")
    76  		}
    77  
    78  		client, ctx, cancel, err := commands.NewClient(context)
    79  		if err != nil {
    80  			return err
    81  		}
    82  		defer cancel()
    83  
    84  		if manifest := context.String("manifest"); manifest != "" {
    85  			desc.Digest, err = digest.Parse(manifest)
    86  			if err != nil {
    87  				return errors.Wrap(err, "invalid manifest digest")
    88  			}
    89  			desc.MediaType = context.String("manifest-type")
    90  		} else {
    91  			if local == "" {
    92  				local = ref
    93  			}
    94  			img, err := client.ImageService().Get(ctx, local)
    95  			if err != nil {
    96  				return errors.Wrap(err, "unable to resolve image to manifest")
    97  			}
    98  			desc = img.Target
    99  
   100  			if pss := context.StringSlice("platform"); len(pss) == 1 {
   101  				p, err := platforms.Parse(pss[0])
   102  				if err != nil {
   103  					return errors.Wrapf(err, "invalid platform %q", pss[0])
   104  				}
   105  
   106  				cs := client.ContentStore()
   107  				if manifests, err := images.Children(ctx, cs, desc); err == nil && len(manifests) > 0 {
   108  					matcher := platforms.NewMatcher(p)
   109  					for _, manifest := range manifests {
   110  						if manifest.Platform != nil && matcher.Match(*manifest.Platform) {
   111  							if _, err := images.Children(ctx, cs, manifest); err != nil {
   112  								return errors.Wrap(err, "no matching manifest")
   113  							}
   114  							desc = manifest
   115  							break
   116  						}
   117  					}
   118  				}
   119  			}
   120  		}
   121  
   122  		resolver, err := commands.GetResolver(ctx, context)
   123  		if err != nil {
   124  			return err
   125  		}
   126  		ongoing := newPushJobs(commands.PushTracker)
   127  
   128  		eg, ctx := errgroup.WithContext(ctx)
   129  
   130  		// used to notify the progress writer
   131  		doneCh := make(chan struct{})
   132  
   133  		eg.Go(func() error {
   134  			defer close(doneCh)
   135  
   136  			log.G(ctx).WithField("image", ref).WithField("digest", desc.Digest).Debug("pushing")
   137  
   138  			jobHandler := images.HandlerFunc(func(ctx gocontext.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   139  				ongoing.add(remotes.MakeRefKey(ctx, desc))
   140  				return nil, nil
   141  			})
   142  
   143  			return client.Push(ctx, ref, desc,
   144  				containerd.WithResolver(resolver),
   145  				containerd.WithImageHandler(jobHandler),
   146  			)
   147  		})
   148  
   149  		// don't show progress if debug mode is set
   150  		if !debug {
   151  			eg.Go(func() error {
   152  				var (
   153  					ticker = time.NewTicker(100 * time.Millisecond)
   154  					fw     = progress.NewWriter(os.Stdout)
   155  					start  = time.Now()
   156  					done   bool
   157  				)
   158  
   159  				defer ticker.Stop()
   160  
   161  				for {
   162  					select {
   163  					case <-ticker.C:
   164  						fw.Flush()
   165  
   166  						tw := tabwriter.NewWriter(fw, 1, 8, 1, ' ', 0)
   167  
   168  						content.Display(tw, ongoing.status(), start)
   169  						tw.Flush()
   170  
   171  						if done {
   172  							fw.Flush()
   173  							return nil
   174  						}
   175  					case <-doneCh:
   176  						done = true
   177  					case <-ctx.Done():
   178  						done = true // allow ui to update once more
   179  					}
   180  				}
   181  			})
   182  		}
   183  		return eg.Wait()
   184  	},
   185  }
   186  
   187  type pushjobs struct {
   188  	jobs    map[string]struct{}
   189  	ordered []string
   190  	tracker docker.StatusTracker
   191  	mu      sync.Mutex
   192  }
   193  
   194  func newPushJobs(tracker docker.StatusTracker) *pushjobs {
   195  	return &pushjobs{
   196  		jobs:    make(map[string]struct{}),
   197  		tracker: tracker,
   198  	}
   199  }
   200  
   201  func (j *pushjobs) add(ref string) {
   202  	j.mu.Lock()
   203  	defer j.mu.Unlock()
   204  
   205  	if _, ok := j.jobs[ref]; ok {
   206  		return
   207  	}
   208  	j.ordered = append(j.ordered, ref)
   209  	j.jobs[ref] = struct{}{}
   210  }
   211  
   212  func (j *pushjobs) status() []content.StatusInfo {
   213  	j.mu.Lock()
   214  	defer j.mu.Unlock()
   215  
   216  	statuses := make([]content.StatusInfo, 0, len(j.jobs))
   217  	for _, name := range j.ordered {
   218  		si := content.StatusInfo{
   219  			Ref: name,
   220  		}
   221  
   222  		status, err := j.tracker.GetStatus(name)
   223  		if err != nil {
   224  			si.Status = "waiting"
   225  		} else {
   226  			si.Offset = status.Offset
   227  			si.Total = status.Total
   228  			si.StartedAt = status.StartedAt
   229  			si.UpdatedAt = status.UpdatedAt
   230  			if status.Offset >= status.Total {
   231  				if status.UploadUUID == "" {
   232  					si.Status = "done"
   233  				} else {
   234  					si.Status = "committing"
   235  				}
   236  			} else {
   237  				si.Status = "uploading"
   238  			}
   239  		}
   240  		statuses = append(statuses, si)
   241  	}
   242  
   243  	return statuses
   244  }