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

     1  package docker
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"runtime"
     8  	"strings"
     9  
    10  	"github.com/docker/distribution"
    11  	"github.com/docker/distribution/manifest/manifestlist"
    12  	"github.com/docker/distribution/manifest/schema1"
    13  	"github.com/docker/distribution/manifest/schema2"
    14  	"github.com/docker/distribution/reference"
    15  	"github.com/docker/distribution/registry/api/errcode"
    16  	engineTypes "github.com/docker/docker/api/types"
    17  	dockerdistribution "github.com/docker/docker/distribution"
    18  	"github.com/docker/docker/image"
    19  	"github.com/docker/docker/image/v1"
    20  	"github.com/docker/docker/registry"
    21  	"github.com/estesp/manifest-tool/types"
    22  	"github.com/opencontainers/go-digest"
    23  	"github.com/sirupsen/logrus"
    24  	"golang.org/x/net/context"
    25  )
    26  
    27  type v2ManifestFetcher struct {
    28  	endpoint    registry.APIEndpoint
    29  	repoInfo    *registry.RepositoryInfo
    30  	repo        distribution.Repository
    31  	confirmedV2 bool
    32  	includeTags bool
    33  	authConfig  engineTypes.AuthConfig
    34  	service     registry.Service
    35  }
    36  
    37  type manifestInfo struct {
    38  	blobDigests []digest.Digest
    39  	layers      []string
    40  	digest      digest.Digest
    41  	platform    manifestlist.PlatformSpec
    42  	length      int64
    43  	jsonBytes   []byte
    44  }
    45  
    46  func (mf *v2ManifestFetcher) Fetch(ctx context.Context, ref reference.Named) ([]types.ImageInspect, error) {
    47  	var err error
    48  
    49  	mf.repo, mf.confirmedV2, err = dockerdistribution.NewV2Repository(ctx, mf.repoInfo, mf.endpoint, nil, &mf.authConfig, "pull")
    50  	if err != nil {
    51  		logrus.Debugf("Error getting v2 registry: %v", err)
    52  		return nil, err
    53  	}
    54  
    55  	images, err := mf.fetchWithRepository(ctx, ref)
    56  	if err != nil {
    57  		if _, ok := err.(fallbackError); ok {
    58  			return nil, err
    59  		}
    60  		if continueOnError(err) {
    61  			logrus.Errorf("Error trying v2 registry: %v", err)
    62  			return nil, fallbackError{err: err, confirmedV2: mf.confirmedV2, transportOK: true}
    63  		}
    64  	}
    65  	for _, img := range images {
    66  		img.MediaType = schema2.MediaTypeManifest
    67  	}
    68  	return images, err
    69  }
    70  
    71  func (mf *v2ManifestFetcher) fetchWithRepository(ctx context.Context, ref reference.Named) ([]types.ImageInspect, error) {
    72  	var (
    73  		manifest    distribution.Manifest
    74  		tagOrDigest string // Used for logging/progress only
    75  		tagList     []string
    76  		imageList   = []types.ImageInspect{}
    77  	)
    78  
    79  	manSvc, err := mf.repo.Manifests(ctx)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	ref = reference.TagNameOnly(ref)
    84  
    85  	if tagged, isTagged := ref.(reference.NamedTagged); isTagged {
    86  		// NOTE: not using TagService.Get, since it uses HEAD requests
    87  		// against the manifests endpoint, which are not supported by
    88  		// all registry versions.
    89  		manifest, err = manSvc.Get(ctx, "", distribution.WithTag(tagged.Tag()))
    90  		if err != nil {
    91  			return nil, allowV1Fallback(err)
    92  		}
    93  		tagOrDigest = tagged.Tag()
    94  	} else if digested, isDigested := ref.(reference.Canonical); isDigested {
    95  		manifest, err = manSvc.Get(ctx, digested.Digest())
    96  		if err != nil {
    97  			return nil, err
    98  		}
    99  		tagOrDigest = digested.Digest().String()
   100  	} else {
   101  		return nil, fmt.Errorf("internal error: reference has neither a tag nor a digest: %s", ref.String())
   102  	}
   103  
   104  	if manifest == nil {
   105  		return nil, fmt.Errorf("image manifest does not exist for tag or digest %q", tagOrDigest)
   106  	}
   107  
   108  	// If manSvc.Get succeeded, we can be confident that the registry on
   109  	// the other side speaks the v2 protocol.
   110  	mf.confirmedV2 = true
   111  
   112  	if mf.includeTags {
   113  		tagList, err = mf.repo.Tags(ctx).All(ctx)
   114  		if err != nil {
   115  			// If this repository doesn't exist on V2, we should
   116  			// permit a fallback to V1.
   117  			if !strings.Contains(err.Error(), "unauthorized") {
   118  				// only error out if the the "list all tags" endpoint isn't blocked by the registry
   119  				// some registries may have a reason to not allow complete tag list queries
   120  				return nil, allowV1Fallback(err)
   121  			}
   122  		}
   123  	}
   124  
   125  	var (
   126  		images    []*image.Image
   127  		mfInfos   []manifestInfo
   128  		mediaType []string
   129  	)
   130  
   131  	switch v := manifest.(type) {
   132  	case *schema1.SignedManifest:
   133  		image, mfInfo, err := mf.pullSchema1(ctx, ref, v)
   134  		images = append(images, image)
   135  		mfInfos = append(mfInfos, mfInfo)
   136  		mediaType = append(mediaType, schema1.MediaTypeManifest)
   137  		if err != nil {
   138  			return nil, err
   139  		}
   140  	case *schema2.DeserializedManifest:
   141  		image, mfInfo, err := mf.pullSchema2(ctx, ref, v)
   142  		images = append(images, image)
   143  		mfInfos = append(mfInfos, mfInfo)
   144  		mediaType = append(mediaType, schema2.MediaTypeManifest)
   145  		if err != nil {
   146  			return nil, err
   147  		}
   148  	case *manifestlist.DeserializedManifestList:
   149  		images, mfInfos, mediaType, err = mf.pullManifestList(ctx, ref, v)
   150  		if err != nil {
   151  			return nil, err
   152  		}
   153  	default:
   154  		return nil, errors.New("unsupported manifest format")
   155  	}
   156  
   157  	for idx, img := range images {
   158  		imgReturn := makeImageInspect(img, tagOrDigest, mfInfos[idx], mediaType[idx], tagList)
   159  		imageList = append(imageList, *imgReturn)
   160  	}
   161  	return imageList, nil
   162  }
   163  
   164  func (mf *v2ManifestFetcher) pullSchema1(ctx context.Context, ref reference.Named, unverifiedManifest *schema1.SignedManifest) (img *image.Image, mfInfo manifestInfo, err error) {
   165  	mfInfo = manifestInfo{}
   166  	var verifiedManifest *schema1.Manifest
   167  	verifiedManifest, err = verifySchema1Manifest(unverifiedManifest, ref)
   168  	if err != nil {
   169  		return nil, mfInfo, err
   170  	}
   171  
   172  	// remove duplicate layers and check parent chain validity
   173  	err = fixManifestLayers(verifiedManifest)
   174  	if err != nil {
   175  		return nil, mfInfo, err
   176  	}
   177  
   178  	// Image history converted to the new format
   179  	var history []image.History
   180  
   181  	// Note that the order of this loop is in the direction of bottom-most
   182  	// to top-most, so that the downloads slice gets ordered correctly.
   183  	for i := len(verifiedManifest.FSLayers) - 1; i >= 0; i-- {
   184  		var throwAway struct {
   185  			ThrowAway bool `json:"throwaway,omitempty"`
   186  		}
   187  		if err := json.Unmarshal([]byte(verifiedManifest.History[i].V1Compatibility), &throwAway); err != nil {
   188  			return nil, mfInfo, err
   189  		}
   190  
   191  		h, err := v1.HistoryFromConfig([]byte(verifiedManifest.History[i].V1Compatibility), throwAway.ThrowAway)
   192  		if err != nil {
   193  			return nil, mfInfo, err
   194  		}
   195  		history = append(history, h)
   196  		mfInfo.blobDigests = append(mfInfo.blobDigests, verifiedManifest.FSLayers[i].BlobSum)
   197  	}
   198  
   199  	seen := make(map[string]bool)
   200  	for i := 0; i < len(verifiedManifest.FSLayers); i++ {
   201  		digest := verifiedManifest.FSLayers[i].BlobSum.String()
   202  		if _, ok := seen[digest]; ok {
   203  			continue
   204  		}
   205  		seen[digest] = true
   206  		mfInfo.layers = append(mfInfo.layers, digest)
   207  	}
   208  
   209  	rootFS := image.NewRootFS()
   210  	configRaw, _ := makeRawConfigFromV1Config([]byte(verifiedManifest.History[0].V1Compatibility), rootFS, history)
   211  
   212  	config, err := json.Marshal(configRaw)
   213  	if err != nil {
   214  		return nil, mfInfo, err
   215  	}
   216  
   217  	img, err = image.NewFromJSON(config)
   218  	if err != nil {
   219  		return nil, mfInfo, err
   220  	}
   221  
   222  	mfInfo.digest = digest.FromBytes(unverifiedManifest.Canonical)
   223  	// add the size of the manifest to the info struct; needed for assembling proper
   224  	// manifest lists
   225  	mfInfo.length = int64(len(unverifiedManifest.Canonical))
   226  	mfInfo.jsonBytes, err = unverifiedManifest.MarshalJSON()
   227  	if err != nil {
   228  		return nil, mfInfo, err
   229  	}
   230  	return img, mfInfo, nil
   231  }
   232  
   233  func verifySchema1Manifest(signedManifest *schema1.SignedManifest, ref reference.Named) (m *schema1.Manifest, err error) {
   234  	// If pull by digest, then verify the manifest digest. NOTE: It is
   235  	// important to do this first, before any other content validation. If the
   236  	// digest cannot be verified, don't even bother with those other things.
   237  	if digested, isCanonical := ref.(reference.Canonical); isCanonical {
   238  		verifier := digested.Digest().Verifier()
   239  		if _, err := verifier.Write(signedManifest.Canonical); err != nil {
   240  			return nil, err
   241  		}
   242  		if !verifier.Verified() {
   243  			err := fmt.Errorf("image verification failed for digest %s", digested.Digest())
   244  			logrus.Error(err)
   245  			return nil, err
   246  		}
   247  	}
   248  	m = &signedManifest.Manifest
   249  
   250  	if m.SchemaVersion != 1 {
   251  		return nil, fmt.Errorf("unsupported schema version %d for %q", m.SchemaVersion, ref.String())
   252  	}
   253  	if len(m.FSLayers) != len(m.History) {
   254  		return nil, fmt.Errorf("length of history not equal to number of layers for %q", ref.String())
   255  	}
   256  	if len(m.FSLayers) == 0 {
   257  		return nil, fmt.Errorf("no FSLayers in manifest for %q", ref.String())
   258  	}
   259  	return m, nil
   260  }
   261  
   262  func fixManifestLayers(m *schema1.Manifest) error {
   263  	imgs := make([]*image.V1Image, len(m.FSLayers))
   264  	for i := range m.FSLayers {
   265  		img := &image.V1Image{}
   266  
   267  		if err := json.Unmarshal([]byte(m.History[i].V1Compatibility), img); err != nil {
   268  			return err
   269  		}
   270  
   271  		imgs[i] = img
   272  		if err := v1.ValidateID(img.ID); err != nil {
   273  			return err
   274  		}
   275  	}
   276  
   277  	if imgs[len(imgs)-1].Parent != "" && runtime.GOOS != "windows" {
   278  		// Windows base layer can point to a base layer parent that is not in manifest.
   279  		return errors.New("Invalid parent ID in the base layer of the image")
   280  	}
   281  
   282  	// check general duplicates to error instead of a deadlock
   283  	idmap := make(map[string]struct{})
   284  
   285  	var lastID string
   286  	for _, img := range imgs {
   287  		// skip IDs that appear after each other, we handle those later
   288  		if _, exists := idmap[img.ID]; img.ID != lastID && exists {
   289  			return fmt.Errorf("ID %+v appears multiple times in manifest", img.ID)
   290  		}
   291  		lastID = img.ID
   292  		idmap[lastID] = struct{}{}
   293  	}
   294  
   295  	// backwards loop so that we keep the remaining indexes after removing items
   296  	for i := len(imgs) - 2; i >= 0; i-- {
   297  		if imgs[i].ID == imgs[i+1].ID { // repeated ID. remove and continue
   298  			m.FSLayers = append(m.FSLayers[:i], m.FSLayers[i+1:]...)
   299  			m.History = append(m.History[:i], m.History[i+1:]...)
   300  		} else if imgs[i].Parent != imgs[i+1].ID {
   301  			return fmt.Errorf("Invalid parent ID. Expected %v, got %v", imgs[i+1].ID, imgs[i].Parent)
   302  		}
   303  	}
   304  
   305  	return nil
   306  }
   307  
   308  func (mf *v2ManifestFetcher) pullSchema2(ctx context.Context, ref reference.Named, mfst *schema2.DeserializedManifest) (img *image.Image, mfInfo manifestInfo, err error) {
   309  	mfInfo.digest, err = schema2ManifestDigest(ref, mfst)
   310  	if err != nil {
   311  		return nil, mfInfo, err
   312  	}
   313  
   314  	target := mfst.Target()
   315  
   316  	configChan := make(chan []byte, 1)
   317  	errChan := make(chan error, 1)
   318  	var cancel func()
   319  	ctx, cancel = context.WithCancel(ctx)
   320  
   321  	// Pull the image config
   322  	go func() {
   323  		configJSON, err := mf.pullSchema2ImageConfig(ctx, target.Digest)
   324  		if err != nil {
   325  			errChan <- ImageConfigPullError{Err: err}
   326  			cancel()
   327  			return
   328  		}
   329  		configChan <- configJSON
   330  	}()
   331  
   332  	var (
   333  		configJSON         []byte      // raw serialized image config
   334  		unmarshalledConfig image.Image // deserialized image config
   335  	)
   336  	if runtime.GOOS == "windows" {
   337  		configJSON, unmarshalledConfig, err = receiveConfig(configChan, errChan)
   338  		if err != nil {
   339  			return nil, mfInfo, err
   340  		}
   341  		if unmarshalledConfig.RootFS == nil {
   342  			return nil, mfInfo, errors.New("image config has no rootfs section")
   343  		}
   344  	}
   345  
   346  	if configJSON == nil {
   347  		configJSON, unmarshalledConfig, err = receiveConfig(configChan, errChan)
   348  		if err != nil {
   349  			return nil, mfInfo, err
   350  		}
   351  	}
   352  
   353  	// collect all references so that we can ask for cross-repo blob
   354  	// mount in the cases where a manifest list is created from images
   355  	// outside the target repo
   356  	for _, descriptor := range mfst.References() {
   357  		mfInfo.blobDigests = append(mfInfo.blobDigests, descriptor.Digest)
   358  	}
   359  	for _, descriptor := range mfst.Layers {
   360  		mfInfo.layers = append(mfInfo.layers, descriptor.Digest.String())
   361  	}
   362  
   363  	img, err = image.NewFromJSON(configJSON)
   364  	if err != nil {
   365  		return nil, mfInfo, err
   366  	}
   367  	// add the size of the manifest to the image response; needed for assembling proper
   368  	// manifest lists
   369  	_, mfBytes, err := mfst.Payload()
   370  	if err != nil {
   371  		return nil, mfInfo, err
   372  	}
   373  	mfInfo.length = int64(len(mfBytes))
   374  	mfInfo.jsonBytes = mfBytes
   375  	return img, mfInfo, nil
   376  }
   377  
   378  func (mf *v2ManifestFetcher) pullSchema2ImageConfig(ctx context.Context, dgst digest.Digest) (configJSON []byte, err error) {
   379  	blobs := mf.repo.Blobs(ctx)
   380  	configJSON, err = blobs.Get(ctx, dgst)
   381  	if err != nil {
   382  		return nil, err
   383  	}
   384  
   385  	// Verify image config digest
   386  	verifier := dgst.Verifier()
   387  	if _, err := verifier.Write(configJSON); err != nil {
   388  		return nil, err
   389  	}
   390  	if !verifier.Verified() {
   391  		err := fmt.Errorf("image config verification failed for digest %s", dgst)
   392  		logrus.Error(err)
   393  		return nil, err
   394  	}
   395  
   396  	return configJSON, nil
   397  }
   398  
   399  func receiveConfig(configChan <-chan []byte, errChan <-chan error) ([]byte, image.Image, error) {
   400  	select {
   401  	case configJSON := <-configChan:
   402  		var unmarshalledConfig image.Image
   403  		if err := json.Unmarshal(configJSON, &unmarshalledConfig); err != nil {
   404  			return nil, image.Image{}, err
   405  		}
   406  		return configJSON, unmarshalledConfig, nil
   407  	case err := <-errChan:
   408  		return nil, image.Image{}, err
   409  		// Don't need a case for ctx.Done in the select because cancellation
   410  		// will trigger an error in p.pullSchema2ImageConfig.
   411  	}
   412  }
   413  
   414  // ImageConfigPullError is an error pulling the image config blob
   415  // (only applies to schema2).
   416  type ImageConfigPullError struct {
   417  	Err error
   418  }
   419  
   420  // Error returns the error string for ImageConfigPullError.
   421  func (e ImageConfigPullError) Error() string {
   422  	return "error pulling image configuration: " + e.Err.Error()
   423  }
   424  
   425  // allowV1Fallback checks if the error is a possible reason to fallback to v1
   426  // (even if confirmedV2 has been set already), and if so, wraps the error in
   427  // a fallbackError with confirmedV2 set to false. Otherwise, it returns the
   428  // error unmodified.
   429  func allowV1Fallback(err error) error {
   430  	switch v := err.(type) {
   431  	case errcode.Errors:
   432  		if len(v) != 0 {
   433  			if v0, ok := v[0].(errcode.Error); ok && shouldV2Fallback(v0) {
   434  				return fallbackError{err: err, confirmedV2: false, transportOK: true}
   435  			}
   436  		}
   437  	case errcode.Error:
   438  		if shouldV2Fallback(v) {
   439  			return fallbackError{err: err, confirmedV2: false, transportOK: true}
   440  		}
   441  	}
   442  	return err
   443  }
   444  
   445  // schema2ManifestDigest computes the manifest digest, and, if pulling by
   446  // digest, ensures that it matches the requested digest.
   447  func schema2ManifestDigest(ref reference.Named, mfst distribution.Manifest) (digest.Digest, error) {
   448  	_, canonical, err := mfst.Payload()
   449  	if err != nil {
   450  		return "", err
   451  	}
   452  
   453  	// If pull by digest, then verify the manifest digest.
   454  	if digested, isDigested := ref.(reference.Canonical); isDigested {
   455  		verifier := digested.Digest().Verifier()
   456  		if _, err := verifier.Write(canonical); err != nil {
   457  			return "", err
   458  		}
   459  		if !verifier.Verified() {
   460  			err := fmt.Errorf("manifest verification failed for digest %s", digested.Digest())
   461  			logrus.Error(err)
   462  			return "", err
   463  		}
   464  		return digested.Digest(), nil
   465  	}
   466  
   467  	return digest.FromBytes(canonical), nil
   468  }
   469  
   470  // pullManifestList handles "manifest lists" which point to various
   471  // platform-specific manifests.
   472  func (mf *v2ManifestFetcher) pullManifestList(ctx context.Context, ref reference.Named, mfstList *manifestlist.DeserializedManifestList) ([]*image.Image, []manifestInfo, []string, error) {
   473  	var (
   474  		imageList = []*image.Image{}
   475  		mfInfos   = []manifestInfo{}
   476  		mediaType = []string{}
   477  	)
   478  	manifestListDigest, err := schema2ManifestDigest(ref, mfstList)
   479  	if err != nil {
   480  		return nil, nil, nil, err
   481  	}
   482  	logrus.Debugf("Pulling manifest list entries for ML digest %v", manifestListDigest)
   483  
   484  	// for displaying basic information on the "outer" manifest list entry, we will
   485  	// create the first entry in the returned arrays to hold the manifest list details:
   486  	mfInfos = append(mfInfos, manifestInfo{digest: manifestListDigest})
   487  	mediaType = append(mediaType, mfstList.MediaType)
   488  	imageList = append(imageList, &image.Image{})
   489  
   490  	for _, manifestDescriptor := range mfstList.Manifests {
   491  		manSvc, err := mf.repo.Manifests(ctx)
   492  		if err != nil {
   493  			return nil, nil, nil, err
   494  		}
   495  
   496  		thisDigest := manifestDescriptor.Digest
   497  		thisPlatform := manifestDescriptor.Platform
   498  		manifest, err := manSvc.Get(ctx, thisDigest)
   499  		if err != nil {
   500  			return nil, nil, nil, err
   501  		}
   502  
   503  		manifestRef, err := reference.WithDigest(ref, thisDigest)
   504  		if err != nil {
   505  			return nil, nil, nil, err
   506  		}
   507  
   508  		switch v := manifest.(type) {
   509  		case *schema1.SignedManifest:
   510  			img, mfInfo, err := mf.pullSchema1(ctx, manifestRef, v)
   511  			imageList = append(imageList, img)
   512  			mfInfo.platform = thisPlatform
   513  			mfInfos = append(mfInfos, mfInfo)
   514  			mediaType = append(mediaType, schema1.MediaTypeManifest)
   515  			if err != nil {
   516  				return nil, nil, nil, err
   517  			}
   518  		case *schema2.DeserializedManifest:
   519  			img, mfInfo, err := mf.pullSchema2(ctx, manifestRef, v)
   520  			imageList = append(imageList, img)
   521  			mfInfo.platform = thisPlatform
   522  			mfInfos = append(mfInfos, mfInfo)
   523  			mediaType = append(mediaType, schema2.MediaTypeManifest)
   524  			if err != nil {
   525  				return nil, nil, nil, err
   526  			}
   527  		default:
   528  			return nil, nil, nil, errors.New("unsupported manifest format")
   529  		}
   530  	}
   531  
   532  	return imageList, mfInfos, mediaType, err
   533  }