|
1 | 1 | package client
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "encoding/json" |
4 | 5 | "errors"
|
| 6 | + "fmt" |
5 | 7 | "io"
|
| 8 | + "io/ioutil" |
| 9 | + "os" |
| 10 | + "path/filepath" |
| 11 | + "strings" |
6 | 12 |
|
7 |
| - "golang.org/x/net/context" |
8 |
| - |
| 13 | + "github.com/docker/engine-api/types" |
9 | 14 | Cli "github.com/hyperhq/hypercli/cli"
|
| 15 | + "github.com/hyperhq/hypercli/image" |
| 16 | + "github.com/hyperhq/hypercli/pkg/archive" |
10 | 17 | "github.com/hyperhq/hypercli/pkg/jsonmessage"
|
11 | 18 | flag "github.com/hyperhq/hypercli/pkg/mflag"
|
| 19 | + "github.com/hyperhq/hypercli/pkg/progress" |
| 20 | + "github.com/hyperhq/hypercli/pkg/streamformatter" |
| 21 | + "github.com/hyperhq/hypercli/pkg/symlink" |
| 22 | + "golang.org/x/net/context" |
12 | 23 | )
|
13 | 24 |
|
14 |
| -// CmdLoad loads an image from a tar archive. |
| 25 | +const ( |
| 26 | + unsupported = "Unsupported image version" |
| 27 | +) |
| 28 | + |
| 29 | +type manifestItem struct { |
| 30 | + Config string |
| 31 | + RepoTags []string |
| 32 | + Layers []string |
| 33 | +} |
| 34 | + |
| 35 | +type readCloser struct { |
| 36 | + io.Reader |
| 37 | + NeedClose io.ReadCloser |
| 38 | +} |
| 39 | + |
| 40 | +func (rc readCloser) Close() error { |
| 41 | + return rc.NeedClose.Close() |
| 42 | +} |
| 43 | + |
| 44 | +func safePath(base, path string) (string, error) { |
| 45 | + return symlink.FollowSymlinkInScope(filepath.Join(base, path), base) |
| 46 | +} |
| 47 | + |
| 48 | +func removeExistLayers(tmpDir string, existLayers []string, layerPaths []string) error { |
| 49 | + keepLayerPaths := make(map[string]bool) |
| 50 | + for _, _layerPath := range layerPaths[len(existLayers):] { |
| 51 | + layerPath := filepath.Join(tmpDir, _layerPath) |
| 52 | + info, err := os.Lstat(layerPath) |
| 53 | + if err != nil { |
| 54 | + return err |
| 55 | + } |
| 56 | + if info.Mode()&os.ModeSymlink == os.ModeSymlink { |
| 57 | + if _realPath, err := filepath.EvalSymlinks(layerPath); err == nil { |
| 58 | + realPath := filepath.Join(filepath.Base(filepath.Dir(_realPath)), "layer.tar") |
| 59 | + keepLayerPaths[realPath] = true |
| 60 | + } |
| 61 | + } |
| 62 | + } |
| 63 | + for idx := range existLayers { |
| 64 | + layerPath := layerPaths[idx] |
| 65 | + if _, ok := keepLayerPaths[layerPath]; !ok { |
| 66 | + if err := os.Remove(filepath.Join(tmpDir, layerPath)); err != nil { |
| 67 | + continue |
| 68 | + } |
| 69 | + } |
| 70 | + } |
| 71 | + return nil |
| 72 | +} |
| 73 | + |
| 74 | +func (cli *DockerCli) getExistLayers(ctx context.Context, tmpDir string) ([]string, []string, error) { |
| 75 | + manifestPath, err := safePath(tmpDir, "manifest.json") |
| 76 | + if err != nil { |
| 77 | + return nil, nil, err |
| 78 | + } |
| 79 | + |
| 80 | + manifestFile, err := os.Open(manifestPath) |
| 81 | + if err != nil { |
| 82 | + return nil, nil, err |
| 83 | + } |
| 84 | + defer manifestFile.Close() |
| 85 | + |
| 86 | + var manifest []manifestItem |
| 87 | + if err := json.NewDecoder(manifestFile).Decode(&manifest); err != nil { |
| 88 | + return nil, nil, err |
| 89 | + } |
| 90 | + |
| 91 | + allLayers := make([][]string, 0) |
| 92 | + repoTags := make([][]string, 0) |
| 93 | + layerPaths := make([]string, 0) |
| 94 | + for _, m := range manifest { |
| 95 | + configPath, err := safePath(tmpDir, m.Config) |
| 96 | + if err != nil { |
| 97 | + return nil, nil, err |
| 98 | + } |
| 99 | + config, err := ioutil.ReadFile(configPath) |
| 100 | + if err != nil { |
| 101 | + return nil, nil, err |
| 102 | + } |
| 103 | + img, err := image.NewFromJSON(config) |
| 104 | + if err != nil { |
| 105 | + return nil, nil, err |
| 106 | + } |
| 107 | + |
| 108 | + if expected, actual := len(m.Layers), len(img.RootFS.DiffIDs); expected != actual { |
| 109 | + return nil, nil, errors.New(unsupported) |
| 110 | + } |
| 111 | + |
| 112 | + layerPaths = append(layerPaths, m.Layers...) |
| 113 | + |
| 114 | + layers := make([]string, 0) |
| 115 | + |
| 116 | + for _, diffID := range img.RootFS.DiffIDs { |
| 117 | + layers = append(layers, string(diffID)) |
| 118 | + } |
| 119 | + |
| 120 | + allLayers = append(allLayers, layers) |
| 121 | + repoTags = append(repoTags, m.RepoTags) |
| 122 | + } |
| 123 | + diffRet, err := cli.client.ImageDiff(ctx, allLayers, repoTags) |
| 124 | + if err != nil { |
| 125 | + return nil, nil, err |
| 126 | + } |
| 127 | + return diffRet.ExistLayers, layerPaths, nil |
| 128 | +} |
| 129 | + |
| 130 | +func (cli *DockerCli) ImageLoadFromTar(ctx context.Context, tr io.Reader, quiet bool) (*types.ImageLoadResponse, error) { |
| 131 | + tmpDir, err := ioutil.TempDir("", "hyper-pull-local-") |
| 132 | + if err != nil { |
| 133 | + return nil, err |
| 134 | + } |
| 135 | + defer os.RemoveAll(tmpDir) |
| 136 | + |
| 137 | + if err := archive.Untar(tr, tmpDir, &archive.TarOptions{NoLchown: true}); err != nil { |
| 138 | + return nil, err |
| 139 | + } |
| 140 | + |
| 141 | + if !quiet { |
| 142 | + fmt.Fprintln(cli.out, "Diffing local image with remote image...") |
| 143 | + } |
| 144 | + |
| 145 | + existLayers, layerPaths, err := cli.getExistLayers(ctx, tmpDir) |
| 146 | + if err != nil { |
| 147 | + return nil, err |
| 148 | + } |
| 149 | + |
| 150 | + if err := removeExistLayers(tmpDir, existLayers, layerPaths); err != nil { |
| 151 | + return nil, err |
| 152 | + } |
| 153 | + |
| 154 | + fs, err := archive.Tar(tmpDir, archive.Gzip) |
| 155 | + if err != nil { |
| 156 | + return nil, err |
| 157 | + } |
| 158 | + defer fs.Close() |
| 159 | + |
| 160 | + hasNewLayers := len(existLayers) != len(layerPaths) |
| 161 | + if hasNewLayers { |
| 162 | + if !quiet { |
| 163 | + fmt.Fprintln(cli.out, "Preparing to upload image...") |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + tarTmpDir, err := ioutil.TempDir("", "hyper-pull-local-") |
| 168 | + if err != nil { |
| 169 | + return nil, err |
| 170 | + } |
| 171 | + defer os.RemoveAll(tarTmpDir) |
| 172 | + |
| 173 | + tarPath := filepath.Join(tarTmpDir, "image.tar") |
| 174 | + |
| 175 | + tf, err := os.Create(tarPath) |
| 176 | + if err != nil { |
| 177 | + return nil, err |
| 178 | + } |
| 179 | + defer tf.Close() |
| 180 | + |
| 181 | + _, err = io.Copy(tf, fs) |
| 182 | + if err != nil { |
| 183 | + return nil, err |
| 184 | + } |
| 185 | + os.RemoveAll(tmpDir) |
| 186 | + |
| 187 | + info, err := tf.Stat() |
| 188 | + if err != nil { |
| 189 | + return nil, err |
| 190 | + } |
| 191 | + tf, err = os.Open(tarPath) |
| 192 | + if err != nil { |
| 193 | + return nil, err |
| 194 | + } |
| 195 | + |
| 196 | + resp, err := cli.client.ImageLoadLocal(ctx, quiet, info.Size()) |
| 197 | + if err != nil { |
| 198 | + return nil, err |
| 199 | + } |
| 200 | + |
| 201 | + if !hasNewLayers || quiet { |
| 202 | + go func() { |
| 203 | + _, err := io.Copy(resp.Conn, tf) |
| 204 | + if err != nil { |
| 205 | + fmt.Fprintln(cli.out, err.Error()) |
| 206 | + resp.Conn.Close() |
| 207 | + return |
| 208 | + } |
| 209 | + tf.Close() |
| 210 | + }() |
| 211 | + return &types.ImageLoadResponse{ |
| 212 | + Body: resp.Conn, |
| 213 | + JSON: true, |
| 214 | + }, nil |
| 215 | + } |
| 216 | + |
| 217 | + pr, pw := io.Pipe() |
| 218 | + progressOutput := streamformatter.NewJSONStreamFormatter().NewProgressOutput(pw, false) |
| 219 | + progressReader := progress.NewProgressReader(tf, progressOutput, info.Size(), "", "Uploading image") |
| 220 | + |
| 221 | + go func() { |
| 222 | + _, err := io.Copy(resp.Conn, progressReader) |
| 223 | + if err != nil { |
| 224 | + fmt.Fprintln(cli.out, err.Error()) |
| 225 | + resp.Conn.Close() |
| 226 | + return |
| 227 | + } |
| 228 | + pw.CloseWithError(io.EOF) |
| 229 | + }() |
| 230 | + |
| 231 | + return &types.ImageLoadResponse{ |
| 232 | + Body: readCloser{io.MultiReader(pr, resp.Conn), resp.Conn}, |
| 233 | + JSON: true, |
| 234 | + }, nil |
| 235 | +} |
| 236 | + |
| 237 | +// ImageDiff diff an image layers with local and imaged |
| 238 | +func (cli *DockerCli) ImageLoadFromDaemon(ctx context.Context, name string, quiet bool) (*types.ImageLoadResponse, error) { |
| 239 | + if !quiet { |
| 240 | + fmt.Fprintln(cli.out, "Loading image from local docker daemon...") |
| 241 | + } |
| 242 | + |
| 243 | + tr, err := cli.client.ImageSaveTarFromDaemon(ctx, []string{name}) |
| 244 | + if err != nil { |
| 245 | + return nil, err |
| 246 | + } |
| 247 | + defer tr.Close() |
| 248 | + |
| 249 | + return cli.ImageLoadFromTar(ctx, tr, quiet) |
| 250 | +} |
| 251 | + |
| 252 | +// CmdLoad load a local image or a tar file |
15 | 253 | //
|
16 | 254 | // The tar archive is read from STDIN by default, or from a tar archive file.
|
17 | 255 | //
|
18 | 256 | // Usage: docker load [OPTIONS]
|
19 | 257 | func (cli *DockerCli) CmdLoad(args ...string) error {
|
20 |
| - cmd := Cli.Subcmd("load", nil, Cli.DockerCommands["load"].Description, true) |
21 |
| - infile := cmd.String([]string{"i", "-input"}, "", "Read from a remote archive file compressed with gzip, bzip, or xz") |
| 258 | + cmd := Cli.Subcmd("load", nil, "Load a local image or a tar file", true) |
| 259 | + local := cmd.String([]string{"l", "-local"}, "", "Read from a local image") |
| 260 | + infile := cmd.String([]string{"i", "-input"}, "", "Read from a local or remote archive file compressed with gzip, bzip, or xz, instead of STDIN") |
22 | 261 | quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Do not show load process")
|
23 | 262 | cmd.Require(flag.Exact, 0)
|
24 | 263 | cmd.ParseFlags(args, true)
|
25 | 264 |
|
26 |
| - if *infile == "" { |
27 |
| - return errors.New("remote archive must be specified via --input") |
| 265 | + *infile = strings.TrimSpace(*infile) |
| 266 | + *local = strings.TrimSpace(*local) |
| 267 | + |
| 268 | + var stdin io.Reader = cli.in |
| 269 | + |
| 270 | + if *infile == "" && *local == "" && stdin == nil { |
| 271 | + return errors.New("source image must be specified via --input, --local or STDIN") |
28 | 272 | }
|
29 | 273 |
|
30 |
| - var input struct { |
31 |
| - FromSrc string `json:"fromSrc"` |
32 |
| - Quiet bool `json:"quiet"` |
| 274 | + var response *types.ImageLoadResponse |
| 275 | + var err error |
| 276 | + |
| 277 | + if *local != "" { |
| 278 | + // Load from local docker daemon |
| 279 | + response, err = cli.ImageLoadFromDaemon(context.Background(), *local, *quiet) |
| 280 | + } else if *infile != "" { |
| 281 | + if strings.HasPrefix(*infile, "http://") || |
| 282 | + strings.HasPrefix(*infile, "https://") || |
| 283 | + strings.HasPrefix(*infile, "ftp://") { |
| 284 | + var input struct { |
| 285 | + FromSrc string `json:"fromSrc"` |
| 286 | + Quiet bool `json:"quiet"` |
| 287 | + } |
| 288 | + input.FromSrc = *infile |
| 289 | + input.Quiet = *quiet |
| 290 | + // Load from remote URL |
| 291 | + response, err = cli.client.ImageLoad(context.Background(), input) |
| 292 | + } else { |
| 293 | + // Load from local tar |
| 294 | + var af *os.File |
| 295 | + af, err = os.Open(*infile) |
| 296 | + if err != nil { |
| 297 | + return err |
| 298 | + } |
| 299 | + defer af.Close() |
| 300 | + response, err = cli.ImageLoadFromTar(context.Background(), af, *quiet) |
| 301 | + } |
| 302 | + } else if stdin != nil { |
| 303 | + // Load from STDIN |
| 304 | + response, err = cli.ImageLoadFromTar(context.Background(), stdin, *quiet) |
33 | 305 | }
|
34 |
| - input.FromSrc = *infile |
35 |
| - input.Quiet = *quiet |
36 | 306 |
|
37 |
| - response, err := cli.client.ImageLoad(context.Background(), input) |
38 | 307 | if err != nil {
|
39 | 308 | return err
|
40 | 309 | }
|
| 310 | + |
| 311 | + if response == nil { |
| 312 | + return nil |
| 313 | + } |
| 314 | + |
41 | 315 | defer response.Body.Close()
|
42 | 316 |
|
43 | 317 | if response.JSON {
|
|
0 commit comments