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  }