Update push client to use status tracker Signed-off-by: Derek McGowan <derek@mcgstyle.net>
		
			
				
	
	
		
			207 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			207 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"os"
 | 
						|
	"sync"
 | 
						|
	"text/tabwriter"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/containerd/containerd"
 | 
						|
	"github.com/containerd/containerd/images"
 | 
						|
	"github.com/containerd/containerd/log"
 | 
						|
	"github.com/containerd/containerd/progress"
 | 
						|
	"github.com/containerd/containerd/remotes"
 | 
						|
	"github.com/containerd/containerd/remotes/docker"
 | 
						|
	digest "github.com/opencontainers/go-digest"
 | 
						|
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
 | 
						|
	"github.com/pkg/errors"
 | 
						|
	"github.com/urfave/cli"
 | 
						|
	"golang.org/x/sync/errgroup"
 | 
						|
)
 | 
						|
 | 
						|
var (
 | 
						|
	pushTracker = docker.NewInMemoryTracker()
 | 
						|
)
 | 
						|
 | 
						|
var pushCommand = cli.Command{
 | 
						|
	Name:      "push",
 | 
						|
	Usage:     "push an image to a remote",
 | 
						|
	ArgsUsage: "[flags] <remote> [<local>]",
 | 
						|
	Description: `Pushes an image reference from containerd.
 | 
						|
 | 
						|
	All resources associated with the manifest reference will be pushed.
 | 
						|
	The ref is used to resolve to a locally existing image manifest.
 | 
						|
	The image manifest must exist before push. Creating a new image
 | 
						|
	manifest can be done through calculating the diff for layers,
 | 
						|
	creating the associated configuration, and creating the manifest
 | 
						|
	which references those resources.
 | 
						|
`,
 | 
						|
	Flags: append(registryFlags, cli.StringFlag{
 | 
						|
		Name:  "manifest",
 | 
						|
		Usage: "Digest of manifest",
 | 
						|
	}, cli.StringFlag{
 | 
						|
		Name:  "manifest-type",
 | 
						|
		Usage: "Media type of manifest digest",
 | 
						|
		Value: ocispec.MediaTypeImageManifest,
 | 
						|
	}),
 | 
						|
	Action: func(clicontext *cli.Context) error {
 | 
						|
		var (
 | 
						|
			ref   = clicontext.Args().First()
 | 
						|
			local = clicontext.Args().Get(1)
 | 
						|
			desc  ocispec.Descriptor
 | 
						|
		)
 | 
						|
 | 
						|
		ctx, cancel := appContext(clicontext)
 | 
						|
		defer cancel()
 | 
						|
 | 
						|
		client, err := getClient(clicontext)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		if manifest := clicontext.String("manifest"); manifest != "" {
 | 
						|
			desc.Digest, err = digest.Parse(manifest)
 | 
						|
			if err != nil {
 | 
						|
				return errors.Wrap(err, "invalid manifest digest")
 | 
						|
			}
 | 
						|
			desc.MediaType = clicontext.String("manifest-type")
 | 
						|
		} else {
 | 
						|
			if local == "" {
 | 
						|
				local = ref
 | 
						|
			}
 | 
						|
			img, err := client.ImageService().Get(ctx, local)
 | 
						|
			if err != nil {
 | 
						|
				return errors.Wrap(err, "unable to resolve image to manifest")
 | 
						|
			}
 | 
						|
			desc = img.Target
 | 
						|
		}
 | 
						|
 | 
						|
		resolver, err := getResolver(ctx, clicontext)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		ongoing := newPushJobs(pushTracker)
 | 
						|
 | 
						|
		eg, ctx := errgroup.WithContext(ctx)
 | 
						|
 | 
						|
		eg.Go(func() error {
 | 
						|
			log.G(ctx).WithField("image", ref).WithField("digest", desc.Digest).Debug("pushing")
 | 
						|
 | 
						|
			jobHandler := images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
 | 
						|
				ongoing.add(remotes.MakeRefKey(ctx, desc))
 | 
						|
				return nil, nil
 | 
						|
			})
 | 
						|
 | 
						|
			return client.Push(ctx, ref, desc,
 | 
						|
				containerd.WithResolver(resolver),
 | 
						|
				containerd.WithImageHandler(jobHandler),
 | 
						|
			)
 | 
						|
		})
 | 
						|
 | 
						|
		errs := make(chan error)
 | 
						|
		go func() {
 | 
						|
			defer close(errs)
 | 
						|
			errs <- eg.Wait()
 | 
						|
		}()
 | 
						|
 | 
						|
		var (
 | 
						|
			ticker = time.NewTicker(100 * time.Millisecond)
 | 
						|
			fw     = progress.NewWriter(os.Stdout)
 | 
						|
			start  = time.Now()
 | 
						|
			done   bool
 | 
						|
		)
 | 
						|
		defer ticker.Stop()
 | 
						|
 | 
						|
		for {
 | 
						|
			select {
 | 
						|
			case <-ticker.C:
 | 
						|
				fw.Flush()
 | 
						|
 | 
						|
				tw := tabwriter.NewWriter(fw, 1, 8, 1, ' ', 0)
 | 
						|
 | 
						|
				display(tw, ongoing.status(), start)
 | 
						|
				tw.Flush()
 | 
						|
 | 
						|
				if done {
 | 
						|
					fw.Flush()
 | 
						|
					return nil
 | 
						|
				}
 | 
						|
			case err := <-errs:
 | 
						|
				if err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
				done = true
 | 
						|
			case <-ctx.Done():
 | 
						|
				done = true // allow ui to update once more
 | 
						|
			}
 | 
						|
		}
 | 
						|
	},
 | 
						|
}
 | 
						|
 | 
						|
