github.com/apcera/util@v0.0.0-20180322191801-7a50bc84ee48/docker/v1/registry.go (about) 1 // Copyright 2014-2015 Apcera Inc. All rights reserved. 2 3 // v1 is a Docker v1 Registry API client implementation. The v1 API has 4 // been deprecated by the public Docker Hub as of December 7th, 2015. 5 // 6 // See: https://docs.docker.com/v1.6/reference/api/registry_api/ 7 package v1 8 9 import ( 10 "encoding/json" 11 "errors" 12 "fmt" 13 "io" 14 "net/http" 15 "net/http/cookiejar" 16 "net/url" 17 "strings" 18 ) 19 20 var ( 21 // DockerHubRegistryURL points to the official Docker registry. 22 DockerHubRegistryURL = "https://index.docker.io" 23 ) 24 25 // Image is a Docker image info (constructed from Docker API response). 26 type Image struct { 27 Name string 28 29 tags map[string]string // Tags available for the image. 30 endpoints []string // Docker registry endpoints. 31 token string // Docker auth token. 32 33 // scheme is an original index URL scheme (will be used to talk to endpoints returned by API). 34 scheme string 35 client *http.Client 36 } 37 38 // GetImage fetches Docker repository information from the specified Docker 39 // registry. If the registry is an empty string it defaults to the DockerHub. 40 // The integer return value is the status code of the HTTP response. 41 func GetImage(name, registryURL string) (*Image, int, error) { 42 if name == "" { 43 return nil, -1, errors.New("image name is empty") 44 } 45 46 var ru *url.URL 47 var err error 48 if len(registryURL) != 0 { 49 ru, err = url.Parse(registryURL) 50 } else { 51 ru, err = url.Parse(DockerHubRegistryURL) 52 registryURL = DockerHubRegistryURL 53 } 54 if err != nil { 55 return nil, -1, err 56 } 57 58 // In order to get layers from Docker CDN we need to hit 'images' endpoint 59 // and request the token. Client should also accept and store cookies, as 60 // they are needed to fetch the layer data later. 61 imagesURL := fmt.Sprintf("%s/v1/repositories/%s/images", registryURL, name) 62 63 req, err := http.NewRequest("GET", imagesURL, nil) 64 if err != nil { 65 return nil, -1, err 66 } 67 68 // FIXME: Send 'Connection: close' header on HTTP requests 69 // as reusing the connection is still prone to EOF/Connection reset errors 70 // in certain network environments... 71 // See: ENGT-9670 72 // https://stackoverflow.com/questions/17714494/golang-http-request-results-in-eof-errors 73 req.Close = true 74 75 req.Header.Set("X-Docker-Token", "true") 76 77 client := &http.Client{ 78 Transport: &http.Transport{ 79 Proxy: http.ProxyFromEnvironment, 80 }, 81 } 82 client.Jar, err = cookiejar.New(nil) // Docker repo API sets and uses cookies for CDN. 83 if err != nil { 84 return nil, -1, err 85 } 86 87 res, err := client.Do(req) 88 if err != nil { 89 return nil, -1, err 90 } 91 defer res.Body.Close() 92 switch res.StatusCode { 93 case http.StatusOK: 94 // Fall through. 95 case http.StatusNotFound: 96 return nil, res.StatusCode, fmt.Errorf("image %q not found", name) 97 default: 98 return nil, res.StatusCode, fmt.Errorf("HTTP %d ", res.StatusCode) 99 } 100 101 token := res.Header.Get("X-Docker-Token") 102 endpoints := strings.Split(res.Header.Get("X-Docker-Endpoints"), ",") 103 104 if len(endpoints) == 0 { 105 return nil, res.StatusCode, errors.New("Docker index response didn't contain any endpoints") 106 } 107 for i := range endpoints { 108 endpoints[i] = strings.Trim(endpoints[i], " ") 109 } 110 111 img := &Image{ 112 Name: name, 113 client: client, 114 endpoints: endpoints, 115 token: token, 116 scheme: ru.Scheme, 117 } 118 119 img.tags, err = img.fetchTags() 120 if err != nil { 121 return nil, res.StatusCode, err 122 } 123 124 return img, res.StatusCode, nil 125 } 126 127 // Tags returns a list of tags available for image 128 func (i *Image) Tags() []string { 129 result := make([]string, 0) 130 131 for tag, _ := range i.tags { 132 result = append(result, tag) 133 } 134 135 return result 136 } 137 138 // TagLayerID returns a layer ID for a given tag. 139 func (i *Image) TagLayerID(tagName string) (string, error) { 140 layerID, ok := i.tags[tagName] 141 if !ok { 142 return "", fmt.Errorf("can't find tag '%s' for image '%s'", tagName, i.Name) 143 } 144 145 return layerID, nil 146 } 147 148 // Metadata unmarshals a Docker image metadata into provided 'v' interface. 149 func (i *Image) Metadata(tagName string, v interface{}) error { 150 layerID, ok := i.tags[tagName] 151 if !ok { 152 return fmt.Errorf("can't find tag '%s' for image '%s'", tagName, i.Name) 153 } 154 155 err := i.parseResponse(fmt.Sprintf("v1/images/%s/json", layerID), &v) 156 if err != nil { 157 return err 158 } 159 return nil 160 } 161 162 // History returns an ordered list of layers that make up Docker. The order is reverse, it goes from 163 // the latest layer to the base layer. Client can iterate these layers and download them using LayerReader. 164 func (i *Image) History(tagName string) ([]string, error) { 165 layerID, ok := i.tags[tagName] 166 if !ok { 167 return nil, fmt.Errorf("can't find tag '%s' for image '%s'", tagName, i.Name) 168 } 169 170 var history []string 171 err := i.parseResponse(fmt.Sprintf("v1/images/%s/ancestry", layerID), &history) 172 if err != nil { 173 return nil, err 174 } 175 return history, nil 176 } 177 178 // LayerReader returns io.ReadCloser that can be used to read Docker layer data. 179 func (i *Image) LayerReader(id string) (io.ReadCloser, error) { 180 resp, err := i.getResponse(fmt.Sprintf("v1/images/%s/layer", id)) 181 if err != nil { 182 return nil, err 183 } 184 return resp.Body, nil 185 } 186 187 // LayerURLs returns several URLs for a specific layer. 188 func (i *Image) LayerURLs(id string) []string { 189 var urls []string 190 for _, ep := range i.endpoints { 191 urls = append(urls, fmt.Sprintf("%s://%s/v1/images/%s/layer", i.scheme, ep, id)) 192 } 193 return urls 194 } 195 196 // AuthorizationHeader exposes the authorization header created for the image 197 // for external layer downloads. 198 func (i *Image) AuthorizationHeader() string { 199 if i.token == "" { 200 return "" 201 } 202 return fmt.Sprintf("Token %s", i.token) 203 } 204 205 // fetchTags fetches tags for the image and caches them in the Image struct, 206 // so that other methods can look them up efficiently. 207 func (i *Image) fetchTags() (map[string]string, error) { 208 // There is a weird quirk about Docker API: if tags are requested from index.docker.io, 209 // it returns a list of short layer IDs, so it's impossible to use them to download actual layers. 210 // However, when we hit the endpoint returned by image index API response, it has an expected format. 211 var tags map[string]string 212 err := i.parseResponse(fmt.Sprintf("v1/repositories/%s/tags", i.Name), &tags) 213 if err != nil { 214 return nil, err 215 } 216 return tags, nil 217 } 218 219 // getAPIResponse takes a path and tries to get Docker API response from each 220 // available Docker API endpoint. It returns raw HTTP response. 221 func (i *Image) getResponse(path string) (*http.Response, error) { 222 errors := make(map[string]error) 223 224 for _, ep := range i.endpoints { 225 resp, err := i.getResponseFromURL(fmt.Sprintf("%s://%s/%s", i.scheme, ep, path)) 226 if err != nil { 227 errors[ep] = err 228 continue 229 } 230 231 return resp, nil 232 } 233 234 return nil, combineEndpointErrors(errors) 235 } 236 237 // parseJSONResponse takes a path and tries to get Docker API response from each 238 // available Docker API endpoint. It tries to parse response as JSON and saves 239 // the parsed version in the provided 'result' variable. 240 func (i *Image) parseResponse(path string, result interface{}) error { 241 errors := make(map[string]error) 242 243 for _, ep := range i.endpoints { 244 err := i.parseResponseFromURL(fmt.Sprintf("%s://%s/%s", i.scheme, ep, path), result) 245 if err != nil { 246 errors[ep] = err 247 continue 248 } 249 250 return nil 251 } 252 253 return combineEndpointErrors(errors) 254 } 255 256 // getAPIResponseFromURL returns raw Docker API response at URL 'u'. 257 func (i *Image) getResponseFromURL(u string) (*http.Response, error) { 258 req, err := http.NewRequest("GET", u, nil) 259 if err != nil { 260 return nil, err 261 } 262 req.Header.Set("Authorization", "Token "+i.token) 263 264 res, err := i.client.Do(req) 265 if err != nil { 266 return nil, err 267 } 268 269 if res.StatusCode != http.StatusOK { 270 defer res.Body.Close() 271 type errorMsg struct { 272 Error string `json:"error"` 273 } 274 275 var errMsg errorMsg 276 if err := json.NewDecoder(res.Body).Decode(&errMsg); err == nil { 277 return nil, fmt.Errorf("%s: HTTP %d - %s", u, res.StatusCode, errMsg.Error) 278 } 279 280 return nil, fmt.Errorf("%s: HTTP %d", u, res.StatusCode) 281 } 282 283 return res, nil 284 } 285 286 // parseResponseFromURL returns parsed JSON of a Docker API response at URL 'u'. 287 func (i *Image) parseResponseFromURL(u string, result interface{}) error { 288 resp, err := i.getResponseFromURL(u) 289 if err != nil { 290 return err 291 } 292 293 defer resp.Body.Close() 294 295 dec := json.NewDecoder(resp.Body) 296 if err := dec.Decode(&result); err != nil { 297 return err 298 } 299 300 return nil 301 } 302 303 // Cookie returns the string representation of the first 304 // cookie stored in stored client's cookie jar. 305 func (i *Image) Cookie(u string) (string, error) { 306 if i.client.Jar == nil { 307 return "", nil 308 } 309 310 baseURL, err := url.Parse(u) 311 if err != nil { 312 return "", fmt.Errorf("Invalid URL: %s", err) 313 } 314 315 cookies := i.client.Jar.Cookies(baseURL) 316 if len(cookies) == 0 { 317 return "", nil 318 } 319 320 return cookies[0].String(), nil 321 } 322 323 // combineEndpointErrors takes a mapping of Docker API endpoints to errors encountered 324 // while talking to them and returns a single error that contains all endpoint URLs 325 // along with error for each URL. 326 func combineEndpointErrors(allErrors map[string]error) error { 327 var parts []string 328 for ep, err := range allErrors { 329 parts = append(parts, fmt.Sprintf("%s: %s", ep, err)) 330 } 331 return errors.New(strings.Join(parts, ", ")) 332 }