github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/engine/distribution/manifest_test.go (about)

     1  package distribution
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"os"
     7  	"strings"
     8  	"sync"
     9  	"testing"
    10  
    11  	"github.com/containerd/containerd/content"
    12  	"github.com/containerd/containerd/content/local"
    13  	"github.com/containerd/containerd/errdefs"
    14  	"github.com/containerd/containerd/remotes"
    15  	"github.com/docker/distribution"
    16  	"github.com/docker/distribution/manifest/manifestlist"
    17  	"github.com/docker/distribution/manifest/ocischema"
    18  	"github.com/docker/distribution/manifest/schema1"
    19  	"github.com/docker/distribution/manifest/schema2"
    20  	"github.com/docker/distribution/reference"
    21  	"github.com/google/go-cmp/cmp/cmpopts"
    22  	digest "github.com/opencontainers/go-digest"
    23  	specs "github.com/opencontainers/image-spec/specs-go/v1"
    24  	"github.com/pkg/errors"
    25  	"gotest.tools/v3/assert"
    26  	"gotest.tools/v3/assert/cmp"
    27  )
    28  
    29  type mockManifestGetter struct {
    30  	manifests map[digest.Digest]distribution.Manifest
    31  	gets      int
    32  }
    33  
    34  func (m *mockManifestGetter) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) {
    35  	m.gets++
    36  	manifest, ok := m.manifests[dgst]
    37  	if !ok {
    38  		return nil, distribution.ErrManifestUnknown{Tag: dgst.String()}
    39  	}
    40  	return manifest, nil
    41  }
    42  
    43  func (m *mockManifestGetter) Exists(ctx context.Context, dgst digest.Digest) (bool, error) {
    44  	_, ok := m.manifests[dgst]
    45  	return ok, nil
    46  }
    47  
    48  type memoryLabelStore struct {
    49  	mu     sync.Mutex
    50  	labels map[digest.Digest]map[string]string
    51  }
    52  
    53  // Get returns all the labels for the given digest
    54  func (s *memoryLabelStore) Get(dgst digest.Digest) (map[string]string, error) {
    55  	s.mu.Lock()
    56  	labels := s.labels[dgst]
    57  	s.mu.Unlock()
    58  	return labels, nil
    59  }
    60  
    61  // Set sets all the labels for a given digest
    62  func (s *memoryLabelStore) Set(dgst digest.Digest, labels map[string]string) error {
    63  	s.mu.Lock()
    64  	if s.labels == nil {
    65  		s.labels = make(map[digest.Digest]map[string]string)
    66  	}
    67  	s.labels[dgst] = labels
    68  	s.mu.Unlock()
    69  	return nil
    70  }
    71  
    72  // Update replaces the given labels for a digest,
    73  // a key with an empty value removes a label.
    74  func (s *memoryLabelStore) Update(dgst digest.Digest, update map[string]string) (map[string]string, error) {
    75  	s.mu.Lock()
    76  	defer s.mu.Unlock()
    77  
    78  	labels, ok := s.labels[dgst]
    79  	if !ok {
    80  		labels = map[string]string{}
    81  	}
    82  	for k, v := range update {
    83  		labels[k] = v
    84  	}
    85  	if s.labels == nil {
    86  		s.labels = map[digest.Digest]map[string]string{}
    87  	}
    88  	s.labels[dgst] = labels
    89  
    90  	return labels, nil
    91  }
    92  
    93  type testingContentStoreWrapper struct {
    94  	ContentStore
    95  	errorOnWriter error
    96  	errorOnCommit error
    97  }
    98  
    99  func (s *testingContentStoreWrapper) Writer(ctx context.Context, opts ...content.WriterOpt) (content.Writer, error) {
   100  	if s.errorOnWriter != nil {
   101  		return nil, s.errorOnWriter
   102  	}
   103  
   104  	w, err := s.ContentStore.Writer(ctx, opts...)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	if s.errorOnCommit != nil {
   110  		w = &testingContentWriterWrapper{w, s.errorOnCommit}
   111  	}
   112  	return w, nil
   113  }
   114  
   115  type testingContentWriterWrapper struct {
   116  	content.Writer
   117  	err error
   118  }
   119  
   120  func (w *testingContentWriterWrapper) Commit(ctx context.Context, size int64, dgst digest.Digest, opts ...content.Opt) error {
   121  	if w.err != nil {
   122  		// The contract for `Commit` is to always close.
   123  		// Since this is returning early before hitting the real `Commit`, we should close it here.
   124  		w.Close()
   125  		return w.err
   126  	}
   127  	return w.Writer.Commit(ctx, size, dgst, opts...)
   128  }
   129  
   130  func TestManifestStore(t *testing.T) {
   131  	ociManifest := &specs.Manifest{}
   132  	serialized, err := json.Marshal(ociManifest)
   133  	assert.NilError(t, err)
   134  	dgst := digest.Canonical.FromBytes(serialized)
   135  
   136  	setupTest := func(t *testing.T) (reference.Named, specs.Descriptor, *mockManifestGetter, *manifestStore, content.Store, func(*testing.T)) {
   137  		root, err := os.MkdirTemp("", strings.Replace(t.Name(), "/", "_", -1))
   138  		assert.NilError(t, err)
   139  		defer func() {
   140  			if t.Failed() {
   141  				os.RemoveAll(root)
   142  			}
   143  		}()
   144  
   145  		cs, err := local.NewLabeledStore(root, &memoryLabelStore{})
   146  		assert.NilError(t, err)
   147  
   148  		mg := &mockManifestGetter{manifests: make(map[digest.Digest]distribution.Manifest)}
   149  		store := &manifestStore{local: cs, remote: mg}
   150  		desc := specs.Descriptor{Digest: dgst, MediaType: specs.MediaTypeImageManifest, Size: int64(len(serialized))}
   151  
   152  		ref, err := reference.Parse("foo/bar")
   153  		assert.NilError(t, err)
   154  
   155  		return ref.(reference.Named), desc, mg, store, cs, func(t *testing.T) {
   156  			assert.Check(t, os.RemoveAll(root))
   157  		}
   158  	}
   159  
   160  	ctx := context.Background()
   161  
   162  	m, _, err := distribution.UnmarshalManifest(specs.MediaTypeImageManifest, serialized)
   163  	assert.NilError(t, err)
   164  
   165  	writeManifest := func(t *testing.T, cs ContentStore, desc specs.Descriptor, opts ...content.Opt) {
   166  		ingestKey := remotes.MakeRefKey(ctx, desc)
   167  		w, err := cs.Writer(ctx, content.WithDescriptor(desc), content.WithRef(ingestKey))
   168  		assert.NilError(t, err)
   169  		defer func() {
   170  			if err := w.Close(); err != nil {
   171  				t.Log(err)
   172  			}
   173  			if t.Failed() {
   174  				if err := cs.Abort(ctx, ingestKey); err != nil {
   175  					t.Log(err)
   176  				}
   177  			}
   178  		}()
   179  
   180  		_, err = w.Write(serialized)
   181  		assert.NilError(t, err)
   182  
   183  		err = w.Commit(ctx, desc.Size, desc.Digest, opts...)
   184  		assert.NilError(t, err)
   185  
   186  	}
   187  
   188  	// All tests should end up with no active ingest
   189  	checkIngest := func(t *testing.T, cs content.Store, desc specs.Descriptor) {
   190  		ingestKey := remotes.MakeRefKey(ctx, desc)
   191  		_, err := cs.Status(ctx, ingestKey)
   192  		assert.Check(t, errdefs.IsNotFound(err), err)
   193  	}
   194  
   195  	t.Run("no remote or local", func(t *testing.T) {
   196  		ref, desc, _, store, cs, teardown := setupTest(t)
   197  		defer teardown(t)
   198  
   199  		_, err = store.Get(ctx, desc, ref)
   200  		checkIngest(t, cs, desc)
   201  		// This error is what our digest getter returns when it doesn't know about the manifest
   202  		assert.Error(t, err, distribution.ErrManifestUnknown{Tag: dgst.String()}.Error())
   203  	})
   204  
   205  	t.Run("no local cache", func(t *testing.T) {
   206  		ref, desc, mg, store, cs, teardown := setupTest(t)
   207  		defer teardown(t)
   208  
   209  		mg.manifests[desc.Digest] = m
   210  
   211  		m2, err := store.Get(ctx, desc, ref)
   212  		checkIngest(t, cs, desc)
   213  		assert.NilError(t, err)
   214  		assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   215  		assert.Check(t, cmp.Equal(mg.gets, 1))
   216  
   217  		i, err := cs.Info(ctx, desc.Digest)
   218  		assert.NilError(t, err)
   219  		assert.Check(t, cmp.Equal(i.Digest, desc.Digest))
   220  
   221  		distKey, distSource := makeDistributionSourceLabel(ref)
   222  		assert.Check(t, hasDistributionSource(i.Labels[distKey], distSource))
   223  
   224  		// Now check again, this should not hit the remote
   225  		m2, err = store.Get(ctx, desc, ref)
   226  		checkIngest(t, cs, desc)
   227  		assert.NilError(t, err)
   228  		assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   229  		assert.Check(t, cmp.Equal(mg.gets, 1))
   230  
   231  		t.Run("digested", func(t *testing.T) {
   232  			ref, err := reference.WithDigest(ref, desc.Digest)
   233  			assert.NilError(t, err)
   234  
   235  			_, err = store.Get(ctx, desc, ref)
   236  			assert.NilError(t, err)
   237  		})
   238  	})
   239  
   240  	t.Run("with local cache", func(t *testing.T) {
   241  		ref, desc, mg, store, cs, teardown := setupTest(t)
   242  		defer teardown(t)
   243  
   244  		// first add the manifest to the coontent store
   245  		writeManifest(t, cs, desc)
   246  
   247  		// now do the get
   248  		m2, err := store.Get(ctx, desc, ref)
   249  		checkIngest(t, cs, desc)
   250  		assert.NilError(t, err)
   251  		assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   252  		assert.Check(t, cmp.Equal(mg.gets, 0))
   253  
   254  		i, err := cs.Info(ctx, desc.Digest)
   255  		assert.NilError(t, err)
   256  		assert.Check(t, cmp.Equal(i.Digest, desc.Digest))
   257  	})
   258  
   259  	// This is for the case of pull by digest where we don't know the media type of the manifest until it's actually pulled.
   260  	t.Run("unknown media type", func(t *testing.T) {
   261  		t.Run("no cache", func(t *testing.T) {
   262  			ref, desc, mg, store, cs, teardown := setupTest(t)
   263  			defer teardown(t)
   264  
   265  			mg.manifests[desc.Digest] = m
   266  			desc.MediaType = ""
   267  
   268  			m2, err := store.Get(ctx, desc, ref)
   269  			checkIngest(t, cs, desc)
   270  			assert.NilError(t, err)
   271  			assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   272  			assert.Check(t, cmp.Equal(mg.gets, 1))
   273  		})
   274  
   275  		t.Run("with cache", func(t *testing.T) {
   276  			t.Run("cached manifest has media type", func(t *testing.T) {
   277  				ref, desc, mg, store, cs, teardown := setupTest(t)
   278  				defer teardown(t)
   279  
   280  				writeManifest(t, cs, desc)
   281  				desc.MediaType = ""
   282  
   283  				m2, err := store.Get(ctx, desc, ref)
   284  				checkIngest(t, cs, desc)
   285  				assert.NilError(t, err)
   286  				assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   287  				assert.Check(t, cmp.Equal(mg.gets, 0))
   288  			})
   289  
   290  			t.Run("cached manifest has no media type", func(t *testing.T) {
   291  				ref, desc, mg, store, cs, teardown := setupTest(t)
   292  				defer teardown(t)
   293  
   294  				desc.MediaType = ""
   295  				writeManifest(t, cs, desc)
   296  
   297  				m2, err := store.Get(ctx, desc, ref)
   298  				checkIngest(t, cs, desc)
   299  				assert.NilError(t, err)
   300  				assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   301  				assert.Check(t, cmp.Equal(mg.gets, 0))
   302  			})
   303  		})
   304  	})
   305  
   306  	// Test that if there is an error with the content store, for whatever
   307  	// reason, that doesn't stop us from getting the manifest.
   308  	//
   309  	// Also makes sure the ingests are aborted.
   310  	t.Run("error persisting manifest", func(t *testing.T) {
   311  		t.Run("error on writer", func(t *testing.T) {
   312  			ref, desc, mg, store, cs, teardown := setupTest(t)
   313  			defer teardown(t)
   314  			mg.manifests[desc.Digest] = m
   315  
   316  			csW := &testingContentStoreWrapper{ContentStore: store.local, errorOnWriter: errors.New("random error")}
   317  			store.local = csW
   318  
   319  			m2, err := store.Get(ctx, desc, ref)
   320  			checkIngest(t, cs, desc)
   321  			assert.NilError(t, err)
   322  			assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   323  			assert.Check(t, cmp.Equal(mg.gets, 1))
   324  
   325  			_, err = cs.Info(ctx, desc.Digest)
   326  			// Nothing here since we couldn't persist
   327  			assert.Check(t, errdefs.IsNotFound(err), err)
   328  		})
   329  
   330  		t.Run("error on commit", func(t *testing.T) {
   331  			ref, desc, mg, store, cs, teardown := setupTest(t)
   332  			defer teardown(t)
   333  			mg.manifests[desc.Digest] = m
   334  
   335  			csW := &testingContentStoreWrapper{ContentStore: store.local, errorOnCommit: errors.New("random error")}
   336  			store.local = csW
   337  
   338  			m2, err := store.Get(ctx, desc, ref)
   339  			checkIngest(t, cs, desc)
   340  			assert.NilError(t, err)
   341  			assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   342  			assert.Check(t, cmp.Equal(mg.gets, 1))
   343  
   344  			_, err = cs.Info(ctx, desc.Digest)
   345  			// Nothing here since we couldn't persist
   346  			assert.Check(t, errdefs.IsNotFound(err), err)
   347  		})
   348  	})
   349  }
   350  
   351  func TestDetectManifestBlobMediaType(t *testing.T) {
   352  	type testCase struct {
   353  		json     []byte
   354  		expected string
   355  	}
   356  	cases := map[string]testCase{
   357  		"mediaType is set":   {[]byte(`{"mediaType": "bananas"}`), "bananas"},
   358  		"oci manifest":       {[]byte(`{"config": {}}`), specs.MediaTypeImageManifest},
   359  		"schema1":            {[]byte(`{"fsLayers": []}`), schema1.MediaTypeManifest},
   360  		"oci index fallback": {[]byte(`{}`), specs.MediaTypeImageIndex},
   361  		// Make sure we prefer mediaType
   362  		"mediaType and config set":   {[]byte(`{"mediaType": "bananas", "config": {}}`), "bananas"},
   363  		"mediaType and fsLayers set": {[]byte(`{"mediaType": "bananas", "fsLayers": []}`), "bananas"},
   364  	}
   365  
   366  	for name, tc := range cases {
   367  		t.Run(name, func(t *testing.T) {
   368  			mt, err := detectManifestBlobMediaType(tc.json)
   369  			assert.NilError(t, err)
   370  			assert.Equal(t, mt, tc.expected)
   371  		})
   372  	}
   373  
   374  }
   375  
   376  func TestDetectManifestBlobMediaTypeInvalid(t *testing.T) {
   377  	type testCase struct {
   378  		json     []byte
   379  		expected string
   380  	}
   381  	cases := map[string]testCase{
   382  		"schema 1 mediaType with manifests": {
   383  			[]byte(`{"mediaType": "` + schema1.MediaTypeManifest + `","manifests":[]}`),
   384  			`media-type: "application/vnd.docker.distribution.manifest.v1+json" should not have "manifests" or "layers"`,
   385  		},
   386  		"schema 1 mediaType with layers": {
   387  			[]byte(`{"mediaType": "` + schema1.MediaTypeManifest + `","layers":[]}`),
   388  			`media-type: "application/vnd.docker.distribution.manifest.v1+json" should not have "manifests" or "layers"`,
   389  		},
   390  		"schema 2 mediaType with manifests": {
   391  			[]byte(`{"mediaType": "` + schema2.MediaTypeManifest + `","manifests":[]}`),
   392  			`media-type: "application/vnd.docker.distribution.manifest.v2+json" should not have "manifests" or "fsLayers"`,
   393  		},
   394  		"schema 2 mediaType with fsLayers": {
   395  			[]byte(`{"mediaType": "` + schema2.MediaTypeManifest + `","fsLayers":[]}`),
   396  			`media-type: "application/vnd.docker.distribution.manifest.v2+json" should not have "manifests" or "fsLayers"`,
   397  		},
   398  		"oci manifest mediaType with manifests": {
   399  			[]byte(`{"mediaType": "` + specs.MediaTypeImageManifest + `","manifests":[]}`),
   400  			`media-type: "application/vnd.oci.image.manifest.v1+json" should not have "manifests" or "fsLayers"`,
   401  		},
   402  		"manifest list mediaType with fsLayers": {
   403  			[]byte(`{"mediaType": "` + manifestlist.MediaTypeManifestList + `","fsLayers":[]}`),
   404  			`media-type: "application/vnd.docker.distribution.manifest.list.v2+json" should not have "config", "layers", or "fsLayers"`,
   405  		},
   406  		"index mediaType with layers": {
   407  			[]byte(`{"mediaType": "` + specs.MediaTypeImageIndex + `","layers":[]}`),
   408  			`media-type: "application/vnd.oci.image.index.v1+json" should not have "config", "layers", or "fsLayers"`,
   409  		},
   410  		"index mediaType with config": {
   411  			[]byte(`{"mediaType": "` + specs.MediaTypeImageIndex + `","config":{}}`),
   412  			`media-type: "application/vnd.oci.image.index.v1+json" should not have "config", "layers", or "fsLayers"`,
   413  		},
   414  		"config and manifests": {
   415  			[]byte(`{"config":{}, "manifests":[]}`),
   416  			`media-type: cannot determine`,
   417  		},
   418  		"layers and manifests": {
   419  			[]byte(`{"layers":[], "manifests":[]}`),
   420  			`media-type: cannot determine`,
   421  		},
   422  		"layers and fsLayers": {
   423  			[]byte(`{"layers":[], "fsLayers":[]}`),
   424  			`media-type: cannot determine`,
   425  		},
   426  		"fsLayers and manifests": {
   427  			[]byte(`{"fsLayers":[], "manifests":[]}`),
   428  			`media-type: cannot determine`,
   429  		},
   430  		"config and fsLayers": {
   431  			[]byte(`{"config":{}, "fsLayers":[]}`),
   432  			`media-type: cannot determine`,
   433  		},
   434  	}
   435  
   436  	for name, tc := range cases {
   437  		t.Run(name, func(t *testing.T) {
   438  			mt, err := detectManifestBlobMediaType(tc.json)
   439  			assert.Error(t, err, tc.expected)
   440  			assert.Equal(t, mt, "")
   441  		})
   442  	}
   443  
   444  }