type pushStatus struct {
 | 
						|
	name    string
 | 
						|
	started bool
 | 
						|
	written int64
 | 
						|
	total   int64
 | 
						|
}
 | 
						|
 | 
						|
type pushjobs struct {
 | 
						|
	jobs    map[string]struct{}
 | 
						|
	ordered []string
 | 
						|
	tracker docker.StatusTracker
 | 
						|
	mu      sync.Mutex
 | 
						|
}
 | 
						|
 | 
						|
func newPushJobs(tracker docker.StatusTracker) *pushjobs {
 | 
						|
	return &pushjobs{
 | 
						|
		jobs:    make(map[string]struct{}),
 | 
						|
		tracker: tracker,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (j *pushjobs) add(ref string) {
 | 
						|
	j.mu.Lock()
 | 
						|
	defer j.mu.Unlock()
 | 
						|
 | 
						|
	if _, ok := j.jobs[ref]; ok {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	j.ordered = append(j.ordered, ref)
 | 
						|
	j.jobs[ref] = struct{}{}
 | 
						|
}
 | 
						|
 | 
						|
func (j *pushjobs) status() []statusInfo {
 | 
						|
	j.mu.Lock()
 | 
						|
	defer j.mu.Unlock()
 | 
						|
 | 
						|
	statuses := make([]statusInfo, 0, len(j.jobs))
 | 
						|
	for _, name := range j.ordered {
 | 
						|
		si := statusInfo{
 | 
						|
			Ref: name,
 | 
						|
		}
 | 
						|
 | 
						|
		status, err := j.tracker.GetStatus(name)
 | 
						|
		if err != nil {
 | 
						|
			si.Status = "waiting"
 | 
						|
		} else {
 | 
						|
			si.Offset = status.Offset
 | 
						|
			si.Total = status.Total
 | 
						|
			si.StartedAt = status.StartedAt
 | 
						|
			si.UpdatedAt = status.UpdatedAt
 | 
						|
			if status.Offset >= status.Total {
 | 
						|
				if status.UploadUUID == "" {
 | 
						|
					si.Status = "done"
 | 
						|
				} else {
 | 
						|
					si.Status = "committing"
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				si.Status = "uploading"
 | 
						|
			}
 | 
						|
		}
 | 
						|
		statuses = append(statuses, si)
 | 
						|
	}
 | 
						|
 | 
						|
	return statuses
 | 
						|
}
 |