github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/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  	cerrdefs "github.com/containerd/containerd/errdefs"
    14  	"github.com/containerd/containerd/remotes"
    15  	"github.com/distribution/reference"
    16  	"github.com/docker/distribution"
    17  	"github.com/docker/distribution/manifest/manifestlist"
    18  	"github.com/docker/distribution/manifest/ocischema"
    19  	"github.com/docker/distribution/manifest/schema1"
    20  	"github.com/docker/distribution/manifest/schema2"
    21  	"github.com/google/go-cmp/cmp/cmpopts"
    22  	"github.com/opencontainers/go-digest"
    23  	ocispec "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 := &ocispec.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, ocispec.Descriptor, *mockManifestGetter, *manifestStore, content.Store, func(*testing.T)) {
   137  		root, err := os.MkdirTemp("", strings.ReplaceAll(t.Name(), "/", "_"))
   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 := ocispec.Descriptor{Digest: dgst, MediaType: ocispec.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(ocispec.MediaTypeImageManifest, serialized)
   163  	assert.NilError(t, err)
   164  
   165  	writeManifest := func(t *testing.T, cs ContentStore, desc ocispec.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  	// All tests should end up with no active ingest
   188  	checkIngest := func(t *testing.T, cs content.Store, desc ocispec.Descriptor) {
   189  		ingestKey := remotes.MakeRefKey(ctx, desc)
   190  		_, err := cs.Status(ctx, ingestKey)
   191  		assert.Check(t, cerrdefs.IsNotFound(err), err)
   192  	}
   193  
   194  	t.Run("no remote or local", func(t *testing.T) {
   195  		ref, desc, _, store, cs, teardown := setupTest(t)
   196  		defer teardown(t)
   197  
   198  		_, err = store.Get(ctx, desc, ref)
   199  		checkIngest(t, cs, desc)
   200  		// This error is what our digest getter returns when it doesn't know about the manifest
   201  		assert.Error(t, err, distribution.ErrManifestUnknown{Tag: dgst.String()}.Error())
   202  	})
   203  
   204  	t.Run("no local cache", func(t *testing.T) {
   205  		ref, desc, mg, store, cs, teardown := setupTest(t)
   206  		defer teardown(t)
   207  
   208  		mg.manifests[desc.Digest] = m
   209  
   210  		m2, err := store.Get(ctx, desc, ref)
   211  		checkIngest(t, cs, desc)
   212  		assert.NilError(t, err)
   213  		assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   214  		assert.Check(t, cmp.Equal(mg.gets, 1))
   215  
   216  		i, err := cs.Info(ctx, desc.Digest)
   217  		assert.NilError(t, err)
   218  		assert.Check(t, cmp.Equal(i.Digest, desc.Digest))
   219  
   220  		distKey, distSource := makeDistributionSourceLabel(ref)
   221  		assert.Check(t, hasDistributionSource(i.Labels[distKey], distSource))
   222  
   223  		// Now check again, this should not hit the remote
   224  		m2, err = store.Get(ctx, desc, ref)
   225  		checkIngest(t, cs, desc)
   226  		assert.NilError(t, err)
   227  		assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   228  		assert.Check(t, cmp.Equal(mg.gets, 1))
   229  
   230  		t.Run("digested", func(t *testing.T) {
   231  			ref, err := reference.WithDigest(ref, desc.Digest)
   232  			assert.NilError(t, err)
   233  
   234  			_, err = store.Get(ctx, desc, ref)
   235  			assert.NilError(t, err)
   236  		})
   237  	})
   238  
   239  	t.Run("with local cache", func(t *testing.T) {
   240  		ref, desc, mg, store, cs, teardown := setupTest(t)
   241  		defer teardown(t)
   242  
   243  		// first add the manifest to the coontent store
   244  		writeManifest(t, cs, desc)
   245  
   246  		// now do the get
   247  		m2, err := store.Get(ctx, desc, ref)
   248  		checkIngest(t, cs, desc)
   249  		assert.NilError(t, err)
   250  		assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   251  		assert.Check(t, cmp.Equal(mg.gets, 0))
   252  
   253  		i, err := cs.Info(ctx, desc.Digest)
   254  		assert.NilError(t, err)
   255  		assert.Check(t, cmp.Equal(i.Digest, desc.Digest))
   256  	})
   257  
   258  	// 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.
   259  	t.Run("unknown media type", func(t *testing.T) {
   260  		t.Run("no cache", func(t *testing.T) {
   261  			ref, desc, mg, store, cs, teardown := setupTest(t)
   262  			defer teardown(t)
   263  
   264  			mg.manifests[desc.Digest] = m
   265  			desc.MediaType = ""
   266  
   267  			m2, err := store.Get(ctx, desc, ref)
   268  			checkIngest(t, cs, desc)
   269  			assert.NilError(t, err)
   270  			assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   271  			assert.Check(t, cmp.Equal(mg.gets, 1))
   272  		})
   273  
   274  		t.Run("with cache", func(t *testing.T) {
   275  			t.Run("cached manifest has media type", func(t *testing.T) {
   276  				ref, desc, mg, store, cs, teardown := setupTest(t)
   277  				defer teardown(t)
   278  
   279  				writeManifest(t, cs, desc)
   280  				desc.MediaType = ""
   281  
   282  				m2, err := store.Get(ctx, desc, ref)
   283  				checkIngest(t, cs, desc)
   284  				assert.NilError(t, err)
   285  				assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   286  				assert.Check(t, cmp.Equal(mg.gets, 0))
   287  			})
   288  
   289  			t.Run("cached manifest has no media type", func(t *testing.T) {
   290  				ref, desc, mg, store, cs, teardown := setupTest(t)
   291  				defer teardown(t)
   292  
   293  				desc.MediaType = ""
   294  				writeManifest(t, cs, desc)
   295  
   296  				m2, err := store.Get(ctx, desc, ref)
   297  				checkIngest(t, cs, desc)
   298  				assert.NilError(t, err)
   299  				assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   300  				assert.Check(t, cmp.Equal(mg.gets, 0))
   301  			})
   302  		})
   303  	})
   304  
   305  	// Test that if there is an error with the content store, for whatever
   306  	// reason, that doesn't stop us from getting the manifest.
   307  	//
   308  	// Also makes sure the ingests are aborted.
   309  	t.Run("error persisting manifest", func(t *testing.T) {
   310  		t.Run("error on writer", func(t *testing.T) {
   311  			ref, desc, mg, store, cs, teardown := setupTest(t)
   312  			defer teardown(t)
   313  			mg.manifests[desc.Digest] = m
   314  
   315  			csW := &testingContentStoreWrapper{ContentStore: store.local, errorOnWriter: errors.New("random error")}
   316  			store.local = csW
   317  
   318  			m2, err := store.Get(ctx, desc, ref)
   319  			checkIngest(t, cs, desc)
   320  			assert.NilError(t, err)
   321  			assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   322  			assert.Check(t, cmp.Equal(mg.gets, 1))
   323  
   324  			_, err = cs.Info(ctx, desc.Digest)
   325  			// Nothing here since we couldn't persist
   326  			assert.Check(t, cerrdefs.IsNotFound(err), err)
   327  		})
   328  
   329  		t.Run("error on commit", func(t *testing.T) {
   330  			ref, desc, mg, store, cs, teardown := setupTest(t)
   331  			defer teardown(t)
   332  			mg.manifests[desc.Digest] = m
   333  
   334  			csW := &testingContentStoreWrapper{ContentStore: store.local, errorOnCommit: errors.New("random error")}
   335  			store.local = csW
   336  
   337  			m2, err := store.Get(ctx, desc, ref)
   338  			checkIngest(t, cs, desc)
   339  			assert.NilError(t, err)
   340  			assert.Check(t, cmp.DeepEqual(m, m2, cmpopts.IgnoreUnexported(ocischema.DeserializedManifest{})))
   341  			assert.Check(t, cmp.Equal(mg.gets, 1))
   342  
   343  			_, err = cs.Info(ctx, desc.Digest)
   344  			// Nothing here since we couldn't persist
   345  			assert.Check(t, cerrdefs.IsNotFound(err), err)
   346  		})
   347  	})
   348  }
   349  
   350  func TestDetectManifestBlobMediaType(t *testing.T) {
   351  	type testCase struct {
   352  		json     []byte
   353  		expected string
   354  	}
   355  	cases := map[string]testCase{
   356  		"mediaType is set":   {[]byte(`{"mediaType": "bananas"}`), "bananas"},
   357  		"oci manifest":       {[]byte(`{"config": {}}`), ocispec.MediaTypeImageManifest},
   358  		"schema1":            {[]byte(`{"fsLayers": []}`), schema1.MediaTypeManifest},
   359  		"oci index fallback": {[]byte(`{}`), ocispec.MediaTypeImageIndex},
   360  		// Make sure we prefer mediaType
   361  		"mediaType and config set":   {[]byte(`{"mediaType": "bananas", "config": {}}`), "bananas"},
   362  		"mediaType and fsLayers set": {[]byte(`{"mediaType": "bananas", "fsLayers": []}`), "bananas"},
   363  	}
   364  
   365  	t.Setenv("DOCKER_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE", "1")
   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  func TestDetectManifestBlobMediaTypeInvalid(t *testing.T) {
   376  	type testCase struct {
   377  		json     []byte
   378  		expected string
   379  	}
   380  	cases := map[string]testCase{
   381  		"schema 1 mediaType with manifests": {
   382  			[]byte(`{"mediaType": "` + schema1.MediaTypeManifest + `","manifests":[]}`),
   383  			`media-type: "application/vnd.docker.distribution.manifest.v1+json" should not have "manifests" or "layers"`,
   384  		},
   385  		"schema 1 mediaType with layers": {
   386  			[]byte(`{"mediaType": "` + schema1.MediaTypeManifest + `","layers":[]}`),
   387  			`media-type: "application/vnd.docker.distribution.manifest.v1+json" should not have "manifests" or "layers"`,
   388  		},
   389  		"schema 2 mediaType with manifests": {
   390  			[]byte(`{"mediaType": "` + schema2.MediaTypeManifest + `","manifests":[]}`),
   391  			`media-type: "application/vnd.docker.distribution.manifest.v2+json" should not have "manifests" or "fsLayers"`,
   392  		},
   393  		"schema 2 mediaType with fsLayers": {
   394  			[]byte(`{"mediaType": "` + schema2.MediaTypeManifest + `","fsLayers":[]}`),
   395  			`media-type: "application/vnd.docker.distribution.manifest.v2+json" should not have "manifests" or "fsLayers"`,
   396  		},
   397  		"oci manifest mediaType with manifests": {
   398  			[]byte(`{"mediaType": "` + ocispec.MediaTypeImageManifest + `","manifests":[]}`),
   399  			`media-type: "application/vnd.oci.image.manifest.v1+json" should not have "manifests" or "fsLayers"`,
   400  		},
   401  		"manifest list mediaType with fsLayers": {
   402  			[]byte(`{"mediaType": "` + manifestlist.MediaTypeManifestList + `","fsLayers":[]}`),
   403  			`media-type: "application/vnd.docker.distribution.manifest.list.v2+json" should not have "config", "layers", or "fsLayers"`,
   404  		},
   405  		"index mediaType with layers": {
   406  			[]byte(`{"mediaType": "` + ocispec.MediaTypeImageIndex + `","layers":[]}`),
   407  			`media-type: "application/vnd.oci.image.index.v1+json" should not have "config", "layers", or "fsLayers"`,
   408  		},
   409  		"index mediaType with config": {
   410  			[]byte(`{"mediaType": "` + ocispec.MediaTypeImageIndex + `","config":{}}`),
   411  			`media-type: "application/vnd.oci.image.index.v1+json" should not have "config", "layers", or "fsLayers"`,
   412  		},
   413  		"config and manifests": {
   414  			[]byte(`{"config":{}, "manifests":[]}`),
   415  			`media-type: cannot determine`,
   416  		},
   417  		"layers and manifests": {
   418  			[]byte(`{"layers":[], "manifests":[]}`),
   419  			`media-type: cannot determine`,
   420  		},
   421  		"layers and fsLayers": {
   422  			[]byte(`{"layers":[], "fsLayers":[]}`),
   423  			`media-type: cannot determine`,
   424  		},
   425  		"fsLayers and manifests": {
   426  			[]byte(`{"fsLayers":[], "manifests":[]}`),
   427  			`media-type: cannot determine`,
   428  		},
   429  		"config and fsLayers": {
   430  			[]byte(`{"config":{}, "fsLayers":[]}`),
   431  			`media-type: cannot determine`,
   432  		},
   433  	}
   434  
   435  	t.Setenv("DOCKER_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE", "1")
   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  }