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 }