github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/distribution/manifest.go (about)

     1  package distribution
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/containerd/containerd/content"
    12  	cerrdefs "github.com/containerd/containerd/errdefs"
    13  	"github.com/containerd/containerd/remotes"
    14  	"github.com/containerd/log"
    15  	"github.com/distribution/reference"
    16  	"github.com/docker/distribution"
    17  	"github.com/docker/distribution/manifest/manifestlist"
    18  	"github.com/docker/distribution/manifest/schema1"
    19  	"github.com/docker/distribution/manifest/schema2"
    20  	"github.com/docker/docker/registry"
    21  	"github.com/opencontainers/go-digest"
    22  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    23  	"github.com/pkg/errors"
    24  )
    25  
    26  // labelDistributionSource describes the source blob comes from.
    27  const labelDistributionSource = "containerd.io/distribution.source"
    28  
    29  // This is used by manifestStore to pare down the requirements to implement a
    30  // full distribution.ManifestService, since `Get` is all we use here.
    31  type manifestGetter interface {
    32  	Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error)
    33  	Exists(ctx context.Context, dgst digest.Digest) (bool, error)
    34  }
    35  
    36  type manifestStore struct {
    37  	local  ContentStore
    38  	remote manifestGetter
    39  }
    40  
    41  // ContentStore is the interface used to persist registry blobs
    42  //
    43  // Currently this is only used to persist manifests and manifest lists.
    44  // It is exported because `distribution.Pull` takes one as an argument.
    45  type ContentStore interface {
    46  	content.Ingester
    47  	content.Provider
    48  	Info(ctx context.Context, dgst digest.Digest) (content.Info, error)
    49  	Abort(ctx context.Context, ref string) error
    50  	Update(ctx context.Context, info content.Info, fieldpaths ...string) (content.Info, error)
    51  }
    52  
    53  func makeDistributionSourceLabel(ref reference.Named) (string, string) {
    54  	domain := reference.Domain(ref)
    55  	if domain == "" {
    56  		domain = registry.DefaultNamespace
    57  	}
    58  	repo := reference.Path(ref)
    59  
    60  	return fmt.Sprintf("%s.%s", labelDistributionSource, domain), repo
    61  }
    62  
    63  // Taken from https://github.com/containerd/containerd/blob/e079e4a155c86f07bbd602fe6753ecacc78198c2/remotes/docker/handler.go#L84-L108
    64  func appendDistributionSourceLabel(originLabel, repo string) string {
    65  	repos := []string{}
    66  	if originLabel != "" {
    67  		repos = strings.Split(originLabel, ",")
    68  	}
    69  	repos = append(repos, repo)
    70  
    71  	// use empty string to present duplicate items
    72  	for i := 1; i < len(repos); i++ {
    73  		tmp, j := repos[i], i-1
    74  		for ; j >= 0 && repos[j] >= tmp; j-- {
    75  			if repos[j] == tmp {
    76  				tmp = ""
    77  			}
    78  			repos[j+1] = repos[j]
    79  		}
    80  		repos[j+1] = tmp
    81  	}
    82  
    83  	i := 0
    84  	for ; i < len(repos) && repos[i] == ""; i++ {
    85  	}
    86  
    87  	return strings.Join(repos[i:], ",")
    88  }
    89  
    90  func hasDistributionSource(label, repo string) bool {
    91  	sources := strings.Split(label, ",")
    92  	for _, s := range sources {
    93  		if s == repo {
    94  			return true
    95  		}
    96  	}
    97  	return false
    98  }
    99  
   100  func (m *manifestStore) getLocal(ctx context.Context, desc ocispec.Descriptor, ref reference.Named) (distribution.Manifest, error) {
   101  	ra, err := m.local.ReaderAt(ctx, desc)
   102  	if err != nil {
   103  		return nil, errors.Wrap(err, "error getting content store reader")
   104  	}
   105  	defer ra.Close()
   106  
   107  	distKey, distRepo := makeDistributionSourceLabel(ref)
   108  	info, err := m.local.Info(ctx, desc.Digest)
   109  	if err != nil {
   110  		return nil, errors.Wrap(err, "error getting content info")
   111  	}
   112  
   113  	if _, ok := ref.(reference.Canonical); ok {
   114  		// Since this is specified by digest...
   115  		// We know we have the content locally, we need to check if we've seen this content at the specified repository before.
   116  		// If we have, we can just return the manifest from the local content store.
   117  		// If we haven't, we need to check the remote repository to see if it has the content, otherwise we can end up returning
   118  		// a manifest that has never even existed in the remote before.
   119  		if !hasDistributionSource(info.Labels[distKey], distRepo) {
   120  			log.G(ctx).WithField("ref", ref).Debug("found manifest but no mataching source repo is listed, checking with remote")
   121  			exists, err := m.remote.Exists(ctx, desc.Digest)
   122  			if err != nil {
   123  				return nil, errors.Wrap(err, "error checking if remote exists")
   124  			}
   125  
   126  			if !exists {
   127  				return nil, errors.Wrapf(cerrdefs.ErrNotFound, "manifest %v not found", desc.Digest)
   128  			}
   129  
   130  		}
   131  	}
   132  
   133  	// Update the distribution sources since we now know the content exists in the remote.
   134  	if info.Labels == nil {
   135  		info.Labels = map[string]string{}
   136  	}
   137  	info.Labels[distKey] = appendDistributionSourceLabel(info.Labels[distKey], distRepo)
   138  	if _, err := m.local.Update(ctx, info, "labels."+distKey); err != nil {
   139  		log.G(ctx).WithError(err).WithField("ref", ref).Warn("Could not update content distribution source")
   140  	}
   141  
   142  	r := io.NewSectionReader(ra, 0, ra.Size())
   143  	data, err := io.ReadAll(r)
   144  	if err != nil {
   145  		return nil, errors.Wrap(err, "error reading manifest from content store")
   146  	}
   147  
   148  	manifest, _, err := distribution.UnmarshalManifest(desc.MediaType, data)
   149  	if err != nil {
   150  		return nil, errors.Wrap(err, "error unmarshaling manifest from content store")
   151  	}
   152  
   153  	return manifest, nil
   154  }
   155  
   156  func (m *manifestStore) getMediaType(ctx context.Context, desc ocispec.Descriptor) (string, error) {
   157  	ra, err := m.local.ReaderAt(ctx, desc)
   158  	if err != nil {
   159  		return "", errors.Wrap(err, "error getting reader to detect media type")
   160  	}
   161  	defer ra.Close()
   162  
   163  	mt, err := detectManifestMediaType(ra)
   164  	if err != nil {
   165  		return "", errors.Wrap(err, "error detecting media type")
   166  	}
   167  	return mt, nil
   168  }
   169  
   170  func (m *manifestStore) Get(ctx context.Context, desc ocispec.Descriptor, ref reference.Named) (distribution.Manifest, error) {
   171  	l := log.G(ctx)
   172  
   173  	if desc.MediaType == "" {
   174  		// When pulling by digest we will not have the media type on the
   175  		// descriptor since we have not made a request to the registry yet
   176  		//
   177  		// We already have the digest, so we only lookup locally... by digest.
   178  		//
   179  		// Let's try to detect the media type so we can have a good ref key
   180  		// here. We may not even have the content locally, and this is fine, but
   181  		// if we do we should determine that.
   182  		mt, err := m.getMediaType(ctx, desc)
   183  		if err != nil && !cerrdefs.IsNotFound(err) {
   184  			l.WithError(err).Warn("Error looking up media type of content")
   185  		}
   186  		desc.MediaType = mt
   187  	}
   188  
   189  	key := remotes.MakeRefKey(ctx, desc)
   190  
   191  	// Here we open a writer to the requested content. This both gives us a
   192  	// reference to write to if indeed we need to persist it and increments the
   193  	// ref count on the content.
   194  	w, err := m.local.Writer(ctx, content.WithDescriptor(desc), content.WithRef(key))
   195  	if err != nil {
   196  		if cerrdefs.IsAlreadyExists(err) {
   197  			var manifest distribution.Manifest
   198  			if manifest, err = m.getLocal(ctx, desc, ref); err == nil {
   199  				return manifest, nil
   200  			}
   201  		}
   202  		// always fallback to the remote if there is an error with the local store
   203  	}
   204  	if w != nil {
   205  		defer w.Close()
   206  	}
   207  
   208  	l.WithError(err).Debug("Fetching manifest from remote")
   209  
   210  	manifest, err := m.remote.Get(ctx, desc.Digest)
   211  	if err != nil {
   212  		if err := m.local.Abort(ctx, key); err != nil {
   213  			l.WithError(err).Warn("Error while attempting to abort content ingest")
   214  		}
   215  		return nil, err
   216  	}
   217  
   218  	if w != nil {
   219  		// if `w` is nil here, something happened with the content store, so don't bother trying to persist.
   220  		if err := m.Put(ctx, manifest, desc, w, ref); err != nil {
   221  			if err := m.local.Abort(ctx, key); err != nil {
   222  				l.WithError(err).Warn("error aborting content ingest")
   223  			}
   224  			l.WithError(err).Warn("Error persisting manifest")
   225  		}
   226  	}
   227  	return manifest, nil
   228  }
   229  
   230  func (m *manifestStore) Put(ctx context.Context, manifest distribution.Manifest, desc ocispec.Descriptor, w content.Writer, ref reference.Named) error {
   231  	mt, payload, err := manifest.Payload()
   232  	if err != nil {
   233  		return err
   234  	}
   235  	desc.Size = int64(len(payload))
   236  	desc.MediaType = mt
   237  
   238  	if _, err = w.Write(payload); err != nil {
   239  		return errors.Wrap(err, "error writing manifest to content store")
   240  	}
   241  
   242  	distKey, distSource := makeDistributionSourceLabel(ref)
   243  	if err := w.Commit(ctx, desc.Size, desc.Digest, content.WithLabels(map[string]string{
   244  		distKey: distSource,
   245  	})); err != nil {
   246  		return errors.Wrap(err, "error committing manifest to content store")
   247  	}
   248  	return nil
   249  }
   250  
   251  func detectManifestMediaType(ra content.ReaderAt) (string, error) {
   252  	dt := make([]byte, ra.Size())
   253  	if _, err := ra.ReadAt(dt, 0); err != nil {
   254  		return "", err
   255  	}
   256  
   257  	return detectManifestBlobMediaType(dt)
   258  }
   259  
   260  // This is used when the manifest store does not know the media type of a sha it
   261  // was told to get. This would currently only happen when pulling by digest.
   262  // The media type is needed so the blob can be unmarshalled properly.
   263  func detectManifestBlobMediaType(dt []byte) (string, error) {
   264  	var mfst struct {
   265  		MediaType string          `json:"mediaType"`
   266  		Manifests json.RawMessage `json:"manifests"` // oci index, manifest list
   267  		Config    json.RawMessage `json:"config"`    // schema2 Manifest
   268  		Layers    json.RawMessage `json:"layers"`    // schema2 Manifest
   269  		FSLayers  json.RawMessage `json:"fsLayers"`  // schema1 Manifest
   270  	}
   271  
   272  	if err := json.Unmarshal(dt, &mfst); err != nil {
   273  		return "", err
   274  	}
   275  
   276  	// We may have a media type specified in the json, in which case that should be used.
   277  	// Docker types should generally have a media type set.
   278  	// OCI (golang) types do not have a `mediaType` defined, and it is optional in the spec.
   279  	//
   280  	// `distribution.UnmarshalManifest`, which is used to unmarshal this for real, checks these media type values.
   281  	// If the specified media type does not match it will error, and in some cases (docker media types) it is required.
   282  	// So pretty much if we don't have a media type we can fall back to OCI.
   283  	// This does have a special fallback for schema1 manifests just because it is easy to detect.
   284  	switch mfst.MediaType {
   285  	case schema2.MediaTypeManifest, ocispec.MediaTypeImageManifest:
   286  		if mfst.Manifests != nil || mfst.FSLayers != nil {
   287  			return "", fmt.Errorf(`media-type: %q should not have "manifests" or "fsLayers"`, mfst.MediaType)
   288  		}
   289  		return mfst.MediaType, nil
   290  	case manifestlist.MediaTypeManifestList, ocispec.MediaTypeImageIndex:
   291  		if mfst.Config != nil || mfst.Layers != nil || mfst.FSLayers != nil {
   292  			return "", fmt.Errorf(`media-type: %q should not have "config", "layers", or "fsLayers"`, mfst.MediaType)
   293  		}
   294  		return mfst.MediaType, nil
   295  	case schema1.MediaTypeManifest:
   296  		if os.Getenv("DOCKER_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE") == "" {
   297  			err := DeprecatedSchema1ImageError(nil)
   298  			log.G(context.TODO()).Warn(err.Error())
   299  			return "", err
   300  		}
   301  		if mfst.Manifests != nil || mfst.Layers != nil {
   302  			return "", fmt.Errorf(`media-type: %q should not have "manifests" or "layers"`, mfst.MediaType)
   303  		}
   304  		return mfst.MediaType, nil
   305  	default:
   306  		if mfst.MediaType != "" {
   307  			return mfst.MediaType, nil
   308  		}
   309  	}
   310  	switch {
   311  	case mfst.FSLayers != nil && mfst.Manifests == nil && mfst.Layers == nil && mfst.Config == nil:
   312  		if os.Getenv("DOCKER_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE") == "" {
   313  			err := DeprecatedSchema1ImageError(nil)
   314  			log.G(context.TODO()).Warn(err.Error())
   315  			return "", err
   316  		}
   317  		return schema1.MediaTypeManifest, nil
   318  	case mfst.Config != nil && mfst.Manifests == nil && mfst.FSLayers == nil,
   319  		mfst.Layers != nil && mfst.Manifests == nil && mfst.FSLayers == nil:
   320  		return ocispec.MediaTypeImageManifest, nil
   321  	case mfst.Config == nil && mfst.Layers == nil && mfst.FSLayers == nil:
   322  		// fallback to index
   323  		return ocispec.MediaTypeImageIndex, nil
   324  	}
   325  	return "", errors.New("media-type: cannot determine")
   326  }