github.com/estesp/manifest-tool@v1.0.3/docker/createml.go (about)

     1  package docker
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net"
     7  	"net/http"
     8  	"net/url"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/docker/distribution/manifest/manifestlist"
    13  	"github.com/docker/distribution/reference"
    14  	"github.com/docker/distribution/registry/api/v2"
    15  	"github.com/docker/distribution/registry/client/auth"
    16  	"github.com/docker/distribution/registry/client/transport"
    17  	"github.com/docker/docker/dockerversion"
    18  	"github.com/docker/docker/registry"
    19  	"github.com/opencontainers/go-digest"
    20  	"github.com/sirupsen/logrus"
    21  
    22  	"github.com/estesp/manifest-tool/types"
    23  )
    24  
    25  // we will store up a list of blobs we must ask the registry
    26  // to cross-mount into our target namespace
    27  type blobMount struct {
    28  	FromRepo string
    29  	Digest   string
    30  }
    31  
    32  // if we have mounted blobs referenced from manifests from
    33  // outside the target repository namespace we will need to
    34  // push them to our target's repo as they will be references
    35  // from the final manifest list object we push
    36  type manifestPush struct {
    37  	Name      string
    38  	Digest    string
    39  	JSONBytes []byte
    40  	MediaType string
    41  }
    42  
    43  // PutManifestList takes an authentication variable and a yaml spec struct and pushes an image list based on the spec
    44  func PutManifestList(a *types.AuthInfo, yamlInput types.YAMLInput, ignoreMissing, insecure bool) (string, int, error) {
    45  	var (
    46  		manifestList      manifestlist.ManifestList
    47  		blobMountRequests []blobMount
    48  		manifestRequests  []manifestPush
    49  	)
    50  
    51  	// process the final image name reference for the manifest list
    52  	targetRef, err := reference.ParseNormalizedNamed(yamlInput.Image)
    53  	if err != nil {
    54  		return "", 0, fmt.Errorf("Error parsing name for manifest list (%s): %v", yamlInput.Image, err)
    55  	}
    56  	targetRepo, err := registry.ParseRepositoryInfo(targetRef)
    57  	if err != nil {
    58  		return "", 0, fmt.Errorf("Error parsing repository name for manifest list (%s): %v", yamlInput.Image, err)
    59  	}
    60  	targetEndpoint, repoName, err := setupRepo(targetRepo, insecure)
    61  	if err != nil {
    62  		return "", 0, fmt.Errorf("Error setting up repository endpoint and references for %q: %v", targetRef, err)
    63  	}
    64  
    65  	// Now create the manifest list payload by looking up the manifest schemas
    66  	// for the constituent images:
    67  	logrus.Info("Retrieving digests of images...")
    68  	for _, img := range yamlInput.Manifests {
    69  		mfstData, repoInfo, err := GetImageData(a, img.Image, insecure, false)
    70  		if err != nil {
    71  			// if ignoreMissing is true, we will skip this error and simply
    72  			// log a warning that we couldn't find it in the registry
    73  			if ignoreMissing {
    74  				logrus.Warnf("Couldn't find or access image reference %q. Skipping image.", img.Image)
    75  				continue
    76  			}
    77  			return "", 0, fmt.Errorf("Inspect of image %q failed with error: %v", img.Image, err)
    78  		}
    79  		if reference.Domain(repoInfo.Name) != reference.Domain(targetRepo.Name) {
    80  			return "", 0, fmt.Errorf("Cannot use source images from a different registry than the target image: %s != %s", reference.Domain(repoInfo.Name), reference.Domain(targetRepo.Name))
    81  		}
    82  		if len(mfstData) > 1 {
    83  			// too many responses--can only happen if a manifest list was returned for the name lookup
    84  			return "", 0, fmt.Errorf("You specified a manifest list entry from a digest that points to a current manifest list. Manifest lists do not allow recursion")
    85  		}
    86  		// the non-manifest list case will always have exactly one manifest response
    87  		imgMfst := mfstData[0]
    88  
    89  		// fill os/arch from inspected image if not specified in input YAML
    90  		if img.Platform.OS == "" && img.Platform.Architecture == "" {
    91  			// prefer a full platform object, if one is already available (and appears to have meaningful content)
    92  			if imgMfst.Platform.OS != "" || imgMfst.Platform.Architecture != "" {
    93  				img.Platform = imgMfst.Platform
    94  			} else if imgMfst.Os != "" || imgMfst.Architecture != "" {
    95  				img.Platform.OS = imgMfst.Os
    96  				img.Platform.Architecture = imgMfst.Architecture
    97  			}
    98  		}
    99  
   100  		// if the origin image has OSFeature and/or OSVersion information, and
   101  		// these values were not specified in the creation YAML, then
   102  		// retain the origin values in the Platform definition for the manifest list:
   103  		if imgMfst.OSVersion != "" && img.Platform.OSVersion == "" {
   104  			img.Platform.OSVersion = imgMfst.OSVersion
   105  		}
   106  		if len(imgMfst.OSFeatures) > 0 && len(img.Platform.OSFeatures) == 0 {
   107  			img.Platform.OSFeatures = imgMfst.OSFeatures
   108  		}
   109  
   110  		// validate os/arch input
   111  		if !isValidOSArch(img.Platform.OS, img.Platform.Architecture, img.Platform.Variant) {
   112  			return "", 0, fmt.Errorf("Manifest entry for image %s has unsupported os/arch or os/arch/variant combination: %s/%s/%s", img.Image, img.Platform.OS, img.Platform.Architecture, img.Platform.Variant)
   113  		}
   114  
   115  		manifest := manifestlist.ManifestDescriptor{
   116  			Platform: img.Platform,
   117  		}
   118  		manifest.Descriptor.Digest, err = digest.Parse(imgMfst.Digest)
   119  		manifest.Size = imgMfst.Size
   120  		manifest.MediaType = imgMfst.MediaType
   121  
   122  		if err != nil {
   123  			return "", 0, fmt.Errorf("Digest parse of image %q failed with error: %v", img.Image, err)
   124  		}
   125  		logrus.Infof("Image %q is digest %s; size: %d", img.Image, imgMfst.Digest, imgMfst.Size)
   126  
   127  		// if this image is in a different repo, we need to add the layer & config digests to the list of
   128  		// requested blob mounts (cross-repository push) before pushing the manifest list
   129  		if repoName != reference.Path(repoInfo.Name) {
   130  			logrus.Debugf("Adding manifest references of %q to blob mount requests", img.Image)
   131  			for _, layer := range imgMfst.References {
   132  				blobMountRequests = append(blobMountRequests, blobMount{FromRepo: reference.Path(repoInfo.Name), Digest: layer})
   133  			}
   134  			// also must add the manifest to be pushed in the target namespace
   135  			logrus.Debugf("Adding manifest %q -> to be pushed to %q as a manifest reference", reference.Path(repoInfo.Name), repoName)
   136  			manifestRequests = append(manifestRequests, manifestPush{
   137  				Name:      reference.Path(repoInfo.Name),
   138  				Digest:    imgMfst.Digest,
   139  				JSONBytes: imgMfst.CanonicalJSON,
   140  				MediaType: imgMfst.MediaType,
   141  			})
   142  		}
   143  		manifestList.Manifests = append(manifestList.Manifests, manifest)
   144  	}
   145  
   146  	if ignoreMissing && len(manifestList.Manifests) == 0 {
   147  		// we need to verify we at least have one valid entry in the list
   148  		// otherwise our manifest list will be totally empty
   149  		return "", 0, fmt.Errorf("all entries were skipped due to missing source image references; no manifest list to push")
   150  	}
   151  	// Set the schema version
   152  	manifestList.Versioned = manifestlist.SchemaVersion
   153  
   154  	urlBuilder, err := v2.NewURLBuilderFromString(targetEndpoint.URL.String(), false)
   155  	if err != nil {
   156  		return "", 0, fmt.Errorf("Can't create URL builder from endpoint (%s): %v", targetEndpoint.URL.String(), err)
   157  	}
   158  	pushURL, err := createManifestURLFromRef(targetRef, urlBuilder)
   159  	if err != nil {
   160  		return "", 0, fmt.Errorf("Error setting up repository endpoint and references for %q: %v", targetRef, err)
   161  	}
   162  	logrus.Debugf("Manifest list push url: %s", pushURL)
   163  
   164  	deserializedManifestList, err := manifestlist.FromDescriptors(manifestList.Manifests)
   165  	if err != nil {
   166  		return "", 0, fmt.Errorf("Cannot deserialize manifest list: %v", err)
   167  	}
   168  	mediaType, p, err := deserializedManifestList.Payload()
   169  	logrus.Debugf("mediaType of manifestList: %s", mediaType)
   170  	if err != nil {
   171  		return "", 0, fmt.Errorf("Cannot retrieve payload for HTTP PUT of manifest list: %v", err)
   172  
   173  	}
   174  	manifestLen := len(p)
   175  	putRequest, err := http.NewRequest("PUT", pushURL, bytes.NewReader(p))
   176  	if err != nil {
   177  		return "", 0, fmt.Errorf("HTTP PUT request creation failed: %v", err)
   178  	}
   179  	putRequest.Header.Set("Content-Type", mediaType)
   180  
   181  	httpClient, err := getHTTPClient(a, targetRepo, targetEndpoint, repoName)
   182  	if err != nil {
   183  		return "", 0, fmt.Errorf("Failed to setup HTTP client to repository: %v", err)
   184  	}
   185  
   186  	// before we push the manifest list, if we have any blob mount requests, we need
   187  	// to ask the registry to mount those blobs in our target so they are available
   188  	// as references
   189  	if err := mountBlobs(httpClient, urlBuilder, targetRef, blobMountRequests); err != nil {
   190  		return "", 0, fmt.Errorf("Couldn't mount blobs for cross-repository push: %v", err)
   191  	}
   192  
   193  	// we also must push any manifests that are referenced in the manifest list into
   194  	// the target namespace
   195  	if err := pushReferences(httpClient, urlBuilder, targetRef, manifestRequests); err != nil {
   196  		return "", 0, fmt.Errorf("Couldn't push manifests referenced in our manifest list: %v", err)
   197  	}
   198  
   199  	resp, err := httpClient.Do(putRequest)
   200  	if err != nil {
   201  		return "", 0, fmt.Errorf("V2 registry PUT of manifest list failed: %v", err)
   202  	}
   203  	defer resp.Body.Close()
   204  
   205  	var finalDigest string
   206  	if statusSuccess(resp.StatusCode) {
   207  		dgstHeader := resp.Header.Get("Docker-Content-Digest")
   208  		dgst, err := digest.Parse(dgstHeader)
   209  		if err != nil {
   210  			return "", 0, err
   211  		}
   212  		finalDigest = string(dgst)
   213  	} else {
   214  		return "", 0, fmt.Errorf("Registry push unsuccessful: response %d: %s", resp.StatusCode, resp.Status)
   215  	}
   216  	// if the YAML includes additional tags, push the added tag references. No other work
   217  	// should be required as we have already made sure all target blobs are cross-repo
   218  	// mounted and all referenced manifests are already pushed.
   219  	for _, tag := range yamlInput.Tags {
   220  		newRef, err := reference.WithTag(targetRef, tag)
   221  		if err != nil {
   222  			return "", 0, fmt.Errorf("Error creating tagged reference for added tag %q: %v", tag, err)
   223  		}
   224  		pushURL, err := createManifestURLFromRef(newRef, urlBuilder)
   225  		if err != nil {
   226  			return "", 0, fmt.Errorf("Error setting up repository endpoint and references for %q: %v", newRef, err)
   227  		}
   228  		logrus.Debugf("[extra tag %q] push url: %s", tag, pushURL)
   229  		putRequest, err := http.NewRequest("PUT", pushURL, bytes.NewReader(p))
   230  		if err != nil {
   231  			return "", 0, fmt.Errorf("[extra tag %q] HTTP PUT request creation failed: %v", tag, err)
   232  		}
   233  		putRequest.Header.Set("Content-Type", mediaType)
   234  		resp, err := httpClient.Do(putRequest)
   235  		if err != nil {
   236  			return "", 0, fmt.Errorf("[extra tag %q] V2 registry PUT of manifest list failed: %v", tag, err)
   237  		}
   238  		defer resp.Body.Close()
   239  
   240  		if statusSuccess(resp.StatusCode) {
   241  			dgstHeader := resp.Header.Get("Docker-Content-Digest")
   242  			dgst, err := digest.Parse(dgstHeader)
   243  			if err != nil {
   244  				return "", 0, err
   245  			}
   246  			if string(dgst) != finalDigest {
   247  				logrus.Warnf("Extra tag %q push resulted in non-matching digest %s (should be %s", tag, string(dgst), finalDigest)
   248  			}
   249  		} else {
   250  			return "", 0, fmt.Errorf("[extra tag %q] Registry push unsuccessful: response %d: %s", tag, resp.StatusCode, resp.Status)
   251  		}
   252  	}
   253  	return finalDigest, manifestLen, nil
   254  }
   255  
   256  func getHTTPClient(a *types.AuthInfo, repoInfo *registry.RepositoryInfo, endpoint registry.APIEndpoint, repoName string) (*http.Client, error) {
   257  	// get the http transport, this will be used in a client to upload manifest
   258  	// TODO - add separate function get client
   259  	base := &http.Transport{
   260  		Proxy: http.ProxyFromEnvironment,
   261  		Dial: (&net.Dialer{
   262  			Timeout:   30 * time.Second,
   263  			KeepAlive: 30 * time.Second,
   264  			DualStack: true,
   265  		}).Dial,
   266  		TLSHandshakeTimeout: 10 * time.Second,
   267  		TLSClientConfig:     endpoint.TLSConfig,
   268  		DisableKeepAlives:   true,
   269  	}
   270  	authConfig, err := getAuthConfig(a, repoInfo.Index)
   271  	if err != nil {
   272  		return nil, fmt.Errorf("Cannot retrieve authconfig: %v", err)
   273  	}
   274  	modifiers := registry.Headers(dockerversion.DockerUserAgent(nil), http.Header{})
   275  	authTransport := transport.NewTransport(base, modifiers...)
   276  	challengeManager, _, err := registry.PingV2Registry(endpoint.URL, authTransport)
   277  	if err != nil {
   278  		return nil, fmt.Errorf("Ping of V2 registry failed: %v", err)
   279  	}
   280  	if authConfig.RegistryToken != "" {
   281  		passThruTokenHandler := &existingTokenHandler{token: authConfig.RegistryToken}
   282  		modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, passThruTokenHandler))
   283  	} else {
   284  		creds := dumbCredentialStore{auth: &authConfig}
   285  		tokenHandler := auth.NewTokenHandler(authTransport, creds, repoName, "push", "pull")
   286  		basicHandler := auth.NewBasicHandler(creds)
   287  		modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
   288  	}
   289  	tr := transport.NewTransport(base, modifiers...)
   290  
   291  	httpClient := &http.Client{
   292  		Transport:     tr,
   293  		CheckRedirect: checkHTTPRedirect,
   294  	}
   295  	return httpClient, nil
   296  }
   297  
   298  func createManifestURLFromRef(targetRef reference.Named, urlBuilder *v2.URLBuilder) (string, error) {
   299  	// get rid of hostname so the target URL is constructed properly
   300  	hostname, name := splitHostname(targetRef.String())
   301  	targetRef, err := getNamedRefWithoutHostname(name)
   302  	if err != nil {
   303  		return "", fmt.Errorf("Can't parse target image repository name from reference: %v", err)
   304  	}
   305  
   306  	// Set the tag to latest, if no tag found in YAML
   307  	if _, isTagged := targetRef.(reference.NamedTagged); !isTagged {
   308  		targetRef = reference.TagNameOnly(targetRef)
   309  	} else {
   310  		tagged, _ := targetRef.(reference.NamedTagged)
   311  		targetRef, err = reference.WithTag(targetRef, tagged.Tag())
   312  		if err != nil {
   313  			return "", fmt.Errorf("Error referencing specified tag to target repository name: %v", err)
   314  		}
   315  	}
   316  
   317  	manifestURL, err := buildManifestURL(urlBuilder, hostname, targetRef)
   318  	if err != nil {
   319  		return "", fmt.Errorf("Failed to build manifest URL from target reference: %v", err)
   320  	}
   321  	return manifestURL, nil
   322  }
   323  
   324  func setupRepo(repoInfo *registry.RepositoryInfo, insecure bool) (registry.APIEndpoint, string, error) {
   325  
   326  	options := registry.ServiceOptions{}
   327  	if insecure {
   328  		options.InsecureRegistries = append(options.InsecureRegistries, reference.Domain(repoInfo.Name))
   329  	}
   330  	registryService, err := registry.NewService(options)
   331  	if err != nil {
   332  		return registry.APIEndpoint{}, "", err
   333  	}
   334  
   335  	endpoints, err := registryService.LookupPushEndpoints(reference.Domain(repoInfo.Name))
   336  	if err != nil {
   337  		return registry.APIEndpoint{}, "", err
   338  	}
   339  	logrus.Debugf("endpoints: %v", endpoints)
   340  	// take highest priority endpoint
   341  	endpoint := endpoints[0]
   342  	// if insecure, and there is an "http" endpoint, prefer that
   343  	if insecure {
   344  		for _, ep := range endpoints {
   345  			if ep.URL.Scheme == "http" {
   346  				endpoint = ep
   347  			}
   348  		}
   349  		endpoint.TLSConfig.InsecureSkipVerify = true
   350  	}
   351  
   352  	repoName := repoInfo.Name.Name()
   353  	// If endpoint does not support CanonicalName, use the Name's path instead
   354  	if endpoint.TrimHostname {
   355  		repoName = reference.Path(repoInfo.Name)
   356  		logrus.Debugf("repoName: %v", repoName)
   357  	}
   358  	return endpoint, repoName, nil
   359  }
   360  
   361  func pushReferences(httpClient *http.Client, urlBuilder *v2.URLBuilder, ref reference.Named, manifests []manifestPush) error {
   362  	// for each referenced manifest object in the manifest list (that is outside of our current repo/name)
   363  	// we need to push by digest the manifest so that it is added as a valid reference in the current
   364  	// repo. This will allow us to push the manifest list properly later and have all valid references.
   365  
   366  	// first get rid of possible hostname so the target URL is constructed properly
   367  	hostname, name := splitHostname(ref.String())
   368  	ref, err := getNamedRefWithoutHostname(name)
   369  	if err != nil {
   370  		return fmt.Errorf("Error parsing repo/name portion of reference without hostname: %s: %v", name, err)
   371  	}
   372  	for _, manifest := range manifests {
   373  		dgst, err := digest.Parse(manifest.Digest)
   374  		if err != nil {
   375  			return fmt.Errorf("Error parsing manifest digest (%s) for referenced manifest %q: %v", manifest.Digest, manifest.Name, err)
   376  		}
   377  		targetRef, err := reference.WithDigest(reference.TrimNamed(ref), dgst)
   378  		if err != nil {
   379  			return fmt.Errorf("Error creating manifest digest target for referenced manifest %q: %v", manifest.Name, err)
   380  		}
   381  		pushURL, err := buildManifestURL(urlBuilder, hostname, targetRef)
   382  		if err != nil {
   383  			return fmt.Errorf("Error setting up manifest push URL for manifest references for %q: %v", manifest.Name, err)
   384  		}
   385  		logrus.Debugf("manifest reference push URL: %s", pushURL)
   386  
   387  		pushRequest, err := http.NewRequest("PUT", pushURL, bytes.NewReader(manifest.JSONBytes))
   388  		if err != nil {
   389  			return fmt.Errorf("HTTP PUT request creation for manifest reference push failed: %v", err)
   390  		}
   391  		pushRequest.Header.Set("Content-Type", manifest.MediaType)
   392  		resp, err := httpClient.Do(pushRequest)
   393  		if err != nil {
   394  			return fmt.Errorf("PUT of manifest reference failed: %v", err)
   395  		}
   396  
   397  		resp.Body.Close()
   398  		if !statusSuccess(resp.StatusCode) {
   399  			return fmt.Errorf("Referenced manifest push unsuccessful: response %d: %s", resp.StatusCode, resp.Status)
   400  		}
   401  		dgstHeader := resp.Header.Get("Docker-Content-Digest")
   402  		dgstResult, err := digest.Parse(dgstHeader)
   403  		if err != nil {
   404  			return fmt.Errorf("Couldn't parse pushed manifest digest response: %v", err)
   405  		}
   406  		if string(dgstResult) != manifest.Digest {
   407  			return fmt.Errorf("Pushed referenced manifest received a different digest: expected %s, got %s", manifest.Digest, string(dgst))
   408  		}
   409  		logrus.Debugf("referenced manifest %q pushed; digest matches: %s", manifest.Name, string(dgst))
   410  	}
   411  	return nil
   412  }
   413  
   414  func mountBlobs(httpClient *http.Client, urlBuilder *v2.URLBuilder, ref reference.Named, blobsRequested []blobMount) error {
   415  	// get rid of hostname so the target URL is constructed properly
   416  	hostname, name := splitHostname(ref.String())
   417  	targetRef, err := getNamedRefWithoutHostname(name)
   418  	if err != nil {
   419  		return fmt.Errorf("Can't parse reference without hostname: %v", err)
   420  	}
   421  
   422  	for _, blob := range blobsRequested {
   423  		// create URL request
   424  		url, err := buildBlobUploadURL(urlBuilder, hostname, targetRef, url.Values{"from": {blob.FromRepo}, "mount": {blob.Digest}})
   425  		if err != nil {
   426  			return fmt.Errorf("Failed to create blob mount URL: %v", err)
   427  		}
   428  		mountRequest, err := http.NewRequest("POST", url, nil)
   429  		if err != nil {
   430  			return fmt.Errorf("HTTP POST request creation for blob mount failed: %v", err)
   431  		}
   432  		mountRequest.Header.Set("Content-Length", "0")
   433  		resp, err := httpClient.Do(mountRequest)
   434  		if err != nil {
   435  			return fmt.Errorf("V2 registry POST of blob mount failed: %v", err)
   436  		}
   437  
   438  		resp.Body.Close()
   439  		if !statusSuccess(resp.StatusCode) {
   440  			return fmt.Errorf("Blob mount failed to url %s: HTTP status %d", url, resp.StatusCode)
   441  		}
   442  		logrus.Debugf("Mount of blob %s succeeded, location: %q", blob.Digest, resp.Header.Get("Location"))
   443  	}
   444  	return nil
   445  }
   446  
   447  func buildManifestURL(ub *v2.URLBuilder, hostname string, targetRef reference.Named) (string, error) {
   448  	if !isHubLibraryRef(targetRef, hostname) {
   449  		return ub.BuildManifestURL(targetRef)
   450  	}
   451  	// this is a library reference and we don't want to lose the "library/" part of the URL ref
   452  	baseURL, err := ub.BuildBaseURL()
   453  	if err != nil {
   454  		return "", err
   455  	}
   456  	tagOrDigest := ""
   457  	switch v := targetRef.(type) {
   458  	case reference.Tagged:
   459  		tagOrDigest = v.Tag()
   460  	case reference.Digested:
   461  		tagOrDigest = v.Digest().String()
   462  	}
   463  	baseURL = fmt.Sprintf("%s%s/%s/%s", baseURL, reference.Path(targetRef), "manifests", tagOrDigest)
   464  	return baseURL, nil
   465  }
   466  
   467  func buildBlobUploadURL(ub *v2.URLBuilder, hostname string, targetRef reference.Named, values url.Values) (string, error) {
   468  	if !isHubLibraryRef(targetRef, hostname) {
   469  		return ub.BuildBlobUploadURL(targetRef, values)
   470  	}
   471  	// this is a library reference and we don't want to lose the "library/" part of the URL ref
   472  	baseURL, err := ub.BuildBaseURL()
   473  	if err != nil {
   474  		return "", err
   475  	}
   476  	baseURL = fmt.Sprintf("%s%s/%s", baseURL, reference.Path(targetRef), "blobs/uploads/")
   477  	return appendValues(baseURL, values), nil
   478  }
   479  
   480  func isHubLibraryRef(targetRef reference.Named, hostname string) bool {
   481  	return strings.HasPrefix(reference.Path(targetRef), DefaultRepoPrefix) && hostname == DefaultHostname
   482  }
   483  
   484  func getNamedRefWithoutHostname(ref string) (reference.Named, error) {
   485  	targetRef, err := reference.Parse(ref)
   486  	if err != nil {
   487  		return nil, fmt.Errorf("Can't parse reference without hostname: %v", err)
   488  	}
   489  	named, isNamed := targetRef.(reference.Named)
   490  	if !isNamed {
   491  		return nil, fmt.Errorf("Parsed reference is not a Named object: %s", ref)
   492  	}
   493  	return named, nil
   494  }
   495  
   496  // NOTE: these two functions are copied from github.com/docker/distribution/registry/api/v2/urls.go
   497  //       to handle the issue of needing to preserve non-normalized names for pushing to "library/" on
   498  //       DockerHub
   499  //
   500  // appendValuesURL appends the parameters to the url.
   501  func appendValuesURL(u *url.URL, values ...url.Values) *url.URL {
   502  	merged := u.Query()
   503  
   504  	for _, v := range values {
   505  		for k, vv := range v {
   506  			merged[k] = append(merged[k], vv...)
   507  		}
   508  	}
   509  	u.RawQuery = merged.Encode()
   510  	return u
   511  }
   512  
   513  // appendValues appends the parameters to the url. Panics if the string is not
   514  // a url.
   515  func appendValues(u string, values ...url.Values) string {
   516  	up, err := url.Parse(u)
   517  
   518  	if err != nil {
   519  		panic(err) // should never happen
   520  	}
   521  
   522  	return appendValuesURL(up, values...).String()
   523  }
   524  
   525  func statusSuccess(status int) bool {
   526  	return status >= 200 && status <= 399
   527  }