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 }