github.com/replicatedhq/ship@v0.55.0/pkg/images/save.go (about)

     1  package images
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/url"
    10  	"os"
    11  	"path"
    12  
    13  	"github.com/docker/docker/api/types"
    14  	docker "github.com/docker/docker/client"
    15  	"github.com/go-kit/kit/log"
    16  	"github.com/go-kit/kit/log/level"
    17  	"github.com/pkg/errors"
    18  )
    19  
    20  // ImageSaver saves an image
    21  type ImageSaver interface {
    22  	SaveImage(ctx context.Context, opts SaveOpts) chan interface{}
    23  }
    24  
    25  var _ ImageManager = &docker.Client{}
    26  
    27  // ImageManager represents a subset of the docker client interface
    28  type ImageManager interface {
    29  	ImagePull(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error)
    30  	ImageTag(ctx context.Context, source, target string) error
    31  	ImageSave(ctx context.Context, imageIDs []string) (io.ReadCloser, error)
    32  	ImagePush(ctx context.Context, image string, options types.ImagePushOptions) (io.ReadCloser, error)
    33  }
    34  
    35  type SaveOpts struct {
    36  	PullURL        string
    37  	SaveURL        string
    38  	IsPrivate      bool
    39  	Filename       string
    40  	DestinationURL *url.URL
    41  	Username       string
    42  	Password       string
    43  }
    44  
    45  type DestinationParams struct {
    46  	AuthConfig           types.AuthConfig
    47  	DestinationImageName string
    48  }
    49  
    50  var _ ImageSaver = &CLISaver{}
    51  
    52  // CLISaver implements ImageSaver via a docker client
    53  type CLISaver struct {
    54  	Logger log.Logger
    55  	client ImageManager
    56  }
    57  
    58  func NewImageSaver(logger log.Logger, client *docker.Client) ImageSaver {
    59  	return &CLISaver{
    60  		Logger: logger,
    61  		client: client,
    62  	}
    63  }
    64  func (s *CLISaver) SaveImage(ctx context.Context, saveOpts SaveOpts) chan interface{} {
    65  	ch := make(chan interface{})
    66  	go func() {
    67  		defer close(ch)
    68  		if err := s.saveImage(ctx, saveOpts, ch); err != nil {
    69  			ch <- err
    70  		}
    71  	}()
    72  	return ch
    73  }
    74  
    75  func (s *CLISaver) saveImage(ctx context.Context, saveOpts SaveOpts, progressCh chan interface{}) error {
    76  	debug := level.Debug(log.With(s.Logger, "method", "saveImage", "image", saveOpts.SaveURL))
    77  
    78  	authOpts := types.AuthConfig{}
    79  	if saveOpts.IsPrivate {
    80  		authOpts.Username = saveOpts.Username
    81  		authOpts.Password = saveOpts.Password
    82  	}
    83  
    84  	debug.Log("stage", "make.auth")
    85  
    86  	authString, err := makeAuthValue(authOpts)
    87  	if err != nil {
    88  		return errors.Wrapf(err, "make auth string")
    89  	}
    90  
    91  	debug.Log("stage", "pull")
    92  
    93  	pullOpts := types.ImagePullOptions{
    94  		RegistryAuth: authString,
    95  	}
    96  	progressReader, err := s.client.ImagePull(ctx, saveOpts.PullURL, pullOpts)
    97  	if err != nil {
    98  		return errors.Wrapf(err, "pull image %s", saveOpts.PullURL)
    99  	}
   100  	err = copyDockerProgress(debug, saveOpts.PullURL, progressReader, progressCh)
   101  	if err != nil {
   102  		return errors.Wrapf(err, "copy docker progress pulling image %s", saveOpts.PullURL)
   103  	}
   104  	if saveOpts.Filename != "" {
   105  		return s.createFile(ctx, progressCh, saveOpts)
   106  	} else if saveOpts.DestinationURL != nil {
   107  		return s.pushImage(ctx, progressCh, saveOpts)
   108  	} else {
   109  		return errors.New("Destination improperly set")
   110  	}
   111  }
   112  
   113  func (s *CLISaver) createFile(ctx context.Context, progressCh chan interface{}, saveOpts SaveOpts) error {
   114  	debug := level.Debug(log.With(s.Logger, "method", "createFile", "image", saveOpts.SaveURL))
   115  
   116  	if saveOpts.PullURL != saveOpts.SaveURL {
   117  		debug.Log("stage", "tag", "old.tag", saveOpts.PullURL, "new.tag", saveOpts.SaveURL)
   118  		err := s.client.ImageTag(ctx, saveOpts.PullURL, saveOpts.SaveURL)
   119  		if err != nil {
   120  			return errors.Wrapf(err, "tag image %s -> %s", saveOpts.PullURL, saveOpts.SaveURL)
   121  		}
   122  	}
   123  
   124  	debug.Log("stage", "file.create")
   125  
   126  	outFile, err := os.Create(saveOpts.Filename)
   127  	if err != nil {
   128  		return errors.Wrapf(err, "open file %s", saveOpts.Filename)
   129  	}
   130  	defer outFile.Close()
   131  
   132  	debug.Log("stage", "save")
   133  
   134  	progressCh <- Progress{
   135  		ID:     saveOpts.SaveURL,
   136  		Status: fmt.Sprintf("Saving %s", saveOpts.SaveURL),
   137  	}
   138  
   139  	imageReader, err := s.client.ImageSave(ctx, []string{saveOpts.SaveURL})
   140  	if err != nil {
   141  		return errors.Wrapf(err, "save image %s", saveOpts.SaveURL)
   142  	}
   143  	defer imageReader.Close()
   144  
   145  	_, err = io.Copy(outFile, imageReader)
   146  	if err != nil {
   147  		return errors.Wrapf(err, "copy image to file")
   148  	}
   149  
   150  	debug.Log("stage", "done")
   151  
   152  	return nil
   153  }
   154  
   155  func (s *CLISaver) pushImage(ctx context.Context, progressCh chan interface{}, saveOpts SaveOpts) error {
   156  	debug := level.Debug(log.With(s.Logger, "method", "pushImage", "image", saveOpts.SaveURL))
   157  
   158  	debug.Log("stage", "make.push.auth")
   159  	destinationParams, err := buildDestinationParams(saveOpts.DestinationURL)
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	if saveOpts.PullURL != destinationParams.DestinationImageName {
   165  		debug.Log("stage", "tag", "old.tag", saveOpts.PullURL, "new.tag", destinationParams.DestinationImageName)
   166  		err := s.client.ImageTag(ctx, saveOpts.PullURL, destinationParams.DestinationImageName)
   167  		if err != nil {
   168  			return errors.Wrapf(err, "tag image %s -> %s", saveOpts.PullURL, destinationParams.DestinationImageName)
   169  		}
   170  	}
   171  
   172  	debug.Log("stage", "make.push.auth")
   173  	registryAuth, err := makeAuthValue(destinationParams.AuthConfig)
   174  	if err != nil {
   175  		return errors.Wrapf(err, "make destination auth string")
   176  	}
   177  
   178  	debug.Log("stage", "push")
   179  	pushOpts := types.ImagePushOptions{
   180  		RegistryAuth: registryAuth,
   181  	}
   182  	progressReader, err := s.client.ImagePush(ctx, destinationParams.DestinationImageName, pushOpts)
   183  	if err != nil {
   184  		return errors.Wrapf(err, "push image %s", destinationParams.DestinationImageName)
   185  	}
   186  	return copyDockerProgress(debug, destinationParams.DestinationImageName, progressReader, progressCh)
   187  }
   188  
   189  func buildDestinationParams(destinationURL *url.URL) (DestinationParams, error) {
   190  	authOpts := types.AuthConfig{}
   191  	if destinationURL.User != nil {
   192  		authOpts.Username = destinationURL.User.Username()
   193  		authOpts.Password, _ = destinationURL.User.Password()
   194  	}
   195  
   196  	destinationParams := DestinationParams{
   197  		AuthConfig:           authOpts,
   198  		DestinationImageName: path.Join(destinationURL.Host, destinationURL.Path),
   199  	}
   200  	return destinationParams, nil
   201  }
   202  
   203  func makeAuthValue(authOpts types.AuthConfig) (string, error) {
   204  	jsonified, err := json.Marshal(authOpts)
   205  	if err != nil {
   206  		return "", err
   207  	}
   208  
   209  	return base64.StdEncoding.EncodeToString(jsonified), nil
   210  }