github.com/uber/kraken@v0.1.4/tools/bin/puller/pull.go (about) 1 // Copyright (c) 2016-2019 Uber Technologies, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 package main 15 16 import ( 17 "bytes" 18 "fmt" 19 "io" 20 "io/ioutil" 21 "net/http" 22 "net/http/httputil" 23 "os/exec" 24 "strings" 25 "sync" 26 "time" 27 28 "github.com/uber/kraken/utils/errutil" 29 "github.com/uber/kraken/utils/log" 30 31 "github.com/docker/distribution" 32 "github.com/docker/distribution/manifest/schema1" 33 "github.com/docker/distribution/manifest/schema2" 34 "github.com/opencontainers/go-digest" 35 ) 36 37 // guessDigest returns digest from the URL. 38 // returns empty string if this push action does not look like a tag. 39 func guessDigest(url string, repo string) string { 40 p := fmt.Sprintf("/v2/%s/manifests/", repo) 41 idx := strings.Index(url, p) 42 if idx < 0 { 43 return "" 44 } 45 return url[idx+len(p):] 46 } 47 48 // PullImage pull images from source registry, it does not check if the file exits 49 func PullImage(source, repo, tag string, useDocker bool) error { 50 t := time.Now() 51 52 if useDocker { 53 log.Info("pulling with docker daemon") 54 err := exec.Command("docker", "pull", source+"/"+repo+":"+tag).Run() 55 if err != nil { 56 return fmt.Errorf("failed to pull image: %s:%s: %s", repo, tag, err.Error()) 57 } 58 return nil 59 } 60 61 log.Info("pulling with http") 62 manifest, err := pullManifest(http.Client{Timeout: transferTimeout}, source, repo, tag) 63 if err != nil { 64 return fmt.Errorf("failed to pull manifest %s:%s: %s", repo, tag, err) 65 } 66 67 layerDigests, err := getLayerDigestsFromManifest(&manifest) 68 if err != nil { 69 return fmt.Errorf("failed to get layer digests from manifest: %s", err) 70 } 71 72 var wg sync.WaitGroup 73 var mu sync.Mutex 74 var errs errutil.MultiError 75 for _, d := range layerDigests { 76 wg.Add(1) 77 d := d 78 go func() { 79 defer wg.Done() 80 err := pullLayer(http.Client{Timeout: transferTimeout}, source, repo, d) 81 if err != nil { 82 mu.Lock() 83 defer mu.Unlock() 84 errs = append(errs, err) 85 return 86 } 87 }() 88 } 89 wg.Wait() 90 91 if errs != nil { 92 return fmt.Errorf("failed to pull image %s:%s: %s", repo, tag, errs) 93 } 94 95 log.Infof("finished pulling image %s:%s in %v", repo, tag, time.Since(t).Seconds()) 96 return nil 97 } 98 99 func pullManifest(client http.Client, source string, name string, reference string) (distribution.Manifest, error) { 100 manifestURL := fmt.Sprintf(baseManifestQuery, source, name, reference) 101 req, err := http.NewRequest("GET", manifestURL, bytes.NewReader([]byte{})) 102 if err != nil { 103 return nil, err 104 } 105 // Add `Accept` header to indicate schema2 is supported 106 req.Header.Add("Accept", schema2.MediaTypeManifest) 107 resp, err := client.Do(req) 108 109 if err != nil { 110 return nil, err 111 } 112 defer resp.Body.Close() 113 114 if resp.StatusCode == 404 { 115 return nil, fmt.Errorf("manifest not found") 116 } 117 118 if resp.StatusCode != 200 { 119 return nil, fmt.Errorf("server returned %v", resp.Status) 120 } 121 122 version := resp.Header.Get("Content-Type") 123 body, err := ioutil.ReadAll(resp.Body) 124 if err != nil { 125 return nil, err 126 } 127 manifest, _, err := distribution.UnmarshalManifest(version, body) 128 if err != nil { 129 return nil, err 130 } 131 132 return manifest, nil 133 } 134 135 func getLayerDigestsFromManifest(manifest *distribution.Manifest) ([]string, error) { 136 var digests []string 137 // Get layers from manifest 138 switch (*manifest).(type) { 139 case *schema1.SignedManifest: 140 fsLayers := (*manifest).(*schema1.SignedManifest).FSLayers 141 for _, fsLayer := range fsLayers { 142 digests = append(digests, fsLayer.BlobSum.String()) 143 } 144 break 145 case *schema2.DeserializedManifest: 146 layerDescriptors := (*manifest).(*schema2.DeserializedManifest).Layers 147 for _, descriptor := range layerDescriptors { 148 digests = append(digests, descriptor.Digest.String()) 149 } 150 // for schema2, we also need a config layer 151 config := (*manifest).(*schema2.DeserializedManifest).Config 152 digests = append(digests, config.Digest.String()) 153 break 154 default: 155 mt, _, err := (*manifest).Payload() 156 if err == nil { 157 err = fmt.Errorf("manifest type %s is not supported", mt) 158 } 159 return nil, err 160 } 161 162 return digests, nil 163 } 164 165 func pullLayer(client http.Client, source, name string, layerDigest string) error { 166 layerURL := fmt.Sprintf(baseLayerQuery, source, name, layerDigest) 167 resp, err := client.Get(layerURL) 168 if err != nil { 169 return err 170 } 171 172 defer resp.Body.Close() 173 174 if resp.StatusCode != 200 { 175 respDump, errDump := httputil.DumpResponse(resp, true) 176 if errDump != nil { 177 return errDump 178 } 179 return fmt.Errorf("failed to pull layer: %s", respDump) 180 } 181 182 ok, err := verifyLayer(digest.Digest(layerDigest), resp.Body) 183 if err != nil { 184 return fmt.Errorf("failed to verfiy layer: %s", err) 185 } 186 187 if !ok { 188 return fmt.Errorf("failed to verify layer: layer digest does not match to the content") 189 } 190 191 return nil 192 } 193 194 func verifyLayer(layerDigest digest.Digest, r io.Reader) (bool, error) { 195 v := layerDigest.Verifier() 196 197 if _, err := io.Copy(v, r); err != nil { 198 return false, err 199 } 200 201 return v.Verified(), nil 202 }