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

     1  package docker
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"strings"
    10  	"syscall"
    11  	"time"
    12  
    13  	"github.com/docker/cli/cli/config"
    14  	"github.com/docker/distribution/manifest/manifestlist"
    15  	"github.com/docker/distribution/reference"
    16  	"github.com/docker/distribution/registry/api/errcode"
    17  	v2 "github.com/docker/distribution/registry/api/v2"
    18  	"github.com/docker/distribution/registry/client"
    19  	"github.com/docker/docker/api"
    20  	engineTypes "github.com/docker/docker/api/types"
    21  	registryTypes "github.com/docker/docker/api/types/registry"
    22  	"github.com/docker/docker/api/types/versions"
    23  	"github.com/docker/docker/distribution"
    24  	"github.com/docker/docker/image"
    25  	"github.com/docker/docker/registry"
    26  	"github.com/estesp/manifest-tool/types"
    27  	"github.com/sirupsen/logrus"
    28  	"golang.org/x/net/context"
    29  )
    30  
    31  const (
    32  	// DefaultHostname is the default built-in registry (DockerHub)
    33  	DefaultHostname = "docker.io"
    34  	// LegacyDefaultHostname is the old hostname used for DockerHub
    35  	LegacyDefaultHostname = "index.docker.io"
    36  	// DefaultRepoPrefix is the prefix used for official images in DockerHub
    37  	DefaultRepoPrefix = "library/"
    38  )
    39  
    40  type existingTokenHandler struct {
    41  	token string
    42  }
    43  
    44  type dumbCredentialStore struct {
    45  	auth *engineTypes.AuthConfig
    46  }
    47  
    48  func (dcs dumbCredentialStore) Basic(*url.URL) (string, string) {
    49  	return dcs.auth.Username, dcs.auth.Password
    50  }
    51  
    52  func (th *existingTokenHandler) Scheme() string {
    53  	return "bearer"
    54  }
    55  
    56  func (th *existingTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
    57  	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.token))
    58  	return nil
    59  }
    60  
    61  func (dcs dumbCredentialStore) RefreshToken(*url.URL, string) string {
    62  	return dcs.auth.IdentityToken
    63  }
    64  
    65  func (dcs dumbCredentialStore) SetRefreshToken(*url.URL, string, string) {
    66  }
    67  
    68  // fallbackError wraps an error that can possibly allow fallback to a different
    69  // endpoint.
    70  type fallbackError struct {
    71  	// err is the error being wrapped.
    72  	err error
    73  	// confirmedV2 is set to true if it was confirmed that the registry
    74  	// supports the v2 protocol. This is used to limit fallbacks to the v1
    75  	// protocol.
    76  	confirmedV2 bool
    77  	transportOK bool
    78  }
    79  
    80  // Error renders the FallbackError as a string.
    81  func (f fallbackError) Error() string {
    82  	return f.err.Error()
    83  }
    84  
    85  type manifestFetcher interface {
    86  	Fetch(ctx context.Context, ref reference.Named) ([]types.ImageInspect, error)
    87  }
    88  
    89  func validateName(name string) error {
    90  	distref, err := reference.ParseNormalizedNamed(name)
    91  	if err != nil {
    92  		return err
    93  	}
    94  	hostname, _ := splitHostname(distref.String())
    95  	if hostname == "" {
    96  		return fmt.Errorf("Please use a fully qualified repository name")
    97  	}
    98  	return nil
    99  }
   100  
   101  // splitHostname splits a repository name to hostname and remotename string.
   102  // If no valid hostname is found, the default hostname is used. Repository name
   103  // needs to be already validated before.
   104  func splitHostname(name string) (hostname, remoteName string) {
   105  	i := strings.IndexRune(name, '/')
   106  	if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost") {
   107  		hostname, remoteName = DefaultHostname, name
   108  	} else {
   109  		hostname, remoteName = name[:i], name[i+1:]
   110  	}
   111  	if hostname == LegacyDefaultHostname {
   112  		hostname = DefaultHostname
   113  	}
   114  	if hostname == DefaultHostname && !strings.ContainsRune(remoteName, '/') {
   115  		remoteName = DefaultRepoPrefix + remoteName
   116  	}
   117  	return
   118  }
   119  
   120  func checkHTTPRedirect(req *http.Request, via []*http.Request) error {
   121  	if len(via) >= 10 {
   122  		return errors.New("stopped after 10 redirects")
   123  	}
   124  
   125  	if len(via) > 0 {
   126  		for headerName, headerVals := range via[0].Header {
   127  			if headerName != "Accept" && headerName != "Range" {
   128  				continue
   129  			}
   130  			for _, val := range headerVals {
   131  				// Don't add to redirected request if redirected
   132  				// request already has a header with the same
   133  				// name and value.
   134  				hasValue := false
   135  				for _, existingVal := range req.Header[headerName] {
   136  					if existingVal == val {
   137  						hasValue = true
   138  						break
   139  					}
   140  				}
   141  				if !hasValue {
   142  					req.Header.Add(headerName, val)
   143  				}
   144  			}
   145  		}
   146  	}
   147  
   148  	return nil
   149  }
   150  
   151  // GetImageData takes registry authentication information and a name of the image to return information about
   152  func GetImageData(a *types.AuthInfo, name string, insecure, includeTags bool) ([]types.ImageInspect, *registry.RepositoryInfo, error) {
   153  	if err := validateName(name); err != nil {
   154  		return nil, nil, err
   155  	}
   156  	ref, err := reference.ParseNormalizedNamed(name)
   157  	if err != nil {
   158  		return nil, nil, err
   159  	}
   160  	repoInfo, err := registry.ParseRepositoryInfo(ref)
   161  	if err != nil {
   162  		return nil, nil, err
   163  	}
   164  	authConfig, err := getAuthConfig(a, repoInfo.Index)
   165  	if err != nil {
   166  		return nil, nil, err
   167  	}
   168  	if err := validateRepoName(repoInfo.Name.Name()); err != nil {
   169  		return nil, nil, err
   170  	}
   171  	options := registry.ServiceOptions{}
   172  	if insecure {
   173  		options.InsecureRegistries = append(options.InsecureRegistries, reference.Domain(repoInfo.Name))
   174  	}
   175  	registryService, err := registry.NewService(options)
   176  	if err != nil {
   177  		return nil, nil, err
   178  	}
   179  
   180  	endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name))
   181  	if err != nil {
   182  		return nil, nil, err
   183  	}
   184  	logrus.Debugf("endpoints: %v", endpoints)
   185  
   186  	var (
   187  		ctx                    = context.Background()
   188  		lastErr                error
   189  		discardNoSupportErrors bool
   190  		foundImages            []types.ImageInspect
   191  		confirmedV2            bool
   192  		confirmedTLSRegistries = make(map[string]struct{})
   193  	)
   194  
   195  	for _, endpoint := range endpoints {
   196  		// make sure I can reach the registry, same as docker pull does
   197  		if endpoint.Version == registry.APIVersion1 {
   198  			logrus.Debugf("Skipping v1 endpoint %s; manifest list requires v2", endpoint.URL)
   199  			continue
   200  		}
   201  		if insecure && endpoint.URL.Scheme == "https" {
   202  			logrus.Debugf("Skipping https endpoint for insecure registry")
   203  			continue
   204  		}
   205  
   206  		if endpoint.URL.Scheme != "https" {
   207  			if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS {
   208  				logrus.Debugf("Skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL)
   209  				continue
   210  			}
   211  		}
   212  		if insecure {
   213  			endpoint.TLSConfig.InsecureSkipVerify = true
   214  		}
   215  
   216  		logrus.Debugf("Trying to fetch image manifest of %s repository from %s %s", repoInfo.Name.Name(), endpoint.URL, endpoint.Version)
   217  
   218  		fetcher, err := newManifestFetcher(endpoint, repoInfo, authConfig, registryService, includeTags)
   219  		if err != nil {
   220  			lastErr = err
   221  			continue
   222  		}
   223  
   224  		if foundImages, err = fetcher.Fetch(ctx, ref); err != nil {
   225  			// Was this fetch cancelled? If so, don't try to fall back.
   226  			fallback := false
   227  			select {
   228  			case <-ctx.Done():
   229  			default:
   230  				if fallbackErr, ok := err.(fallbackError); ok {
   231  					fallback = true
   232  					confirmedV2 = confirmedV2 || fallbackErr.confirmedV2
   233  					if fallbackErr.transportOK && endpoint.URL.Scheme == "https" {
   234  						confirmedTLSRegistries[endpoint.URL.Host] = struct{}{}
   235  					}
   236  					err = fallbackErr.err
   237  				}
   238  			}
   239  			if fallback {
   240  				if _, ok := err.(distribution.ErrNoSupport); !ok {
   241  					// Because we found an error that's not ErrNoSupport, discard all subsequent ErrNoSupport errors.
   242  					discardNoSupportErrors = true
   243  					// save the current error
   244  					lastErr = err
   245  				} else if !discardNoSupportErrors {
   246  					// Save the ErrNoSupport error, because it's either the first error or all encountered errors
   247  					// were also ErrNoSupport errors.
   248  					lastErr = err
   249  				}
   250  				continue
   251  			}
   252  			logrus.Infof("Not continuing with pull after error: %v", err)
   253  			return nil, nil, err
   254  		}
   255  
   256  		return foundImages, repoInfo, nil
   257  	}
   258  
   259  	if lastErr == nil {
   260  		lastErr = fmt.Errorf("no endpoints found for %s", ref.String())
   261  	}
   262  
   263  	return nil, nil, lastErr
   264  }
   265  
   266  func newManifestFetcher(endpoint registry.APIEndpoint, repoInfo *registry.RepositoryInfo, authConfig engineTypes.AuthConfig, registryService registry.Service, includeTags bool) (manifestFetcher, error) {
   267  	switch endpoint.Version {
   268  	case registry.APIVersion2:
   269  		return &v2ManifestFetcher{
   270  			endpoint:    endpoint,
   271  			authConfig:  authConfig,
   272  			service:     registryService,
   273  			repoInfo:    repoInfo,
   274  			includeTags: includeTags,
   275  		}, nil
   276  	case registry.APIVersion1:
   277  		return &v1ManifestFetcher{
   278  			endpoint:   endpoint,
   279  			authConfig: authConfig,
   280  			service:    registryService,
   281  			repoInfo:   repoInfo,
   282  		}, nil
   283  	}
   284  	return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL)
   285  }
   286  
   287  func getAuthConfig(a *types.AuthInfo, index *registryTypes.IndexInfo) (engineTypes.AuthConfig, error) {
   288  
   289  	var (
   290  		username      = a.Username
   291  		password      = a.Password
   292  		cfg           = a.DockerCfg
   293  		defAuthConfig = engineTypes.AuthConfig{
   294  			Username: a.Username,
   295  			Password: a.Password,
   296  			Email:    "stub@example.com",
   297  		}
   298  	)
   299  
   300  	if username != "" && password != "" {
   301  		return defAuthConfig, nil
   302  	}
   303  
   304  	confFile, err := config.Load(cfg)
   305  	if err != nil {
   306  		return engineTypes.AuthConfig{}, err
   307  	}
   308  	authConfig := registry.ResolveAuthConfig(confFile.AuthConfigs, index)
   309  	logrus.Debugf("authConfig for %s: %v", index.Name, authConfig.Username)
   310  
   311  	return authConfig, nil
   312  }
   313  
   314  func validateRepoName(name string) error {
   315  	if name == "" {
   316  		return fmt.Errorf("Repository name can't be empty")
   317  	}
   318  	if name == api.NoBaseImageSpecifier {
   319  		return fmt.Errorf("'%s' is a reserved name", api.NoBaseImageSpecifier)
   320  	}
   321  	return nil
   322  }
   323  
   324  func makeImageInspect(img *image.Image, tag string, mfInfo manifestInfo, mediaType string, tagList []string) *types.ImageInspect {
   325  	var digest string
   326  	if err := mfInfo.digest.Validate(); err == nil {
   327  		digest = mfInfo.digest.String()
   328  	}
   329  
   330  	// for manifest lists, we only want to display the basic info that this is
   331  	// a manifest list and its digest information:
   332  	if mediaType == manifestlist.MediaTypeManifestList {
   333  		return &types.ImageInspect{
   334  			MediaType: mediaType,
   335  			Digest:    digest,
   336  		}
   337  	}
   338  
   339  	var digests []string
   340  	for _, blobDigest := range mfInfo.blobDigests {
   341  		digests = append(digests, blobDigest.String())
   342  	}
   343  	return &types.ImageInspect{
   344  		Size:            mfInfo.length,
   345  		MediaType:       mediaType,
   346  		Tag:             tag,
   347  		Digest:          digest,
   348  		RepoTags:        tagList,
   349  		Comment:         img.Comment,
   350  		Created:         img.Created.Format(time.RFC3339Nano),
   351  		ContainerConfig: &img.ContainerConfig,
   352  		DockerVersion:   img.DockerVersion,
   353  		Author:          img.Author,
   354  		Config:          img.Config,
   355  		Architecture:    img.Architecture,
   356  		Os:              img.OS,
   357  		OSVersion:       img.OSVersion,
   358  		OSFeatures:      img.OSFeatures,
   359  		References:      digests,
   360  		Layers:          mfInfo.layers,
   361  		Platform:        mfInfo.platform,
   362  		CanonicalJSON:   mfInfo.jsonBytes,
   363  	}
   364  }
   365  
   366  func makeRawConfigFromV1Config(imageJSON []byte, rootfs *image.RootFS, history []image.History) (map[string]*json.RawMessage, error) {
   367  	var dver struct {
   368  		DockerVersion string `json:"docker_version"`
   369  	}
   370  
   371  	if err := json.Unmarshal(imageJSON, &dver); err != nil {
   372  		return nil, err
   373  	}
   374  
   375  	useFallback := versions.LessThan(dver.DockerVersion, "1.8.3")
   376  
   377  	if useFallback {
   378  		var v1Image image.V1Image
   379  		err := json.Unmarshal(imageJSON, &v1Image)
   380  		if err != nil {
   381  			return nil, err
   382  		}
   383  		imageJSON, err = json.Marshal(v1Image)
   384  		if err != nil {
   385  			return nil, err
   386  		}
   387  	}
   388  
   389  	var c map[string]*json.RawMessage
   390  	if err := json.Unmarshal(imageJSON, &c); err != nil {
   391  		return nil, err
   392  	}
   393  
   394  	c["rootfs"] = rawJSON(rootfs)
   395  	c["history"] = rawJSON(history)
   396  
   397  	return c, nil
   398  }
   399  
   400  func rawJSON(value interface{}) *json.RawMessage {
   401  	jsonval, err := json.Marshal(value)
   402  	if err != nil {
   403  		return nil
   404  	}
   405  	return (*json.RawMessage)(&jsonval)
   406  }
   407  
   408  func continueOnError(err error) bool {
   409  	switch v := err.(type) {
   410  	case errcode.Errors:
   411  		if len(v) == 0 {
   412  			return true
   413  		}
   414  		return continueOnError(v[0])
   415  	case distribution.ErrNoSupport:
   416  		return continueOnError(v.Err)
   417  	case errcode.Error:
   418  		return shouldV2Fallback(v)
   419  	case *client.UnexpectedHTTPResponseError:
   420  		return true
   421  	case ImageConfigPullError:
   422  		return false
   423  	case error:
   424  		return !strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error()))
   425  	}
   426  	// let's be nice and fallback if the error is a completely
   427  	// unexpected one.
   428  	// If new errors have to be handled in some way, please
   429  	// add them to the switch above.
   430  	return true
   431  }
   432  
   433  // shouldV2Fallback returns true if this error is a reason to fall back to v1.
   434  func shouldV2Fallback(err errcode.Error) bool {
   435  	switch err.Code {
   436  	case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown:
   437  		return true
   438  	}
   439  	return false
   440  }