github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/registry/client/fetcher.go (about)

     1  package client
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  
     7  	"github.com/docker/cli/cli/manifest/types"
     8  	"github.com/docker/distribution"
     9  	"github.com/docker/distribution/manifest/manifestlist"
    10  	"github.com/docker/distribution/manifest/ocischema"
    11  	"github.com/docker/distribution/manifest/schema2"
    12  	"github.com/docker/distribution/reference"
    13  	"github.com/docker/distribution/registry/api/errcode"
    14  	v2 "github.com/docker/distribution/registry/api/v2"
    15  	distclient "github.com/docker/distribution/registry/client"
    16  	"github.com/docker/docker/registry"
    17  	"github.com/opencontainers/go-digest"
    18  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    19  	"github.com/pkg/errors"
    20  	"github.com/sirupsen/logrus"
    21  )
    22  
    23  // fetchManifest pulls a manifest from a registry and returns it. An error
    24  // is returned if no manifest is found matching namedRef.
    25  func fetchManifest(ctx context.Context, repo distribution.Repository, ref reference.Named) (types.ImageManifest, error) {
    26  	manifest, err := getManifest(ctx, repo, ref)
    27  	if err != nil {
    28  		return types.ImageManifest{}, err
    29  	}
    30  
    31  	switch v := manifest.(type) {
    32  	// Removed Schema 1 support
    33  	case *schema2.DeserializedManifest:
    34  		imageManifest, err := pullManifestSchemaV2(ctx, ref, repo, *v)
    35  		if err != nil {
    36  			return types.ImageManifest{}, err
    37  		}
    38  		return imageManifest, nil
    39  	case *ocischema.DeserializedManifest:
    40  		imageManifest, err := pullManifestOCISchema(ctx, ref, repo, *v)
    41  		if err != nil {
    42  			return types.ImageManifest{}, err
    43  		}
    44  		return imageManifest, nil
    45  	case *manifestlist.DeserializedManifestList:
    46  		return types.ImageManifest{}, errors.Errorf("%s is a manifest list", ref)
    47  	}
    48  	return types.ImageManifest{}, errors.Errorf("%s is not a manifest", ref)
    49  }
    50  
    51  func fetchList(ctx context.Context, repo distribution.Repository, ref reference.Named) ([]types.ImageManifest, error) {
    52  	manifest, err := getManifest(ctx, repo, ref)
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  
    57  	switch v := manifest.(type) {
    58  	case *manifestlist.DeserializedManifestList:
    59  		imageManifests, err := pullManifestList(ctx, ref, repo, *v)
    60  		if err != nil {
    61  			return nil, err
    62  		}
    63  		return imageManifests, nil
    64  	default:
    65  		return nil, errors.Errorf("unsupported manifest format: %v", v)
    66  	}
    67  }
    68  
    69  func getManifest(ctx context.Context, repo distribution.Repository, ref reference.Named) (distribution.Manifest, error) {
    70  	manSvc, err := repo.Manifests(ctx)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	dgst, opts, err := getManifestOptionsFromReference(ref)
    76  	if err != nil {
    77  		return nil, errors.Errorf("image manifest for %q does not exist", ref)
    78  	}
    79  	return manSvc.Get(ctx, dgst, opts...)
    80  }
    81  
    82  func pullManifestSchemaV2(ctx context.Context, ref reference.Named, repo distribution.Repository, mfst schema2.DeserializedManifest) (types.ImageManifest, error) {
    83  	manifestDesc, err := validateManifestDigest(ref, mfst)
    84  	if err != nil {
    85  		return types.ImageManifest{}, err
    86  	}
    87  	configJSON, err := pullManifestSchemaV2ImageConfig(ctx, mfst.Target().Digest, repo)
    88  	if err != nil {
    89  		return types.ImageManifest{}, err
    90  	}
    91  
    92  	if manifestDesc.Platform == nil {
    93  		manifestDesc.Platform = &ocispec.Platform{}
    94  	}
    95  
    96  	// Fill in os and architecture fields from config JSON
    97  	if err := json.Unmarshal(configJSON, manifestDesc.Platform); err != nil {
    98  		return types.ImageManifest{}, err
    99  	}
   100  
   101  	return types.NewImageManifest(ref, manifestDesc, &mfst), nil
   102  }
   103  
   104  func pullManifestOCISchema(ctx context.Context, ref reference.Named, repo distribution.Repository, mfst ocischema.DeserializedManifest) (types.ImageManifest, error) {
   105  	manifestDesc, err := validateManifestDigest(ref, mfst)
   106  	if err != nil {
   107  		return types.ImageManifest{}, err
   108  	}
   109  	configJSON, err := pullManifestSchemaV2ImageConfig(ctx, mfst.Target().Digest, repo)
   110  	if err != nil {
   111  		return types.ImageManifest{}, err
   112  	}
   113  
   114  	if manifestDesc.Platform == nil {
   115  		manifestDesc.Platform = &ocispec.Platform{}
   116  	}
   117  
   118  	// Fill in os and architecture fields from config JSON
   119  	if err := json.Unmarshal(configJSON, manifestDesc.Platform); err != nil {
   120  		return types.ImageManifest{}, err
   121  	}
   122  
   123  	return types.NewOCIImageManifest(ref, manifestDesc, &mfst), nil
   124  }
   125  
   126  func pullManifestSchemaV2ImageConfig(ctx context.Context, dgst digest.Digest, repo distribution.Repository) ([]byte, error) {
   127  	blobs := repo.Blobs(ctx)
   128  	configJSON, err := blobs.Get(ctx, dgst)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  
   133  	verifier := dgst.Verifier()
   134  	if _, err := verifier.Write(configJSON); err != nil {
   135  		return nil, err
   136  	}
   137  	if !verifier.Verified() {
   138  		return nil, errors.Errorf("image config verification failed for digest %s", dgst)
   139  	}
   140  	return configJSON, nil
   141  }
   142  
   143  // validateManifestDigest computes the manifest digest, and, if pulling by
   144  // digest, ensures that it matches the requested digest.
   145  func validateManifestDigest(ref reference.Named, mfst distribution.Manifest) (ocispec.Descriptor, error) {
   146  	mediaType, canonical, err := mfst.Payload()
   147  	if err != nil {
   148  		return ocispec.Descriptor{}, err
   149  	}
   150  	desc := ocispec.Descriptor{
   151  		Digest:    digest.FromBytes(canonical),
   152  		Size:      int64(len(canonical)),
   153  		MediaType: mediaType,
   154  	}
   155  
   156  	// If pull by digest, then verify the manifest digest.
   157  	if digested, isDigested := ref.(reference.Canonical); isDigested {
   158  		if digested.Digest() != desc.Digest {
   159  			err := errors.Errorf("manifest verification failed for digest %s", digested.Digest())
   160  			return ocispec.Descriptor{}, err
   161  		}
   162  	}
   163  
   164  	return desc, nil
   165  }
   166  
   167  // pullManifestList handles "manifest lists" which point to various
   168  // platform-specific manifests.
   169  func pullManifestList(ctx context.Context, ref reference.Named, repo distribution.Repository, mfstList manifestlist.DeserializedManifestList) ([]types.ImageManifest, error) {
   170  	infos := []types.ImageManifest{}
   171  
   172  	if _, err := validateManifestDigest(ref, mfstList); err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	for _, manifestDescriptor := range mfstList.Manifests {
   177  		manSvc, err := repo.Manifests(ctx)
   178  		if err != nil {
   179  			return nil, err
   180  		}
   181  		manifest, err := manSvc.Get(ctx, manifestDescriptor.Digest)
   182  		if err != nil {
   183  			return nil, err
   184  		}
   185  
   186  		manifestRef, err := reference.WithDigest(ref, manifestDescriptor.Digest)
   187  		if err != nil {
   188  			return nil, err
   189  		}
   190  
   191  		var imageManifest types.ImageManifest
   192  		switch v := manifest.(type) {
   193  		case *schema2.DeserializedManifest:
   194  			imageManifest, err = pullManifestSchemaV2(ctx, manifestRef, repo, *v)
   195  		case *ocischema.DeserializedManifest:
   196  			imageManifest, err = pullManifestOCISchema(ctx, manifestRef, repo, *v)
   197  		default:
   198  			err = errors.Errorf("unsupported manifest type: %T", manifest)
   199  		}
   200  		if err != nil {
   201  			return nil, err
   202  		}
   203  
   204  		// Replace platform from config
   205  		imageManifest.Descriptor.Platform = types.OCIPlatform(&manifestDescriptor.Platform)
   206  
   207  		infos = append(infos, imageManifest)
   208  	}
   209  	return infos, nil
   210  }
   211  
   212  func continueOnError(err error) bool {
   213  	switch v := err.(type) {
   214  	case errcode.Errors:
   215  		if len(v) == 0 {
   216  			return true
   217  		}
   218  		return continueOnError(v[0])
   219  	case errcode.Error:
   220  		e := err.(errcode.Error)
   221  		switch e.Code {
   222  		case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown, v2.ErrorCodeNameUnknown:
   223  			return true
   224  		}
   225  		return false
   226  	case *distclient.UnexpectedHTTPResponseError:
   227  		return true
   228  	}
   229  	return false
   230  }
   231  
   232  func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named, each func(context.Context, distribution.Repository, reference.Named) (bool, error)) error {
   233  	endpoints, err := allEndpoints(namedRef, c.insecureRegistry)
   234  	if err != nil {
   235  		return err
   236  	}
   237  
   238  	repoInfo, err := registry.ParseRepositoryInfo(namedRef)
   239  	if err != nil {
   240  		return err
   241  	}
   242  
   243  	confirmedTLSRegistries := make(map[string]bool)
   244  	for _, endpoint := range endpoints {
   245  		if endpoint.Version == registry.APIVersion1 {
   246  			logrus.Debugf("skipping v1 endpoint %s", endpoint.URL)
   247  			continue
   248  		}
   249  
   250  		if endpoint.URL.Scheme != "https" {
   251  			if _, confirmedTLS := confirmedTLSRegistries[endpoint.URL.Host]; confirmedTLS {
   252  				logrus.Debugf("skipping non-TLS endpoint %s for host/port that appears to use TLS", endpoint.URL)
   253  				continue
   254  			}
   255  		}
   256  
   257  		if c.insecureRegistry {
   258  			endpoint.TLSConfig.InsecureSkipVerify = true
   259  		}
   260  		repoEndpoint := repositoryEndpoint{endpoint: endpoint, info: repoInfo}
   261  		repo, err := c.getRepositoryForReference(ctx, namedRef, repoEndpoint)
   262  		if err != nil {
   263  			logrus.Debugf("error %s with repo endpoint %+v", err, repoEndpoint)
   264  			if _, ok := err.(ErrHTTPProto); ok {
   265  				continue
   266  			}
   267  			return err
   268  		}
   269  
   270  		if endpoint.URL.Scheme == "http" && !c.insecureRegistry {
   271  			logrus.Debugf("skipping non-tls registry endpoint: %s", endpoint.URL)
   272  			continue
   273  		}
   274  		done, err := each(ctx, repo, namedRef)
   275  		if err != nil {
   276  			if continueOnError(err) {
   277  				if endpoint.URL.Scheme == "https" {
   278  					confirmedTLSRegistries[endpoint.URL.Host] = true
   279  				}
   280  				logrus.Debugf("continuing on error (%T) %s", err, err)
   281  				continue
   282  			}
   283  			logrus.Debugf("not continuing on error (%T) %s", err, err)
   284  			return err
   285  		}
   286  		if done {
   287  			return nil
   288  		}
   289  	}
   290  	return newNotFoundError(namedRef.String())
   291  }
   292  
   293  // allEndpoints returns a list of endpoints ordered by priority (v2, https, v1).
   294  func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoint, error) {
   295  	repoInfo, err := registry.ParseRepositoryInfo(namedRef)
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  
   300  	var serviceOpts registry.ServiceOptions
   301  	if insecure {
   302  		logrus.Debugf("allowing insecure registry for: %s", reference.Domain(namedRef))
   303  		serviceOpts.InsecureRegistries = []string{reference.Domain(namedRef)}
   304  	}
   305  	registryService, err := registry.NewService(serviceOpts)
   306  	if err != nil {
   307  		return []registry.APIEndpoint{}, err
   308  	}
   309  	endpoints, err := registryService.LookupPullEndpoints(reference.Domain(repoInfo.Name))
   310  	logrus.Debugf("endpoints for %s: %v", namedRef, endpoints)
   311  	return endpoints, err
   312  }
   313  
   314  func newNotFoundError(ref string) *notFoundError {
   315  	return &notFoundError{err: errors.New("no such manifest: " + ref)}
   316  }
   317  
   318  type notFoundError struct {
   319  	err error
   320  }
   321  
   322  func (n *notFoundError) Error() string {
   323  	return n.err.Error()
   324  }
   325  
   326  // NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
   327  func (n *notFoundError) NotFound() {}