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