github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/registry/client/fetcher.go (about)

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