github.com/docker/app@v0.9.1-beta3.0.20210611140623-a48f773ab002/internal/commands/push.go (about)

     1  package commands
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/docker/app/internal/image"
    12  	"github.com/docker/app/internal/store"
    13  
    14  	"github.com/containerd/containerd/platforms"
    15  	"github.com/docker/app/internal"
    16  	"github.com/docker/app/internal/log"
    17  	"github.com/docker/cli/cli"
    18  	"github.com/docker/cli/cli/command"
    19  	"github.com/docker/cnab-to-oci/remotes"
    20  	"github.com/docker/distribution/reference"
    21  	"github.com/docker/docker/pkg/term"
    22  	"github.com/morikuni/aec"
    23  	ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1"
    24  	"github.com/pkg/errors"
    25  	"github.com/sirupsen/logrus"
    26  	"github.com/spf13/cobra"
    27  )
    28  
    29  const ( // Docker specific annotations and values
    30  	// DockerAppFormatAnnotation is the top level annotation specifying the kind of the App AppImage
    31  	DockerAppFormatAnnotation = "io.docker.app.format"
    32  	// DockerAppFormatCNAB is the DockerAppFormatAnnotation value for CNAB
    33  	DockerAppFormatCNAB = "cnab"
    34  
    35  	// DockerTypeAnnotation is the annotation that designates the type of the application
    36  	DockerTypeAnnotation = "io.docker.type"
    37  	// DockerTypeApp is the value used to fill DockerTypeAnnotation when targeting a docker-app
    38  	DockerTypeApp = "app"
    39  )
    40  
    41  func pushCmd(dockerCli command.Cli) *cobra.Command {
    42  	cmd := &cobra.Command{
    43  		Use:     "push APP_IMAGE",
    44  		Short:   "Push an App image to a registry",
    45  		Example: `$ docker app push myrepo/myapp:mytag`,
    46  		Args:    cli.ExactArgs(1),
    47  		RunE: func(cmd *cobra.Command, args []string) error {
    48  			return runPush(dockerCli, args[0])
    49  		},
    50  	}
    51  	return cmd
    52  }
    53  
    54  func runPush(dockerCli command.Cli, name string) error {
    55  	defer muteDockerCli(dockerCli)()
    56  	imageStore, err := prepareImageStore()
    57  	if err != nil {
    58  		return err
    59  	}
    60  
    61  	// Get the bundle
    62  	ref, err := imageStore.LookUp(name)
    63  	if err != nil {
    64  		return errors.Wrapf(err, "could not push %q", name)
    65  	}
    66  	named, ok := ref.(reference.Named)
    67  	if !ok {
    68  		return fmt.Errorf("could not push by ID. first tag your app image")
    69  	}
    70  
    71  	bndl, err := resolveReferenceAndBundle(imageStore, named)
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	cnabRef := reference.TagNameOnly(named)
    77  
    78  	// Push the bundle
    79  	return pushBundle(dockerCli, bndl, cnabRef)
    80  }
    81  
    82  func resolveReferenceAndBundle(imageStore store.ImageStore, ref reference.Reference) (*image.AppImage, error) {
    83  	bndl, err := imageStore.Read(ref)
    84  	if err != nil {
    85  		return nil, errors.Wrapf(err, "could not push %q: no such App image", reference.FamiliarString(ref))
    86  	}
    87  
    88  	if err := bndl.Validate(); err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	return bndl, err
    93  }
    94  
    95  func pushBundle(dockerCli command.Cli, bndl *image.AppImage, cnabRef reference.Named) error {
    96  	insecureRegistries, err := internal.InsecureRegistriesFromEngine(dockerCli)
    97  	if err != nil {
    98  		return errors.Wrap(err, "could not retrieve insecure registries")
    99  	}
   100  	resolver := remotes.CreateResolver(dockerCli.ConfigFile(), insecureRegistries...)
   101  	var display fixupDisplay = &plainDisplay{out: os.Stdout}
   102  	if term.IsTerminal(os.Stdout.Fd()) {
   103  		display = &interactiveDisplay{out: os.Stdout}
   104  	}
   105  	fixupOptions := []remotes.FixupOption{
   106  		remotes.WithEventCallback(display.onEvent),
   107  		remotes.WithAutoBundleUpdate(),
   108  		remotes.WithPushImages(dockerCli.Client(), dockerCli.Out()),
   109  		remotes.WithRelocationMap(bndl.RelocationMap),
   110  	}
   111  	// bundle fixup
   112  	relocationMap, err := remotes.FixupBundle(context.Background(), bndl.Bundle, cnabRef, resolver, fixupOptions...)
   113  	if err != nil {
   114  		return errors.Wrapf(err, "fixing up %q for push", cnabRef)
   115  	}
   116  	bndl.RelocationMap = relocationMap
   117  	// push bundle manifest
   118  	logrus.Debugf("Pushing the bundle %q", cnabRef)
   119  	descriptor, err := remotes.Push(log.WithLogContext(context.Background()), bndl.Bundle, bndl.RelocationMap, cnabRef, resolver, true, withAppAnnotations)
   120  	if err != nil {
   121  		return errors.Wrapf(err, "pushing to %q", cnabRef)
   122  	}
   123  	fmt.Fprintf(os.Stdout, "Successfully pushed bundle to %s. Digest is %s.\n", cnabRef, descriptor.Digest)
   124  	return nil
   125  }
   126  
   127  func withAppAnnotations(index *ocischemav1.Index) error {
   128  	if index.Annotations == nil {
   129  		index.Annotations = make(map[string]string)
   130  	}
   131  	index.Annotations[DockerAppFormatAnnotation] = DockerAppFormatCNAB
   132  	index.Annotations[DockerTypeAnnotation] = DockerTypeApp
   133  	return nil
   134  }
   135  
   136  type fixupDisplay interface {
   137  	onEvent(remotes.FixupEvent)
   138  }
   139  
   140  type interactiveDisplay struct {
   141  	out               io.Writer
   142  	previousLineCount int
   143  	images            []interactiveImageState
   144  }
   145  
   146  func (r *interactiveDisplay) onEvent(ev remotes.FixupEvent) {
   147  	out := bytes.NewBuffer(nil)
   148  	for i := 0; i < r.previousLineCount; i++ {
   149  		fmt.Fprint(out, aec.NewBuilder(aec.Up(1), aec.EraseLine(aec.EraseModes.All)).ANSI)
   150  	}
   151  	switch ev.EventType {
   152  	case remotes.FixupEventTypeCopyImageStart:
   153  		r.images = append(r.images, interactiveImageState{name: ev.SourceImage})
   154  	case remotes.FixupEventTypeCopyImageEnd:
   155  		r.images[r.imageIndex(ev.SourceImage)].done = true
   156  	case remotes.FixupEventTypeProgress:
   157  		r.images[r.imageIndex(ev.SourceImage)].onProgress(ev.Progress)
   158  	}
   159  	r.previousLineCount = 0
   160  	for _, s := range r.images {
   161  		r.previousLineCount += s.print(out)
   162  	}
   163  	r.out.Write(out.Bytes()) //nolint:errcheck // nothing much we can do with an error to write to output.
   164  }
   165  
   166  func (r *interactiveDisplay) imageIndex(name string) int {
   167  	for ix, state := range r.images {
   168  		if state.name == name {
   169  			return ix
   170  		}
   171  	}
   172  	return 0
   173  }
   174  
   175  type interactiveImageState struct {
   176  	name     string
   177  	progress remotes.ProgressSnapshot
   178  	done     bool
   179  }
   180  
   181  func (s *interactiveImageState) onProgress(p remotes.ProgressSnapshot) {
   182  	s.progress = p
   183  }
   184  
   185  func (s *interactiveImageState) print(out io.Writer) int {
   186  	if s.done {
   187  		fmt.Fprint(out, aec.Apply(s.name, aec.BlueF))
   188  	} else {
   189  		fmt.Fprint(out, s.name)
   190  	}
   191  	fmt.Fprint(out, "\n")
   192  	lineCount := 1
   193  
   194  	for _, p := range s.progress.Roots {
   195  		lineCount += printDescriptorProgress(out, &p, 1)
   196  	}
   197  	return lineCount
   198  }
   199  
   200  func printDescriptorProgress(out io.Writer, p *remotes.DescriptorProgressSnapshot, depth int) int {
   201  	fmt.Fprint(out, strings.Repeat(" ", depth))
   202  	name := p.MediaType
   203  	if p.Platform != nil {
   204  		name = platforms.Format(*p.Platform)
   205  	}
   206  	if len(p.Children) == 0 {
   207  		name = fmt.Sprintf("%s...: %s", p.Digest.String()[:15], p.Action)
   208  	}
   209  	doneCount := 0
   210  	for _, c := range p.Children {
   211  		if c.Done {
   212  			doneCount++
   213  		}
   214  	}
   215  	display := name
   216  	if len(p.Children) > 0 {
   217  		display = fmt.Sprintf("%s [%d/%d] (%s...)", name, doneCount, len(p.Children), p.Digest.String()[:15])
   218  	}
   219  	if p.Done {
   220  		display = aec.Apply(display, aec.BlueF)
   221  	}
   222  	if hasError(p) {
   223  		display = aec.Apply(display, aec.RedF)
   224  	}
   225  	fmt.Fprintln(out, display)
   226  	lineCount := 1
   227  	if p.Done {
   228  		return lineCount
   229  	}
   230  	for _, c := range p.Children {
   231  		lineCount += printDescriptorProgress(out, &c, depth+1)
   232  	}
   233  	return lineCount
   234  }
   235  
   236  func hasError(p *remotes.DescriptorProgressSnapshot) bool {
   237  	if p.Error != nil {
   238  		return true
   239  	}
   240  	for _, c := range p.Children {
   241  		if hasError(&c) {
   242  			return true
   243  		}
   244  	}
   245  	return false
   246  }
   247  
   248  type plainDisplay struct {
   249  	out io.Writer
   250  }
   251  
   252  func (r *plainDisplay) onEvent(ev remotes.FixupEvent) {
   253  	switch ev.EventType {
   254  	case remotes.FixupEventTypeCopyImageStart:
   255  		fmt.Fprintf(r.out, "Handling image %s...", ev.SourceImage)
   256  	case remotes.FixupEventTypeCopyImageEnd:
   257  		if ev.Error != nil {
   258  			fmt.Fprintf(r.out, "\nFailure: %s\n", ev.Error)
   259  		} else {
   260  			fmt.Fprint(r.out, " done!\n")
   261  		}
   262  	}
   263  }