github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/registry/remote/repository_test.go (about) 1 /* 2 Copyright The ORAS Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package remote 17 18 import ( 19 "bytes" 20 "context" 21 "crypto/tls" 22 "encoding/json" 23 "errors" 24 "fmt" 25 "io" 26 "net" 27 "net/http" 28 "net/http/httptest" 29 "net/url" 30 "reflect" 31 "strconv" 32 "strings" 33 "sync/atomic" 34 "testing" 35 36 "github.com/opcr-io/oras-go/v2/content" 37 "github.com/opcr-io/oras-go/v2/errdef" 38 "github.com/opcr-io/oras-go/v2/internal/interfaces" 39 "github.com/opcr-io/oras-go/v2/registry" 40 "github.com/opcr-io/oras-go/v2/registry/remote/auth" 41 "github.com/opencontainers/go-digest" 42 specs "github.com/opencontainers/image-spec/specs-go" 43 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 44 "golang.org/x/sync/errgroup" 45 ) 46 47 type testIOStruct struct { 48 isTag bool 49 clientSuppliedReference string 50 serverCalculatedDigest digest.Digest // for non-HEAD (body-containing) requests only 51 errExpectedOnHEAD bool 52 errExpectedOnGET bool 53 } 54 55 const theAmazingBanClan = "Ban Gu, Ban Chao, Ban Zhao" 56 const theAmazingBanDigest = "b526a4f2be963a2f9b0990c001255669eab8a254ab1a6e3f84f1820212ac7078" 57 58 // The following truth table aims to cover the expected GET/HEAD request outcome 59 // for all possible permutations of the client/server "containing a digest", for 60 // both Manifests and Blobs. Where the results between the two differ, the index 61 // of the first column has an exclamation mark. 62 // 63 // The client is said to "contain a digest" if the user-supplied reference string 64 // is of the form that contains a digest rather than a tag. The server, on the 65 // other hand, is said to "contain a digest" if the server responded with the 66 // special header `Docker-Content-Digest`. 67 // 68 // In this table, anything denoted with an asterisk indicates that the true 69 // response should actually be the opposite of what's expected; for example, 70 // `*PASS` means we will get a `PASS`, even though the true answer would be its 71 // diametric opposite--a `FAIL`. This may seem odd, and deserves an explanation. 72 // This function has blind-spots, and while it can expend power to gain sight, 73 // i.e., perform the expensive validation, we chose not to. The reason is two- 74 // fold: a) we "know" that even if we say "!PASS", it will eventually fail later 75 // when checks are performed, and with that assumption, we have the luxury for 76 // the second point, which is b) performance. 77 // 78 // _______________________________________________________________________________________________________________ 79 // | ID | CLIENT | SERVER | Manifest.GET | Blob.GET | Manifest.HEAD | Blob.HEAD | 80 // |----+-----------------+------------------+-----------------------+-----------+---------------------+-----------+ 81 // | 1 | tag | missing | CALCULATE,PASS | n/a | FAIL | n/a | 82 // | 2 | tag | presentCorrect | TRUST,PASS | n/a | TRUST,PASS | n/a | 83 // | 3 | tag | presentIncorrect | TRUST,*PASS | n/a | TRUST,*PASS | n/a | 84 // | 4 | correctDigest | missing | TRUST,PASS | PASS | TRUST,PASS | PASS | 85 // | 5 | correctDigest | presentCorrect | TRUST,COMPARE,PASS | PASS | TRUST,COMPARE,PASS | PASS | 86 // | 6 | correctDigest | presentIncorrect | TRUST,COMPARE,FAIL | FAIL | TRUST,COMPARE,FAIL | FAIL | 87 // --------------------------------------------------------------------------------------------------------------- 88 func getTestIOStructMapForGetDescriptorClass() map[string]testIOStruct { 89 correctDigest := fmt.Sprintf("sha256:%v", theAmazingBanDigest) 90 incorrectDigest := fmt.Sprintf("sha256:%v", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") 91 92 return map[string]testIOStruct{ 93 "1. Client:Tag & Server:DigestMissing": { 94 isTag: true, 95 errExpectedOnHEAD: true, 96 }, 97 "2. Client:Tag & Server:DigestValid": { 98 isTag: true, 99 serverCalculatedDigest: digest.Digest(correctDigest), 100 }, 101 "3. Client:Tag & Server:DigestWrongButSyntacticallyValid": { 102 isTag: true, 103 serverCalculatedDigest: digest.Digest(incorrectDigest), 104 }, 105 "4. Client:DigestValid & Server:DigestMissing": { 106 clientSuppliedReference: correctDigest, 107 }, 108 "5. Client:DigestValid & Server:DigestValid": { 109 clientSuppliedReference: correctDigest, 110 serverCalculatedDigest: digest.Digest(correctDigest), 111 }, 112 "6. Client:DigestValid & Server:DigestWrongButSyntacticallyValid": { 113 clientSuppliedReference: correctDigest, 114 serverCalculatedDigest: digest.Digest(incorrectDigest), 115 errExpectedOnHEAD: true, 116 errExpectedOnGET: true, 117 }, 118 } 119 } 120 121 func TestRepository_Fetch(t *testing.T) { 122 blob := []byte("hello world") 123 blobDesc := ocispec.Descriptor{ 124 MediaType: "test", 125 Digest: digest.FromBytes(blob), 126 Size: int64(len(blob)), 127 } 128 index := []byte(`{"manifests":[]}`) 129 indexDesc := ocispec.Descriptor{ 130 MediaType: ocispec.MediaTypeImageIndex, 131 Digest: digest.FromBytes(index), 132 Size: int64(len(index)), 133 } 134 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 135 if r.Method != http.MethodGet { 136 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 137 w.WriteHeader(http.StatusMethodNotAllowed) 138 return 139 } 140 switch r.URL.Path { 141 case "/v2/test/blobs/" + blobDesc.Digest.String(): 142 w.Header().Set("Content-Type", "application/octet-stream") 143 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 144 if _, err := w.Write(blob); err != nil { 145 t.Errorf("failed to write %q: %v", r.URL, err) 146 } 147 case "/v2/test/manifests/" + indexDesc.Digest.String(): 148 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { 149 t.Errorf("manifest not convertable: %s", accept) 150 w.WriteHeader(http.StatusBadRequest) 151 return 152 } 153 w.Header().Set("Content-Type", indexDesc.MediaType) 154 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 155 if _, err := w.Write(index); err != nil { 156 t.Errorf("failed to write %q: %v", r.URL, err) 157 } 158 default: 159 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 160 w.WriteHeader(http.StatusNotFound) 161 } 162 })) 163 defer ts.Close() 164 uri, err := url.Parse(ts.URL) 165 if err != nil { 166 t.Fatalf("invalid test http server: %v", err) 167 } 168 169 repo, err := NewRepository(uri.Host + "/test") 170 if err != nil { 171 t.Fatalf("NewRepository() error = %v", err) 172 } 173 repo.PlainHTTP = true 174 ctx := context.Background() 175 176 rc, err := repo.Fetch(ctx, blobDesc) 177 if err != nil { 178 t.Fatalf("Repository.Fetch() error = %v", err) 179 } 180 buf := bytes.NewBuffer(nil) 181 if _, err := buf.ReadFrom(rc); err != nil { 182 t.Errorf("fail to read: %v", err) 183 } 184 if err := rc.Close(); err != nil { 185 t.Errorf("fail to close: %v", err) 186 } 187 if got := buf.Bytes(); !bytes.Equal(got, blob) { 188 t.Errorf("Repository.Fetch() = %v, want %v", got, blob) 189 } 190 191 rc, err = repo.Fetch(ctx, indexDesc) 192 if err != nil { 193 t.Fatalf("Repository.Fetch() error = %v", err) 194 } 195 buf.Reset() 196 if _, err := buf.ReadFrom(rc); err != nil { 197 t.Errorf("fail to read: %v", err) 198 } 199 if err := rc.Close(); err != nil { 200 t.Errorf("fail to close: %v", err) 201 } 202 if got := buf.Bytes(); !bytes.Equal(got, index) { 203 t.Errorf("Repository.Fetch() = %v, want %v", got, index) 204 } 205 } 206 207 func TestRepository_Push(t *testing.T) { 208 blob := []byte("hello world") 209 blobDesc := ocispec.Descriptor{ 210 MediaType: "test", 211 Digest: digest.FromBytes(blob), 212 Size: int64(len(blob)), 213 } 214 var gotBlob []byte 215 index := []byte(`{"manifests":[]}`) 216 indexDesc := ocispec.Descriptor{ 217 MediaType: ocispec.MediaTypeImageIndex, 218 Digest: digest.FromBytes(index), 219 Size: int64(len(index)), 220 } 221 var gotIndex []byte 222 uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c" 223 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 224 switch { 225 case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/": 226 w.Header().Set("Location", "/v2/test/blobs/uploads/"+uuid) 227 w.WriteHeader(http.StatusAccepted) 228 return 229 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid: 230 if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" { 231 w.WriteHeader(http.StatusBadRequest) 232 break 233 } 234 if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() { 235 w.WriteHeader(http.StatusBadRequest) 236 break 237 } 238 buf := bytes.NewBuffer(nil) 239 if _, err := buf.ReadFrom(r.Body); err != nil { 240 t.Errorf("fail to read: %v", err) 241 } 242 gotBlob = buf.Bytes() 243 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 244 w.WriteHeader(http.StatusCreated) 245 return 246 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 247 if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { 248 w.WriteHeader(http.StatusBadRequest) 249 break 250 } 251 buf := bytes.NewBuffer(nil) 252 if _, err := buf.ReadFrom(r.Body); err != nil { 253 t.Errorf("fail to read: %v", err) 254 } 255 gotIndex = buf.Bytes() 256 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 257 w.WriteHeader(http.StatusCreated) 258 return 259 default: 260 w.WriteHeader(http.StatusForbidden) 261 } 262 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 263 })) 264 defer ts.Close() 265 uri, err := url.Parse(ts.URL) 266 if err != nil { 267 t.Fatalf("invalid test http server: %v", err) 268 } 269 270 repo, err := NewRepository(uri.Host + "/test") 271 if err != nil { 272 t.Fatalf("NewRepository() error = %v", err) 273 } 274 repo.PlainHTTP = true 275 ctx := context.Background() 276 277 err = repo.Push(ctx, blobDesc, bytes.NewReader(blob)) 278 if err != nil { 279 t.Fatalf("Repository.Push() error = %v", err) 280 } 281 if !bytes.Equal(gotBlob, blob) { 282 t.Errorf("Repository.Push() = %v, want %v", gotBlob, blob) 283 } 284 285 err = repo.Push(ctx, indexDesc, bytes.NewReader(index)) 286 if err != nil { 287 t.Fatalf("Repository.Push() error = %v", err) 288 } 289 if !bytes.Equal(gotIndex, index) { 290 t.Errorf("Repository.Push() = %v, want %v", gotIndex, index) 291 } 292 } 293 294 func TestRepository_Exists(t *testing.T) { 295 blob := []byte("hello world") 296 blobDesc := ocispec.Descriptor{ 297 MediaType: "test", 298 Digest: digest.FromBytes(blob), 299 Size: int64(len(blob)), 300 } 301 index := []byte(`{"manifests":[]}`) 302 indexDesc := ocispec.Descriptor{ 303 MediaType: ocispec.MediaTypeImageIndex, 304 Digest: digest.FromBytes(index), 305 Size: int64(len(index)), 306 } 307 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 308 if r.Method != http.MethodHead { 309 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 310 w.WriteHeader(http.StatusMethodNotAllowed) 311 return 312 } 313 switch r.URL.Path { 314 case "/v2/test/blobs/" + blobDesc.Digest.String(): 315 w.Header().Set("Content-Type", "application/octet-stream") 316 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 317 w.Header().Set("Content-Length", strconv.Itoa(int(blobDesc.Size))) 318 case "/v2/test/manifests/" + indexDesc.Digest.String(): 319 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { 320 t.Errorf("manifest not convertable: %s", accept) 321 w.WriteHeader(http.StatusBadRequest) 322 return 323 } 324 w.Header().Set("Content-Type", indexDesc.MediaType) 325 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 326 w.Header().Set("Content-Length", strconv.Itoa(int(indexDesc.Size))) 327 default: 328 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 329 w.WriteHeader(http.StatusNotFound) 330 } 331 })) 332 defer ts.Close() 333 uri, err := url.Parse(ts.URL) 334 if err != nil { 335 t.Fatalf("invalid test http server: %v", err) 336 } 337 338 repo, err := NewRepository(uri.Host + "/test") 339 if err != nil { 340 t.Fatalf("NewRepository() error = %v", err) 341 } 342 repo.PlainHTTP = true 343 ctx := context.Background() 344 345 exists, err := repo.Exists(ctx, blobDesc) 346 if err != nil { 347 t.Fatalf("Repository.Exists() error = %v", err) 348 } 349 if !exists { 350 t.Errorf("Repository.Exists() = %v, want %v", exists, true) 351 } 352 353 exists, err = repo.Exists(ctx, indexDesc) 354 if err != nil { 355 t.Fatalf("Repository.Exists() error = %v", err) 356 } 357 if !exists { 358 t.Errorf("Repository.Exists() = %v, want %v", exists, true) 359 } 360 } 361 362 func TestRepository_Delete(t *testing.T) { 363 blob := []byte("hello world") 364 blobDesc := ocispec.Descriptor{ 365 MediaType: "test", 366 Digest: digest.FromBytes(blob), 367 Size: int64(len(blob)), 368 } 369 blobDeleted := false 370 index := []byte(`{"manifests":[]}`) 371 indexDesc := ocispec.Descriptor{ 372 MediaType: ocispec.MediaTypeImageIndex, 373 Digest: digest.FromBytes(index), 374 Size: int64(len(index)), 375 } 376 indexDeleted := false 377 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 378 if r.Method != http.MethodDelete { 379 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 380 w.WriteHeader(http.StatusMethodNotAllowed) 381 return 382 } 383 switch r.URL.Path { 384 case "/v2/test/blobs/" + blobDesc.Digest.String(): 385 blobDeleted = true 386 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 387 w.WriteHeader(http.StatusAccepted) 388 case "/v2/test/manifests/" + indexDesc.Digest.String(): 389 indexDeleted = true 390 // no "Docker-Content-Digest" header for manifest deletion 391 w.WriteHeader(http.StatusAccepted) 392 default: 393 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 394 w.WriteHeader(http.StatusNotFound) 395 } 396 })) 397 defer ts.Close() 398 uri, err := url.Parse(ts.URL) 399 if err != nil { 400 t.Fatalf("invalid test http server: %v", err) 401 } 402 403 repo, err := NewRepository(uri.Host + "/test") 404 if err != nil { 405 t.Fatalf("NewRepository() error = %v", err) 406 } 407 repo.PlainHTTP = true 408 ctx := context.Background() 409 410 err = repo.Delete(ctx, blobDesc) 411 if err != nil { 412 t.Fatalf("Repository.Delete() error = %v", err) 413 } 414 if !blobDeleted { 415 t.Errorf("Repository.Delete() = %v, want %v", blobDeleted, true) 416 } 417 418 err = repo.Delete(ctx, indexDesc) 419 if err != nil { 420 t.Fatalf("Repository.Delete() error = %v", err) 421 } 422 if !indexDeleted { 423 t.Errorf("Repository.Delete() = %v, want %v", indexDeleted, true) 424 } 425 } 426 427 func TestRepository_Resolve(t *testing.T) { 428 blob := []byte("hello world") 429 blobDesc := ocispec.Descriptor{ 430 MediaType: "test", 431 Digest: digest.FromBytes(blob), 432 Size: int64(len(blob)), 433 } 434 index := []byte(`{"manifests":[]}`) 435 indexDesc := ocispec.Descriptor{ 436 MediaType: ocispec.MediaTypeImageIndex, 437 Digest: digest.FromBytes(index), 438 Size: int64(len(index)), 439 } 440 ref := "foobar" 441 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 442 if r.Method != http.MethodHead { 443 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 444 w.WriteHeader(http.StatusMethodNotAllowed) 445 return 446 } 447 switch r.URL.Path { 448 case "/v2/test/manifests/" + blobDesc.Digest.String(): 449 w.WriteHeader(http.StatusNotFound) 450 case "/v2/test/manifests/" + indexDesc.Digest.String(), 451 "/v2/test/manifests/" + ref: 452 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { 453 t.Errorf("manifest not convertable: %s", accept) 454 w.WriteHeader(http.StatusBadRequest) 455 return 456 } 457 w.Header().Set("Content-Type", indexDesc.MediaType) 458 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 459 w.Header().Set("Content-Length", strconv.Itoa(int(indexDesc.Size))) 460 default: 461 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 462 w.WriteHeader(http.StatusNotFound) 463 } 464 })) 465 defer ts.Close() 466 uri, err := url.Parse(ts.URL) 467 if err != nil { 468 t.Fatalf("invalid test http server: %v", err) 469 } 470 471 repoName := uri.Host + "/test" 472 repo, err := NewRepository(repoName) 473 if err != nil { 474 t.Fatalf("NewRepository() error = %v", err) 475 } 476 repo.PlainHTTP = true 477 ctx := context.Background() 478 479 _, err = repo.Resolve(ctx, blobDesc.Digest.String()) 480 if !errors.Is(err, errdef.ErrNotFound) { 481 t.Errorf("Repository.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound) 482 } 483 484 got, err := repo.Resolve(ctx, indexDesc.Digest.String()) 485 if err != nil { 486 t.Fatalf("Repository.Resolve() error = %v", err) 487 } 488 if !reflect.DeepEqual(got, indexDesc) { 489 t.Errorf("Repository.Resolve() = %v, want %v", got, indexDesc) 490 } 491 492 got, err = repo.Resolve(ctx, ref) 493 if err != nil { 494 t.Fatalf("Repository.Resolve() error = %v", err) 495 } 496 if !reflect.DeepEqual(got, indexDesc) { 497 t.Errorf("Repository.Resolve() = %v, want %v", got, indexDesc) 498 } 499 500 tagDigestRef := "whatever" + "@" + indexDesc.Digest.String() 501 got, err = repo.Resolve(ctx, tagDigestRef) 502 if err != nil { 503 t.Fatalf("Repository.Resolve() error = %v", err) 504 } 505 if !reflect.DeepEqual(got, indexDesc) { 506 t.Errorf("Repository.Resolve() = %v, want %v", got, indexDesc) 507 } 508 509 fqdnRef := repoName + ":" + tagDigestRef 510 got, err = repo.Resolve(ctx, fqdnRef) 511 if err != nil { 512 t.Fatalf("Repository.Resolve() error = %v", err) 513 } 514 if !reflect.DeepEqual(got, indexDesc) { 515 t.Errorf("Repository.Resolve() = %v, want %v", got, indexDesc) 516 } 517 } 518 519 func TestRepository_Tag(t *testing.T) { 520 blob := []byte("hello world") 521 blobDesc := ocispec.Descriptor{ 522 MediaType: "test", 523 Digest: digest.FromBytes(blob), 524 Size: int64(len(blob)), 525 } 526 index := []byte(`{"manifests":[]}`) 527 indexDesc := ocispec.Descriptor{ 528 MediaType: ocispec.MediaTypeImageIndex, 529 Digest: digest.FromBytes(index), 530 Size: int64(len(index)), 531 } 532 var gotIndex []byte 533 ref := "foobar" 534 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 535 switch { 536 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+blobDesc.Digest.String(): 537 w.WriteHeader(http.StatusNotFound) 538 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 539 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { 540 t.Errorf("manifest not convertable: %s", accept) 541 w.WriteHeader(http.StatusBadRequest) 542 return 543 } 544 w.Header().Set("Content-Type", indexDesc.MediaType) 545 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 546 if _, err := w.Write(index); err != nil { 547 t.Errorf("failed to write %q: %v", r.URL, err) 548 } 549 case r.Method == http.MethodPut && 550 r.URL.Path == "/v2/test/manifests/"+ref || r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 551 if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { 552 w.WriteHeader(http.StatusBadRequest) 553 break 554 } 555 buf := bytes.NewBuffer(nil) 556 if _, err := buf.ReadFrom(r.Body); err != nil { 557 t.Errorf("fail to read: %v", err) 558 } 559 gotIndex = buf.Bytes() 560 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 561 w.WriteHeader(http.StatusCreated) 562 return 563 default: 564 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 565 w.WriteHeader(http.StatusForbidden) 566 } 567 })) 568 defer ts.Close() 569 uri, err := url.Parse(ts.URL) 570 if err != nil { 571 t.Fatalf("invalid test http server: %v", err) 572 } 573 574 repo, err := NewRepository(uri.Host + "/test") 575 if err != nil { 576 t.Fatalf("NewRepository() error = %v", err) 577 } 578 repo.PlainHTTP = true 579 ctx := context.Background() 580 581 err = repo.Tag(ctx, blobDesc, ref) 582 if err == nil { 583 t.Errorf("Repository.Tag() error = %v, wantErr %v", err, true) 584 } 585 586 err = repo.Tag(ctx, indexDesc, ref) 587 if err != nil { 588 t.Fatalf("Repository.Tag() error = %v", err) 589 } 590 if !bytes.Equal(gotIndex, index) { 591 t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index) 592 } 593 594 gotIndex = nil 595 err = repo.Tag(ctx, indexDesc, indexDesc.Digest.String()) 596 if err != nil { 597 t.Fatalf("Repository.Tag() error = %v", err) 598 } 599 if !bytes.Equal(gotIndex, index) { 600 t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index) 601 } 602 } 603 604 func TestRepository_PushReference(t *testing.T) { 605 index := []byte(`{"manifests":[]}`) 606 indexDesc := ocispec.Descriptor{ 607 MediaType: ocispec.MediaTypeImageIndex, 608 Digest: digest.FromBytes(index), 609 Size: int64(len(index)), 610 } 611 var gotIndex []byte 612 ref := "foobar" 613 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 614 switch { 615 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+ref: 616 if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { 617 w.WriteHeader(http.StatusBadRequest) 618 break 619 } 620 buf := bytes.NewBuffer(nil) 621 if _, err := buf.ReadFrom(r.Body); err != nil { 622 t.Errorf("fail to read: %v", err) 623 } 624 gotIndex = buf.Bytes() 625 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 626 w.WriteHeader(http.StatusCreated) 627 return 628 default: 629 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 630 w.WriteHeader(http.StatusForbidden) 631 } 632 })) 633 defer ts.Close() 634 uri, err := url.Parse(ts.URL) 635 if err != nil { 636 t.Fatalf("invalid test http server: %v", err) 637 } 638 639 repo, err := NewRepository(uri.Host + "/test") 640 if err != nil { 641 t.Fatalf("NewRepository() error = %v", err) 642 } 643 repo.PlainHTTP = true 644 ctx := context.Background() 645 err = repo.PushReference(ctx, indexDesc, bytes.NewReader(index), ref) 646 if err != nil { 647 t.Fatalf("Repository.PushReference() error = %v", err) 648 } 649 if !bytes.Equal(gotIndex, index) { 650 t.Errorf("Repository.PushReference() = %v, want %v", gotIndex, index) 651 } 652 } 653 654 func TestRepository_FetchReference(t *testing.T) { 655 blob := []byte("hello world") 656 blobDesc := ocispec.Descriptor{ 657 MediaType: "test", 658 Digest: digest.FromBytes(blob), 659 Size: int64(len(blob)), 660 } 661 index := []byte(`{"manifests":[]}`) 662 indexDesc := ocispec.Descriptor{ 663 MediaType: ocispec.MediaTypeImageIndex, 664 Digest: digest.FromBytes(index), 665 Size: int64(len(index)), 666 } 667 ref := "foobar" 668 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 669 if r.Method != http.MethodGet { 670 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 671 w.WriteHeader(http.StatusMethodNotAllowed) 672 return 673 } 674 switch r.URL.Path { 675 case "/v2/test/manifests/" + blobDesc.Digest.String(): 676 w.WriteHeader(http.StatusNotFound) 677 case "/v2/test/manifests/" + indexDesc.Digest.String(), 678 "/v2/test/manifests/" + ref: 679 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { 680 t.Errorf("manifest not convertable: %s", accept) 681 w.WriteHeader(http.StatusBadRequest) 682 return 683 } 684 w.Header().Set("Content-Type", indexDesc.MediaType) 685 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 686 if _, err := w.Write(index); err != nil { 687 t.Errorf("failed to write %q: %v", r.URL, err) 688 } 689 default: 690 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 691 w.WriteHeader(http.StatusNotFound) 692 } 693 })) 694 defer ts.Close() 695 uri, err := url.Parse(ts.URL) 696 if err != nil { 697 t.Fatalf("invalid test http server: %v", err) 698 } 699 700 repoName := uri.Host + "/test" 701 repo, err := NewRepository(repoName) 702 if err != nil { 703 t.Fatalf("NewRepository() error = %v", err) 704 } 705 repo.PlainHTTP = true 706 ctx := context.Background() 707 708 // test with blob digest 709 _, _, err = repo.FetchReference(ctx, blobDesc.Digest.String()) 710 if !errors.Is(err, errdef.ErrNotFound) { 711 t.Errorf("Repository.FetchReference() error = %v, wantErr %v", err, errdef.ErrNotFound) 712 } 713 714 // test with manifest digest 715 gotDesc, rc, err := repo.FetchReference(ctx, indexDesc.Digest.String()) 716 if err != nil { 717 t.Fatalf("Repository.FetchReference() error = %v", err) 718 } 719 if !reflect.DeepEqual(gotDesc, indexDesc) { 720 t.Errorf("Repository.FetchReference() = %v, want %v", gotDesc, indexDesc) 721 } 722 buf := bytes.NewBuffer(nil) 723 if _, err := buf.ReadFrom(rc); err != nil { 724 t.Errorf("fail to read: %v", err) 725 } 726 if err := rc.Close(); err != nil { 727 t.Errorf("fail to close: %v", err) 728 } 729 if got := buf.Bytes(); !bytes.Equal(got, index) { 730 t.Errorf("Repository.FetchReference() = %v, want %v", got, index) 731 } 732 733 // test with manifest tag 734 gotDesc, rc, err = repo.FetchReference(ctx, ref) 735 if err != nil { 736 t.Fatalf("Repository.FetchReference() error = %v", err) 737 } 738 if !reflect.DeepEqual(gotDesc, indexDesc) { 739 t.Errorf("Repository.FetchReference() = %v, want %v", gotDesc, indexDesc) 740 } 741 buf.Reset() 742 if _, err := buf.ReadFrom(rc); err != nil { 743 t.Errorf("fail to read: %v", err) 744 } 745 if err := rc.Close(); err != nil { 746 t.Errorf("fail to close: %v", err) 747 } 748 if got := buf.Bytes(); !bytes.Equal(got, index) { 749 t.Errorf("Repository.FetchReference() = %v, want %v", got, index) 750 } 751 752 // test with manifest tag@digest 753 tagDigestRef := "whatever" + "@" + indexDesc.Digest.String() 754 gotDesc, rc, err = repo.FetchReference(ctx, tagDigestRef) 755 if err != nil { 756 t.Fatalf("Repository.FetchReference() error = %v", err) 757 } 758 if !reflect.DeepEqual(gotDesc, indexDesc) { 759 t.Errorf("Repository.FetchReference() = %v, want %v", gotDesc, indexDesc) 760 } 761 buf.Reset() 762 if _, err := buf.ReadFrom(rc); err != nil { 763 t.Errorf("fail to read: %v", err) 764 } 765 if err := rc.Close(); err != nil { 766 t.Errorf("fail to close: %v", err) 767 } 768 if got := buf.Bytes(); !bytes.Equal(got, index) { 769 t.Errorf("Repository.FetchReference() = %v, want %v", got, index) 770 } 771 772 // test with manifest FQDN 773 fqdnRef := repoName + ":" + tagDigestRef 774 gotDesc, rc, err = repo.FetchReference(ctx, fqdnRef) 775 if err != nil { 776 t.Fatalf("Repository.FetchReference() error = %v", err) 777 } 778 if !reflect.DeepEqual(gotDesc, indexDesc) { 779 t.Errorf("Repository.FetchReference() = %v, want %v", gotDesc, indexDesc) 780 } 781 buf.Reset() 782 if _, err := buf.ReadFrom(rc); err != nil { 783 t.Errorf("fail to read: %v", err) 784 } 785 if err := rc.Close(); err != nil { 786 t.Errorf("fail to close: %v", err) 787 } 788 if got := buf.Bytes(); !bytes.Equal(got, index) { 789 t.Errorf("Repository.FetchReference() = %v, want %v", got, index) 790 } 791 } 792 793 func TestRepository_Tags(t *testing.T) { 794 tagSet := [][]string{ 795 {"the", "quick", "brown", "fox"}, 796 {"jumps", "over", "the", "lazy"}, 797 {"dog"}, 798 } 799 var ts *httptest.Server 800 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 801 if r.Method != http.MethodGet || r.URL.Path != "/v2/test/tags/list" { 802 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 803 w.WriteHeader(http.StatusNotFound) 804 return 805 } 806 q := r.URL.Query() 807 n, err := strconv.Atoi(q.Get("n")) 808 if err != nil || n != 4 { 809 t.Errorf("bad page size: %s", q.Get("n")) 810 w.WriteHeader(http.StatusBadRequest) 811 return 812 } 813 var tags []string 814 switch q.Get("test") { 815 case "foo": 816 tags = tagSet[1] 817 w.Header().Set("Link", fmt.Sprintf(`<%s/v2/test/tags/list?n=4&test=bar>; rel="next"`, ts.URL)) 818 case "bar": 819 tags = tagSet[2] 820 default: 821 tags = tagSet[0] 822 w.Header().Set("Link", `</v2/test/tags/list?n=4&test=foo>; rel="next"`) 823 } 824 result := struct { 825 Tags []string `json:"tags"` 826 }{ 827 Tags: tags, 828 } 829 if err := json.NewEncoder(w).Encode(result); err != nil { 830 t.Errorf("failed to write response: %v", err) 831 } 832 })) 833 defer ts.Close() 834 uri, err := url.Parse(ts.URL) 835 if err != nil { 836 t.Fatalf("invalid test http server: %v", err) 837 } 838 839 repo, err := NewRepository(uri.Host + "/test") 840 if err != nil { 841 t.Fatalf("NewRepository() error = %v", err) 842 } 843 repo.PlainHTTP = true 844 repo.TagListPageSize = 4 845 846 ctx := context.Background() 847 index := 0 848 if err := repo.Tags(ctx, "", func(got []string) error { 849 if index > 2 { 850 t.Fatalf("out of index bound: %d", index) 851 } 852 tags := tagSet[index] 853 index++ 854 if !reflect.DeepEqual(got, tags) { 855 t.Errorf("Repository.Tags() = %v, want %v", got, tags) 856 } 857 return nil 858 }); err != nil { 859 t.Errorf("Repository.Tags() error = %v", err) 860 } 861 } 862 863 func TestRepository_Predecessors(t *testing.T) { 864 manifest := []byte(`{"layers":[]}`) 865 manifestDesc := ocispec.Descriptor{ 866 MediaType: ocispec.MediaTypeImageManifest, 867 Digest: digest.FromBytes(manifest), 868 Size: int64(len(manifest)), 869 } 870 referrerSet := [][]ocispec.Descriptor{ 871 { 872 { 873 MediaType: ocispec.MediaTypeArtifactManifest, 874 Size: 1, 875 Digest: digest.FromString("1"), 876 ArtifactType: "application/vnd.test", 877 }, 878 { 879 MediaType: ocispec.MediaTypeArtifactManifest, 880 Size: 2, 881 Digest: digest.FromString("2"), 882 ArtifactType: "application/vnd.test", 883 }, 884 }, 885 { 886 { 887 MediaType: ocispec.MediaTypeArtifactManifest, 888 Size: 3, 889 Digest: digest.FromString("3"), 890 ArtifactType: "application/vnd.test", 891 }, 892 { 893 MediaType: ocispec.MediaTypeArtifactManifest, 894 Size: 4, 895 Digest: digest.FromString("4"), 896 ArtifactType: "application/vnd.test", 897 }, 898 }, 899 { 900 { 901 MediaType: ocispec.MediaTypeArtifactManifest, 902 Size: 5, 903 Digest: digest.FromString("5"), 904 ArtifactType: "application/vnd.test", 905 }, 906 }, 907 } 908 var ts *httptest.Server 909 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 910 path := "/v2/test/referrers/" + manifestDesc.Digest.String() 911 if r.Method != http.MethodGet || r.URL.Path != path { 912 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 913 w.WriteHeader(http.StatusNotFound) 914 return 915 } 916 q := r.URL.Query() 917 n, err := strconv.Atoi(q.Get("n")) 918 if err != nil || n != 2 { 919 t.Errorf("bad page size: %s", q.Get("n")) 920 w.WriteHeader(http.StatusBadRequest) 921 return 922 } 923 var referrers []ocispec.Descriptor 924 switch q.Get("test") { 925 case "foo": 926 referrers = referrerSet[1] 927 w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path)) 928 case "bar": 929 referrers = referrerSet[2] 930 default: 931 referrers = referrerSet[0] 932 w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path)) 933 } 934 result := ocispec.Index{ 935 Versioned: specs.Versioned{ 936 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 937 }, 938 MediaType: ocispec.MediaTypeImageIndex, 939 Manifests: referrers, 940 } 941 if err := json.NewEncoder(w).Encode(result); err != nil { 942 t.Errorf("failed to write response: %v", err) 943 } 944 })) 945 defer ts.Close() 946 uri, err := url.Parse(ts.URL) 947 if err != nil { 948 t.Fatalf("invalid test http server: %v", err) 949 } 950 951 repo, err := NewRepository(uri.Host + "/test") 952 if err != nil { 953 t.Fatalf("NewRepository() error = %v", err) 954 } 955 repo.PlainHTTP = true 956 repo.ReferrerListPageSize = 2 957 958 ctx := context.Background() 959 got, err := repo.Predecessors(ctx, manifestDesc) 960 if err != nil { 961 t.Fatalf("Repository.Predecessors() error = %v", err) 962 } 963 var want []ocispec.Descriptor 964 for _, referrers := range referrerSet { 965 want = append(want, referrers...) 966 } 967 if !reflect.DeepEqual(got, want) { 968 t.Errorf("Repository.Predecessors() = %v, want %v", got, want) 969 } 970 } 971 972 func TestRepository_Referrers(t *testing.T) { 973 manifest := []byte(`{"layers":[]}`) 974 manifestDesc := ocispec.Descriptor{ 975 MediaType: ocispec.MediaTypeImageManifest, 976 Digest: digest.FromBytes(manifest), 977 Size: int64(len(manifest)), 978 } 979 referrerSet := [][]ocispec.Descriptor{ 980 { 981 { 982 MediaType: ocispec.MediaTypeArtifactManifest, 983 Size: 1, 984 Digest: digest.FromString("1"), 985 ArtifactType: "application/vnd.test", 986 }, 987 { 988 MediaType: ocispec.MediaTypeArtifactManifest, 989 Size: 2, 990 Digest: digest.FromString("2"), 991 ArtifactType: "application/vnd.test", 992 }, 993 }, 994 { 995 { 996 MediaType: ocispec.MediaTypeArtifactManifest, 997 Size: 3, 998 Digest: digest.FromString("3"), 999 ArtifactType: "application/vnd.test", 1000 }, 1001 { 1002 MediaType: ocispec.MediaTypeArtifactManifest, 1003 Size: 4, 1004 Digest: digest.FromString("4"), 1005 ArtifactType: "application/vnd.test", 1006 }, 1007 }, 1008 { 1009 { 1010 MediaType: ocispec.MediaTypeArtifactManifest, 1011 Size: 5, 1012 Digest: digest.FromString("5"), 1013 ArtifactType: "application/vnd.test", 1014 }, 1015 }, 1016 } 1017 var ts *httptest.Server 1018 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1019 path := "/v2/test/referrers/" + manifestDesc.Digest.String() 1020 if r.Method != http.MethodGet || r.URL.Path != path { 1021 referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) 1022 if r.URL.Path != "/v2/test/manifests/"+referrersTag { 1023 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1024 } 1025 w.WriteHeader(http.StatusNotFound) 1026 return 1027 } 1028 q := r.URL.Query() 1029 n, err := strconv.Atoi(q.Get("n")) 1030 if err != nil || n != 2 { 1031 t.Errorf("bad page size: %s", q.Get("n")) 1032 w.WriteHeader(http.StatusBadRequest) 1033 return 1034 } 1035 var referrers []ocispec.Descriptor 1036 switch q.Get("test") { 1037 case "foo": 1038 referrers = referrerSet[1] 1039 w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path)) 1040 case "bar": 1041 referrers = referrerSet[2] 1042 default: 1043 referrers = referrerSet[0] 1044 w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path)) 1045 } 1046 result := ocispec.Index{ 1047 Versioned: specs.Versioned{ 1048 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 1049 }, 1050 MediaType: ocispec.MediaTypeImageIndex, 1051 Manifests: referrers, 1052 } 1053 if err := json.NewEncoder(w).Encode(result); err != nil { 1054 t.Errorf("failed to write response: %v", err) 1055 } 1056 })) 1057 defer ts.Close() 1058 uri, err := url.Parse(ts.URL) 1059 if err != nil { 1060 t.Fatalf("invalid test http server: %v", err) 1061 } 1062 1063 ctx := context.Background() 1064 1065 // test auto detect 1066 // remote server supports Referrers, should be no error 1067 repo, err := NewRepository(uri.Host + "/test") 1068 if err != nil { 1069 t.Fatalf("NewRepository() error = %v", err) 1070 } 1071 repo.PlainHTTP = true 1072 repo.ReferrerListPageSize = 2 1073 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1074 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1075 } 1076 index := 0 1077 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1078 if index >= len(referrerSet) { 1079 t.Fatalf("out of index bound: %d", index) 1080 } 1081 referrers := referrerSet[index] 1082 index++ 1083 if !reflect.DeepEqual(got, referrers) { 1084 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 1085 } 1086 return nil 1087 }); err != nil { 1088 t.Errorf("Repository.Referrers() error = %v", err) 1089 } 1090 if state := repo.loadReferrersState(); state != referrersStateSupported { 1091 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1092 } 1093 1094 // test force attempt Referrers 1095 // remote server supports Referrers, should be no error 1096 repo, err = NewRepository(uri.Host + "/test") 1097 if err != nil { 1098 t.Fatalf("NewRepository() error = %v", err) 1099 } 1100 repo.PlainHTTP = true 1101 repo.ReferrerListPageSize = 2 1102 repo.SetReferrersCapability(true) 1103 if state := repo.loadReferrersState(); state != referrersStateSupported { 1104 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1105 } 1106 index = 0 1107 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1108 if index >= len(referrerSet) { 1109 t.Fatalf("out of index bound: %d", index) 1110 } 1111 referrers := referrerSet[index] 1112 index++ 1113 if !reflect.DeepEqual(got, referrers) { 1114 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 1115 } 1116 return nil 1117 }); err != nil { 1118 t.Errorf("Repository.Referrers() error = %v", err) 1119 } 1120 if state := repo.loadReferrersState(); state != referrersStateSupported { 1121 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1122 } 1123 1124 // test force attempt tag schema 1125 // request tag schema but got 404, should be no error 1126 repo, err = NewRepository(uri.Host + "/test") 1127 if err != nil { 1128 t.Fatalf("NewRepository() error = %v", err) 1129 } 1130 repo.PlainHTTP = true 1131 repo.ReferrerListPageSize = 2 1132 repo.SetReferrersCapability(false) 1133 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1134 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1135 } 1136 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1137 return nil 1138 }); err != nil { 1139 t.Errorf("Repository.Referrers() error = %v", err) 1140 } 1141 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1142 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1143 } 1144 } 1145 1146 func TestRepository_Referrers_TagSchemaFallback(t *testing.T) { 1147 manifest := []byte(`{"layers":[]}`) 1148 manifestDesc := ocispec.Descriptor{ 1149 MediaType: ocispec.MediaTypeImageManifest, 1150 Digest: digest.FromBytes(manifest), 1151 Size: int64(len(manifest)), 1152 } 1153 1154 referrers := []ocispec.Descriptor{ 1155 { 1156 MediaType: ocispec.MediaTypeArtifactManifest, 1157 Size: 1, 1158 Digest: digest.FromString("1"), 1159 ArtifactType: "application/vnd.test", 1160 }, 1161 { 1162 MediaType: ocispec.MediaTypeArtifactManifest, 1163 Size: 2, 1164 Digest: digest.FromString("2"), 1165 ArtifactType: "application/vnd.test", 1166 }, 1167 { 1168 MediaType: ocispec.MediaTypeArtifactManifest, 1169 Size: 3, 1170 Digest: digest.FromString("3"), 1171 ArtifactType: "application/vnd.test", 1172 }, 1173 { 1174 MediaType: ocispec.MediaTypeArtifactManifest, 1175 Size: 4, 1176 Digest: digest.FromString("4"), 1177 ArtifactType: "application/vnd.test", 1178 }, 1179 { 1180 MediaType: ocispec.MediaTypeArtifactManifest, 1181 Size: 5, 1182 Digest: digest.FromString("5"), 1183 ArtifactType: "application/vnd.test", 1184 }, 1185 } 1186 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1187 referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) 1188 path := "/v2/test/manifests/" + referrersTag 1189 if r.Method != http.MethodGet || r.URL.Path != path { 1190 if r.URL.Path != "/v2/test/referrers/"+manifestDesc.Digest.String() { 1191 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1192 } 1193 w.WriteHeader(http.StatusNotFound) 1194 return 1195 } 1196 1197 result := ocispec.Index{ 1198 Versioned: specs.Versioned{ 1199 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 1200 }, 1201 MediaType: ocispec.MediaTypeImageIndex, 1202 Manifests: referrers, 1203 } 1204 if err := json.NewEncoder(w).Encode(result); err != nil { 1205 t.Errorf("failed to write response: %v", err) 1206 } 1207 })) 1208 defer ts.Close() 1209 uri, err := url.Parse(ts.URL) 1210 if err != nil { 1211 t.Fatalf("invalid test http server: %v", err) 1212 } 1213 ctx := context.Background() 1214 1215 // test auto detect 1216 // remote server does not support Referrers, should fallback to tag schema 1217 repo, err := NewRepository(uri.Host + "/test") 1218 if err != nil { 1219 t.Fatalf("NewRepository() error = %v", err) 1220 } 1221 repo.PlainHTTP = true 1222 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1223 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1224 } 1225 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1226 if !reflect.DeepEqual(got, referrers) { 1227 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 1228 } 1229 return nil 1230 }); err != nil { 1231 t.Errorf("Repository.Referrers() error = %v", err) 1232 } 1233 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1234 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1235 } 1236 1237 // test force attempt Referrers 1238 // remote server does not support Referrers, should return error 1239 repo, err = NewRepository(uri.Host + "/test") 1240 if err != nil { 1241 t.Fatalf("NewRepository() error = %v", err) 1242 } 1243 repo.PlainHTTP = true 1244 repo.SetReferrersCapability(true) 1245 if state := repo.loadReferrersState(); state != referrersStateSupported { 1246 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1247 } 1248 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1249 return nil 1250 }); err == nil { 1251 t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, true) 1252 } 1253 if state := repo.loadReferrersState(); state != referrersStateSupported { 1254 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1255 } 1256 1257 // test force attempt tag schema 1258 // should request tag schema 1259 repo, err = NewRepository(uri.Host + "/test") 1260 if err != nil { 1261 t.Fatalf("NewRepository() error = %v", err) 1262 } 1263 repo.PlainHTTP = true 1264 repo.SetReferrersCapability(false) 1265 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1266 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1267 } 1268 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1269 if !reflect.DeepEqual(got, referrers) { 1270 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 1271 } 1272 return nil 1273 }); err != nil { 1274 t.Errorf("Repository.Referrers() error = %v", err) 1275 } 1276 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1277 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1278 } 1279 } 1280 1281 func TestRepository_Referrers_TagSchemaFallback_NotFound(t *testing.T) { 1282 manifest := []byte(`{"layers":[]}`) 1283 manifestDesc := ocispec.Descriptor{ 1284 MediaType: ocispec.MediaTypeImageManifest, 1285 Digest: digest.FromBytes(manifest), 1286 Size: int64(len(manifest)), 1287 } 1288 1289 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1290 referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String() 1291 referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) 1292 tagSchemaUrl := "/v2/test/manifests/" + referrersTag 1293 if r.Method == http.MethodGet || 1294 r.URL.Path == referrersUrl || 1295 r.URL.Path == tagSchemaUrl { 1296 w.WriteHeader(http.StatusNotFound) 1297 return 1298 } 1299 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1300 w.WriteHeader(http.StatusNotFound) 1301 })) 1302 defer ts.Close() 1303 uri, err := url.Parse(ts.URL) 1304 if err != nil { 1305 t.Fatalf("invalid test http server: %v", err) 1306 } 1307 ctx := context.Background() 1308 1309 // test auto detect 1310 // tag schema referrers not found, should be no error 1311 repo, err := NewRepository(uri.Host + "/test") 1312 if err != nil { 1313 t.Fatalf("NewRepository() error = %v", err) 1314 } 1315 repo.PlainHTTP = true 1316 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1317 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1318 } 1319 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1320 return nil 1321 }); err != nil { 1322 t.Errorf("Repository.Referrers() error = %v", err) 1323 } 1324 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1325 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1326 } 1327 1328 // test force attempt tag schema 1329 // tag schema referrers not found, should be no error 1330 repo, err = NewRepository(uri.Host + "/test") 1331 if err != nil { 1332 t.Fatalf("NewRepository() error = %v", err) 1333 } 1334 repo.PlainHTTP = true 1335 repo.SetReferrersCapability(false) 1336 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1337 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1338 } 1339 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1340 return nil 1341 }); err != nil { 1342 t.Errorf("Repository.Referrers() error = %v", err) 1343 } 1344 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1345 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1346 } 1347 } 1348 1349 func TestRepository_Referrers_BadRequest(t *testing.T) { 1350 manifest := []byte(`{"layers":[]}`) 1351 manifestDesc := ocispec.Descriptor{ 1352 MediaType: ocispec.MediaTypeImageManifest, 1353 Digest: digest.FromBytes(manifest), 1354 Size: int64(len(manifest)), 1355 } 1356 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1357 referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String() 1358 referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) 1359 tagSchemaUrl := "/v2/test/manifests/" + referrersTag 1360 if r.Method == http.MethodGet || 1361 r.URL.Path == referrersUrl || 1362 r.URL.Path == tagSchemaUrl { 1363 w.WriteHeader(http.StatusBadRequest) 1364 return 1365 } 1366 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1367 w.WriteHeader(http.StatusNotFound) 1368 })) 1369 defer ts.Close() 1370 uri, err := url.Parse(ts.URL) 1371 if err != nil { 1372 t.Fatalf("invalid test http server: %v", err) 1373 } 1374 ctx := context.Background() 1375 1376 // test auto detect 1377 // Referrers returns error 1378 repo, err := NewRepository(uri.Host + "/test") 1379 if err != nil { 1380 t.Fatalf("NewRepository() error = %v", err) 1381 } 1382 repo.PlainHTTP = true 1383 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1384 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1385 } 1386 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1387 return nil 1388 }); err == nil { 1389 t.Errorf("Repository.Referrers() error = nil, wantErr %v", true) 1390 } 1391 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1392 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1393 } 1394 1395 // test force attempt Referrers 1396 // Referrers returns error 1397 repo, err = NewRepository(uri.Host + "/test") 1398 if err != nil { 1399 t.Fatalf("NewRepository() error = %v", err) 1400 } 1401 repo.PlainHTTP = true 1402 repo.SetReferrersCapability(true) 1403 if state := repo.loadReferrersState(); state != referrersStateSupported { 1404 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1405 } 1406 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1407 return nil 1408 }); err == nil { 1409 t.Errorf("Repository.Referrers() error = nil, wantErr %v", true) 1410 } 1411 if state := repo.loadReferrersState(); state != referrersStateSupported { 1412 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1413 } 1414 1415 // test force attempt tag schema 1416 // Referrers returns error 1417 repo, err = NewRepository(uri.Host + "/test") 1418 if err != nil { 1419 t.Fatalf("NewRepository() error = %v", err) 1420 } 1421 repo.PlainHTTP = true 1422 repo.SetReferrersCapability(false) 1423 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1424 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1425 } 1426 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1427 return nil 1428 }); err == nil { 1429 t.Errorf("Repository.Referrers() error = nil, wantErr %v", true) 1430 } 1431 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1432 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1433 } 1434 } 1435 1436 func TestRepository_Referrers_RepositoryNotFound(t *testing.T) { 1437 manifest := []byte(`{"layers":[]}`) 1438 manifestDesc := ocispec.Descriptor{ 1439 MediaType: ocispec.MediaTypeImageManifest, 1440 Digest: digest.FromBytes(manifest), 1441 Size: int64(len(manifest)), 1442 } 1443 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1444 referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String() 1445 referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) 1446 tagSchemaUrl := "/v2/test/manifests/" + referrersTag 1447 if r.Method == http.MethodGet && 1448 (r.URL.Path == referrersUrl || r.URL.Path == tagSchemaUrl) { 1449 w.WriteHeader(http.StatusNotFound) 1450 w.Write([]byte(`{ "errors": [ { "code": "NAME_UNKNOWN", "message": "repository name not known to registry" } ] }`)) 1451 return 1452 } 1453 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1454 w.WriteHeader(http.StatusNotFound) 1455 })) 1456 defer ts.Close() 1457 uri, err := url.Parse(ts.URL) 1458 if err != nil { 1459 t.Fatalf("invalid test http server: %v", err) 1460 } 1461 ctx := context.Background() 1462 1463 // test auto detect 1464 // repository not found, should return error 1465 repo, err := NewRepository(uri.Host + "/test") 1466 if err != nil { 1467 t.Fatalf("NewRepository() error = %v", err) 1468 } 1469 repo.PlainHTTP = true 1470 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1471 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1472 } 1473 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1474 return nil 1475 }); err == nil { 1476 t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, true) 1477 } 1478 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1479 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1480 } 1481 1482 // test force attempt Referrers 1483 // repository not found, should return error 1484 repo, err = NewRepository(uri.Host + "/test") 1485 if err != nil { 1486 t.Fatalf("NewRepository() error = %v", err) 1487 } 1488 repo.PlainHTTP = true 1489 repo.SetReferrersCapability(true) 1490 if state := repo.loadReferrersState(); state != referrersStateSupported { 1491 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1492 } 1493 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1494 return nil 1495 }); err == nil { 1496 t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, true) 1497 } 1498 if state := repo.loadReferrersState(); state != referrersStateSupported { 1499 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1500 } 1501 1502 // test force attempt tag schema 1503 // repository not found, but should not return error 1504 repo, err = NewRepository(uri.Host + "/test") 1505 if err != nil { 1506 t.Fatalf("NewRepository() error = %v", err) 1507 } 1508 repo.PlainHTTP = true 1509 repo.SetReferrersCapability(false) 1510 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1511 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1512 } 1513 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1514 return nil 1515 }); err != nil { 1516 t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, nil) 1517 } 1518 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1519 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1520 } 1521 } 1522 1523 func TestRepository_Referrers_ServerFiltering(t *testing.T) { 1524 manifest := []byte(`{"layers":[]}`) 1525 manifestDesc := ocispec.Descriptor{ 1526 MediaType: ocispec.MediaTypeImageManifest, 1527 Digest: digest.FromBytes(manifest), 1528 Size: int64(len(manifest)), 1529 } 1530 referrerSet := [][]ocispec.Descriptor{ 1531 { 1532 { 1533 MediaType: ocispec.MediaTypeArtifactManifest, 1534 Size: 1, 1535 Digest: digest.FromString("1"), 1536 ArtifactType: "application/vnd.test", 1537 }, 1538 { 1539 MediaType: ocispec.MediaTypeArtifactManifest, 1540 Size: 2, 1541 Digest: digest.FromString("2"), 1542 ArtifactType: "application/vnd.test", 1543 }, 1544 }, 1545 { 1546 { 1547 MediaType: ocispec.MediaTypeArtifactManifest, 1548 Size: 3, 1549 Digest: digest.FromString("3"), 1550 ArtifactType: "application/vnd.test", 1551 }, 1552 { 1553 MediaType: ocispec.MediaTypeArtifactManifest, 1554 Size: 4, 1555 Digest: digest.FromString("4"), 1556 ArtifactType: "application/vnd.test", 1557 }, 1558 }, 1559 { 1560 { 1561 MediaType: ocispec.MediaTypeArtifactManifest, 1562 Size: 5, 1563 Digest: digest.FromString("5"), 1564 ArtifactType: "application/vnd.test", 1565 }, 1566 }, 1567 } 1568 var ts *httptest.Server 1569 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1570 path := "/v2/test/referrers/" + manifestDesc.Digest.String() 1571 queryParams, err := url.ParseQuery(r.URL.RawQuery) 1572 if err != nil { 1573 t.Fatal("failed to parse url query") 1574 } 1575 if r.Method != http.MethodGet || 1576 r.URL.Path != path || 1577 reflect.DeepEqual(queryParams["artifactType"], []string{"application%2Fvnd.test"}) { 1578 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1579 w.WriteHeader(http.StatusNotFound) 1580 return 1581 } 1582 q := r.URL.Query() 1583 n, err := strconv.Atoi(q.Get("n")) 1584 if err != nil || n != 2 { 1585 t.Errorf("bad page size: %s", q.Get("n")) 1586 w.WriteHeader(http.StatusBadRequest) 1587 return 1588 } 1589 var referrers []ocispec.Descriptor 1590 switch q.Get("test") { 1591 case "foo": 1592 referrers = referrerSet[1] 1593 w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path)) 1594 case "bar": 1595 referrers = referrerSet[2] 1596 default: 1597 referrers = referrerSet[0] 1598 w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path)) 1599 } 1600 result := ocispec.Index{ 1601 Versioned: specs.Versioned{ 1602 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 1603 }, 1604 MediaType: ocispec.MediaTypeImageIndex, 1605 Manifests: referrers, 1606 Annotations: map[string]string{ 1607 ocispec.AnnotationReferrersFiltersApplied: "artifactType", 1608 }, 1609 } 1610 if err := json.NewEncoder(w).Encode(result); err != nil { 1611 t.Errorf("failed to write response: %v", err) 1612 } 1613 })) 1614 defer ts.Close() 1615 uri, err := url.Parse(ts.URL) 1616 if err != nil { 1617 t.Fatalf("invalid test http server: %v", err) 1618 } 1619 1620 repo, err := NewRepository(uri.Host + "/test") 1621 if err != nil { 1622 t.Fatalf("NewRepository() error = %v", err) 1623 } 1624 repo.PlainHTTP = true 1625 repo.ReferrerListPageSize = 2 1626 1627 ctx := context.Background() 1628 index := 0 1629 if err := repo.Referrers(ctx, manifestDesc, "application/vnd.test", func(got []ocispec.Descriptor) error { 1630 if index >= len(referrerSet) { 1631 t.Fatalf("out of index bound: %d", index) 1632 } 1633 referrers := referrerSet[index] 1634 index++ 1635 if !reflect.DeepEqual(got, referrers) { 1636 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 1637 } 1638 return nil 1639 }); err != nil { 1640 t.Errorf("Repository.Referrers() error = %v", err) 1641 } 1642 if index != len(referrerSet) { 1643 t.Errorf("fn invoked %d time(s), want %d", index, len(referrerSet)) 1644 } 1645 } 1646 1647 func TestRepository_Referrers_ClientFiltering(t *testing.T) { 1648 manifest := []byte(`{"layers":[]}`) 1649 manifestDesc := ocispec.Descriptor{ 1650 MediaType: ocispec.MediaTypeImageManifest, 1651 Digest: digest.FromBytes(manifest), 1652 Size: int64(len(manifest)), 1653 } 1654 referrerSet := [][]ocispec.Descriptor{ 1655 { 1656 { 1657 MediaType: ocispec.MediaTypeArtifactManifest, 1658 Size: 1, 1659 Digest: digest.FromString("1"), 1660 ArtifactType: "application/vnd.test", 1661 }, 1662 { 1663 MediaType: ocispec.MediaTypeArtifactManifest, 1664 Size: 2, 1665 Digest: digest.FromString("2"), 1666 ArtifactType: "application/vnd.foo", 1667 }, 1668 }, 1669 { 1670 { 1671 MediaType: ocispec.MediaTypeArtifactManifest, 1672 Size: 3, 1673 Digest: digest.FromString("3"), 1674 ArtifactType: "application/vnd.test", 1675 }, 1676 { 1677 MediaType: ocispec.MediaTypeArtifactManifest, 1678 Size: 4, 1679 Digest: digest.FromString("4"), 1680 ArtifactType: "application/vnd.bar", 1681 }, 1682 }, 1683 { 1684 { 1685 MediaType: ocispec.MediaTypeArtifactManifest, 1686 Size: 5, 1687 Digest: digest.FromString("5"), 1688 ArtifactType: "application/vnd.baz", 1689 }, 1690 }, 1691 } 1692 filteredReferrerSet := [][]ocispec.Descriptor{ 1693 { 1694 { 1695 MediaType: ocispec.MediaTypeArtifactManifest, 1696 Size: 1, 1697 Digest: digest.FromString("1"), 1698 ArtifactType: "application/vnd.test", 1699 }, 1700 }, 1701 { 1702 { 1703 MediaType: ocispec.MediaTypeArtifactManifest, 1704 Size: 3, 1705 Digest: digest.FromString("3"), 1706 ArtifactType: "application/vnd.test", 1707 }, 1708 }, 1709 } 1710 var ts *httptest.Server 1711 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1712 path := "/v2/test/referrers/" + manifestDesc.Digest.String() 1713 queryParams, err := url.ParseQuery(r.URL.RawQuery) 1714 if err != nil { 1715 t.Fatal("failed to parse url query") 1716 } 1717 if r.Method != http.MethodGet || 1718 r.URL.Path != path || 1719 reflect.DeepEqual(queryParams["artifactType"], []string{"application%2Fvnd.test"}) { 1720 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1721 w.WriteHeader(http.StatusNotFound) 1722 return 1723 } 1724 q := r.URL.Query() 1725 n, err := strconv.Atoi(q.Get("n")) 1726 if err != nil || n != 2 { 1727 t.Errorf("bad page size: %s", q.Get("n")) 1728 w.WriteHeader(http.StatusBadRequest) 1729 return 1730 } 1731 var referrers []ocispec.Descriptor 1732 switch q.Get("test") { 1733 case "foo": 1734 referrers = referrerSet[1] 1735 w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path)) 1736 case "bar": 1737 referrers = referrerSet[2] 1738 default: 1739 referrers = referrerSet[0] 1740 w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path)) 1741 } 1742 result := ocispec.Index{ 1743 Versioned: specs.Versioned{ 1744 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 1745 }, 1746 MediaType: ocispec.MediaTypeImageIndex, 1747 Manifests: referrers, 1748 } 1749 if err := json.NewEncoder(w).Encode(result); err != nil { 1750 t.Errorf("failed to write response: %v", err) 1751 } 1752 })) 1753 defer ts.Close() 1754 uri, err := url.Parse(ts.URL) 1755 if err != nil { 1756 t.Fatalf("invalid test http server: %v", err) 1757 } 1758 1759 repo, err := NewRepository(uri.Host + "/test") 1760 if err != nil { 1761 t.Fatalf("NewRepository() error = %v", err) 1762 } 1763 repo.PlainHTTP = true 1764 repo.ReferrerListPageSize = 2 1765 1766 ctx := context.Background() 1767 index := 0 1768 if err := repo.Referrers(ctx, manifestDesc, "application/vnd.test", func(got []ocispec.Descriptor) error { 1769 if index >= len(filteredReferrerSet) { 1770 t.Fatalf("out of index bound: %d", index) 1771 } 1772 referrers := filteredReferrerSet[index] 1773 index++ 1774 if !reflect.DeepEqual(got, referrers) { 1775 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 1776 } 1777 return nil 1778 }); err != nil { 1779 t.Errorf("Repository.Referrers() error = %v", err) 1780 } 1781 if index != len(filteredReferrerSet) { 1782 t.Errorf("fn invoked %d time(s), want %d", index, len(referrerSet)) 1783 } 1784 } 1785 1786 func TestRepository_Referrers_TagSchemaFallback_ClientFiltering(t *testing.T) { 1787 manifest := []byte(`{"layers":[]}`) 1788 manifestDesc := ocispec.Descriptor{ 1789 MediaType: ocispec.MediaTypeImageManifest, 1790 Digest: digest.FromBytes(manifest), 1791 Size: int64(len(manifest)), 1792 } 1793 1794 referrers := []ocispec.Descriptor{ 1795 { 1796 MediaType: ocispec.MediaTypeArtifactManifest, 1797 Size: 1, 1798 Digest: digest.FromString("1"), 1799 ArtifactType: "application/vnd.test", 1800 }, 1801 { 1802 MediaType: ocispec.MediaTypeArtifactManifest, 1803 Size: 2, 1804 Digest: digest.FromString("2"), 1805 ArtifactType: "application/vnd.foo", 1806 }, 1807 { 1808 MediaType: ocispec.MediaTypeArtifactManifest, 1809 Size: 3, 1810 Digest: digest.FromString("3"), 1811 ArtifactType: "application/vnd.test", 1812 }, 1813 { 1814 MediaType: ocispec.MediaTypeArtifactManifest, 1815 Size: 4, 1816 Digest: digest.FromString("4"), 1817 ArtifactType: "application/vnd.bar", 1818 }, 1819 { 1820 MediaType: ocispec.MediaTypeArtifactManifest, 1821 Size: 5, 1822 Digest: digest.FromString("5"), 1823 ArtifactType: "application/vnd.baz", 1824 }, 1825 } 1826 filteredReferrers := []ocispec.Descriptor{ 1827 { 1828 MediaType: ocispec.MediaTypeArtifactManifest, 1829 Size: 1, 1830 Digest: digest.FromString("1"), 1831 ArtifactType: "application/vnd.test", 1832 }, 1833 { 1834 MediaType: ocispec.MediaTypeArtifactManifest, 1835 Size: 3, 1836 Digest: digest.FromString("3"), 1837 ArtifactType: "application/vnd.test", 1838 }, 1839 } 1840 1841 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1842 referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) 1843 path := "/v2/test/manifests/" + referrersTag 1844 if r.Method != http.MethodGet || r.URL.Path != path { 1845 if r.URL.Path != "/v2/test/referrers/"+manifestDesc.Digest.String() { 1846 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1847 } 1848 w.WriteHeader(http.StatusNotFound) 1849 return 1850 } 1851 1852 result := ocispec.Index{ 1853 Versioned: specs.Versioned{ 1854 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 1855 }, 1856 MediaType: ocispec.MediaTypeImageIndex, 1857 Manifests: referrers, 1858 } 1859 if err := json.NewEncoder(w).Encode(result); err != nil { 1860 t.Errorf("failed to write response: %v", err) 1861 } 1862 })) 1863 defer ts.Close() 1864 uri, err := url.Parse(ts.URL) 1865 if err != nil { 1866 t.Fatalf("invalid test http server: %v", err) 1867 } 1868 1869 repo, err := NewRepository(uri.Host + "/test") 1870 if err != nil { 1871 t.Fatalf("NewRepository() error = %v", err) 1872 } 1873 repo.PlainHTTP = true 1874 1875 ctx := context.Background() 1876 if err := repo.Referrers(ctx, manifestDesc, "application/vnd.test", func(got []ocispec.Descriptor) error { 1877 if !reflect.DeepEqual(got, filteredReferrers) { 1878 t.Errorf("Repository.Referrers() = %v, want %v", got, filteredReferrers) 1879 } 1880 return nil 1881 }); err != nil { 1882 t.Errorf("Repository.Referrers() error = %v", err) 1883 } 1884 } 1885 1886 func Test_BlobStore_Fetch(t *testing.T) { 1887 blob := []byte("hello world") 1888 blobDesc := ocispec.Descriptor{ 1889 MediaType: "test", 1890 Digest: digest.FromBytes(blob), 1891 Size: int64(len(blob)), 1892 } 1893 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1894 if r.Method != http.MethodGet { 1895 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 1896 w.WriteHeader(http.StatusMethodNotAllowed) 1897 return 1898 } 1899 switch r.URL.Path { 1900 case "/v2/test/blobs/" + blobDesc.Digest.String(): 1901 w.Header().Set("Content-Type", "application/octet-stream") 1902 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 1903 if _, err := w.Write(blob); err != nil { 1904 t.Errorf("failed to write %q: %v", r.URL, err) 1905 } 1906 default: 1907 w.WriteHeader(http.StatusNotFound) 1908 } 1909 })) 1910 defer ts.Close() 1911 uri, err := url.Parse(ts.URL) 1912 if err != nil { 1913 t.Fatalf("invalid test http server: %v", err) 1914 } 1915 1916 repo, err := NewRepository(uri.Host + "/test") 1917 if err != nil { 1918 t.Fatalf("NewRepository() error = %v", err) 1919 } 1920 repo.PlainHTTP = true 1921 store := repo.Blobs() 1922 ctx := context.Background() 1923 1924 rc, err := store.Fetch(ctx, blobDesc) 1925 if err != nil { 1926 t.Fatalf("Blobs.Fetch() error = %v", err) 1927 } 1928 buf := bytes.NewBuffer(nil) 1929 if _, err := buf.ReadFrom(rc); err != nil { 1930 t.Errorf("fail to read: %v", err) 1931 } 1932 if err := rc.Close(); err != nil { 1933 t.Errorf("fail to close: %v", err) 1934 } 1935 if got := buf.Bytes(); !bytes.Equal(got, blob) { 1936 t.Errorf("Blobs.Fetch() = %v, want %v", got, blob) 1937 } 1938 1939 content := []byte("foobar") 1940 contentDesc := ocispec.Descriptor{ 1941 MediaType: "test", 1942 Digest: digest.FromBytes(content), 1943 Size: int64(len(content)), 1944 } 1945 _, err = store.Fetch(ctx, contentDesc) 1946 if !errors.Is(err, errdef.ErrNotFound) { 1947 t.Errorf("Blobs.Fetch() error = %v, wantErr %v", err, errdef.ErrNotFound) 1948 } 1949 } 1950 1951 func Test_BlobStore_Fetch_Seek(t *testing.T) { 1952 blob := []byte("hello world") 1953 blobDesc := ocispec.Descriptor{ 1954 MediaType: "test", 1955 Digest: digest.FromBytes(blob), 1956 Size: int64(len(blob)), 1957 } 1958 seekable := false 1959 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1960 if r.Method != http.MethodGet { 1961 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 1962 w.WriteHeader(http.StatusMethodNotAllowed) 1963 return 1964 } 1965 switch r.URL.Path { 1966 case "/v2/test/blobs/" + blobDesc.Digest.String(): 1967 w.Header().Set("Content-Type", "application/octet-stream") 1968 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 1969 if seekable { 1970 w.Header().Set("Accept-Ranges", "bytes") 1971 } 1972 rangeHeader := r.Header.Get("Range") 1973 if !seekable || rangeHeader == "" { 1974 w.WriteHeader(http.StatusOK) 1975 if _, err := w.Write(blob); err != nil { 1976 t.Errorf("failed to write %q: %v", r.URL, err) 1977 } 1978 return 1979 } 1980 var start, end int 1981 _, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end) 1982 if err != nil { 1983 t.Errorf("invalid range header: %s", rangeHeader) 1984 w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) 1985 return 1986 } 1987 if start < 0 || start > end || start >= int(blobDesc.Size) { 1988 t.Errorf("invalid range: %s", rangeHeader) 1989 w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) 1990 return 1991 } 1992 end++ 1993 if end > int(blobDesc.Size) { 1994 end = int(blobDesc.Size) 1995 } 1996 w.WriteHeader(http.StatusPartialContent) 1997 if _, err := w.Write(blob[start:end]); err != nil { 1998 t.Errorf("failed to write %q: %v", r.URL, err) 1999 } 2000 default: 2001 w.WriteHeader(http.StatusNotFound) 2002 } 2003 })) 2004 defer ts.Close() 2005 uri, err := url.Parse(ts.URL) 2006 if err != nil { 2007 t.Fatalf("invalid test http server: %v", err) 2008 } 2009 2010 repo, err := NewRepository(uri.Host + "/test") 2011 if err != nil { 2012 t.Fatalf("NewRepository() error = %v", err) 2013 } 2014 repo.PlainHTTP = true 2015 store := repo.Blobs() 2016 ctx := context.Background() 2017 2018 rc, err := store.Fetch(ctx, blobDesc) 2019 if err != nil { 2020 t.Fatalf("Blobs.Fetch() error = %v", err) 2021 } 2022 if _, ok := rc.(io.Seeker); ok { 2023 t.Errorf("Blobs.Fetch() returns io.Seeker on non-seekable content") 2024 } 2025 buf := bytes.NewBuffer(nil) 2026 if _, err := buf.ReadFrom(rc); err != nil { 2027 t.Errorf("fail to read: %v", err) 2028 } 2029 if err := rc.Close(); err != nil { 2030 t.Errorf("fail to close: %v", err) 2031 } 2032 if got := buf.Bytes(); !bytes.Equal(got, blob) { 2033 t.Errorf("Blobs.Fetch() = %v, want %v", got, blob) 2034 } 2035 2036 seekable = true 2037 rc, err = store.Fetch(ctx, blobDesc) 2038 if err != nil { 2039 t.Fatalf("Blobs.Fetch() error = %v", err) 2040 } 2041 s, ok := rc.(io.Seeker) 2042 if !ok { 2043 t.Fatalf("Blobs.Fetch() = %v, want io.Seeker", rc) 2044 } 2045 buf.Reset() 2046 if _, err := buf.ReadFrom(rc); err != nil { 2047 t.Errorf("fail to read: %v", err) 2048 } 2049 if got := buf.Bytes(); !bytes.Equal(got, blob) { 2050 t.Errorf("Blobs.Fetch() = %v, want %v", got, blob) 2051 } 2052 2053 _, err = s.Seek(3, io.SeekStart) 2054 if err != nil { 2055 t.Errorf("fail to seek: %v", err) 2056 } 2057 buf.Reset() 2058 if _, err := buf.ReadFrom(rc); err != nil { 2059 t.Errorf("fail to read: %v", err) 2060 } 2061 if got := buf.Bytes(); !bytes.Equal(got, blob[3:]) { 2062 t.Errorf("Blobs.Fetch() = %v, want %v", got, blob[3:]) 2063 } 2064 2065 if err := rc.Close(); err != nil { 2066 t.Errorf("fail to close: %v", err) 2067 } 2068 } 2069 2070 func Test_BlobStore_Fetch_ZeroSizedBlob(t *testing.T) { 2071 blob := []byte("") 2072 blobDesc := ocispec.Descriptor{ 2073 MediaType: "test", 2074 Digest: digest.FromBytes(blob), 2075 Size: int64(len(blob)), 2076 } 2077 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2078 if r.Method != http.MethodGet { 2079 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2080 w.WriteHeader(http.StatusMethodNotAllowed) 2081 return 2082 } 2083 2084 switch r.URL.Path { 2085 case "/v2/test/blobs/" + blobDesc.Digest.String(): 2086 if rangeHeader := r.Header.Get("Range"); rangeHeader != "" { 2087 t.Errorf("unexpected range header") 2088 w.WriteHeader(http.StatusBadRequest) 2089 return 2090 } 2091 w.Header().Set("Content-Type", "application/octet-stream") 2092 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2093 default: 2094 w.WriteHeader(http.StatusNotFound) 2095 } 2096 })) 2097 defer ts.Close() 2098 uri, err := url.Parse(ts.URL) 2099 if err != nil { 2100 t.Fatalf("invalid test http server: %v", err) 2101 } 2102 2103 repo, err := NewRepository(uri.Host + "/test") 2104 if err != nil { 2105 t.Fatalf("NewRepository() error = %v", err) 2106 } 2107 repo.PlainHTTP = true 2108 store := repo.Blobs() 2109 ctx := context.Background() 2110 2111 rc, err := store.Fetch(ctx, blobDesc) 2112 if err != nil { 2113 t.Fatalf("Blobs.Fetch() error = %v", err) 2114 } 2115 buf := bytes.NewBuffer(nil) 2116 if _, err := buf.ReadFrom(rc); err != nil { 2117 t.Errorf("fail to read: %v", err) 2118 } 2119 if err := rc.Close(); err != nil { 2120 t.Errorf("fail to close: %v", err) 2121 } 2122 if got := buf.Bytes(); !bytes.Equal(got, blob) { 2123 t.Errorf("Blobs.Fetch() = %v, want %v", got, blob) 2124 } 2125 } 2126 2127 func Test_BlobStore_Push(t *testing.T) { 2128 blob := []byte("hello world") 2129 blobDesc := ocispec.Descriptor{ 2130 MediaType: "test", 2131 Digest: digest.FromBytes(blob), 2132 Size: int64(len(blob)), 2133 } 2134 var gotBlob []byte 2135 uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c" 2136 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2137 switch { 2138 case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/": 2139 w.Header().Set("Location", "/v2/test/blobs/uploads/"+uuid) 2140 w.WriteHeader(http.StatusAccepted) 2141 return 2142 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid: 2143 if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" { 2144 w.WriteHeader(http.StatusBadRequest) 2145 break 2146 } 2147 if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() { 2148 w.WriteHeader(http.StatusBadRequest) 2149 break 2150 } 2151 buf := bytes.NewBuffer(nil) 2152 if _, err := buf.ReadFrom(r.Body); err != nil { 2153 t.Errorf("fail to read: %v", err) 2154 } 2155 gotBlob = buf.Bytes() 2156 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2157 w.WriteHeader(http.StatusCreated) 2158 return 2159 default: 2160 w.WriteHeader(http.StatusForbidden) 2161 } 2162 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2163 })) 2164 defer ts.Close() 2165 uri, err := url.Parse(ts.URL) 2166 if err != nil { 2167 t.Fatalf("invalid test http server: %v", err) 2168 } 2169 2170 repo, err := NewRepository(uri.Host + "/test") 2171 if err != nil { 2172 t.Fatalf("NewRepository() error = %v", err) 2173 } 2174 repo.PlainHTTP = true 2175 store := repo.Blobs() 2176 ctx := context.Background() 2177 2178 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 2179 if err != nil { 2180 t.Fatalf("Blobs.Push() error = %v", err) 2181 } 2182 if !bytes.Equal(gotBlob, blob) { 2183 t.Errorf("Blobs.Push() = %v, want %v", gotBlob, blob) 2184 } 2185 } 2186 2187 func Test_BlobStore_Exists(t *testing.T) { 2188 blob := []byte("hello world") 2189 blobDesc := ocispec.Descriptor{ 2190 MediaType: "test", 2191 Digest: digest.FromBytes(blob), 2192 Size: int64(len(blob)), 2193 } 2194 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2195 if r.Method != http.MethodHead { 2196 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2197 w.WriteHeader(http.StatusMethodNotAllowed) 2198 return 2199 } 2200 switch r.URL.Path { 2201 case "/v2/test/blobs/" + blobDesc.Digest.String(): 2202 w.Header().Set("Content-Type", "application/octet-stream") 2203 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2204 w.Header().Set("Content-Length", strconv.Itoa(int(blobDesc.Size))) 2205 default: 2206 w.WriteHeader(http.StatusNotFound) 2207 } 2208 })) 2209 defer ts.Close() 2210 uri, err := url.Parse(ts.URL) 2211 if err != nil { 2212 t.Fatalf("invalid test http server: %v", err) 2213 } 2214 2215 repo, err := NewRepository(uri.Host + "/test") 2216 if err != nil { 2217 t.Fatalf("NewRepository() error = %v", err) 2218 } 2219 repo.PlainHTTP = true 2220 store := repo.Blobs() 2221 ctx := context.Background() 2222 2223 exists, err := store.Exists(ctx, blobDesc) 2224 if err != nil { 2225 t.Fatalf("Blobs.Exists() error = %v", err) 2226 } 2227 if !exists { 2228 t.Errorf("Blobs.Exists() = %v, want %v", exists, true) 2229 } 2230 2231 content := []byte("foobar") 2232 contentDesc := ocispec.Descriptor{ 2233 MediaType: "test", 2234 Digest: digest.FromBytes(content), 2235 Size: int64(len(content)), 2236 } 2237 exists, err = store.Exists(ctx, contentDesc) 2238 if err != nil { 2239 t.Fatalf("Blobs.Exists() error = %v", err) 2240 } 2241 if exists { 2242 t.Errorf("Blobs.Exists() = %v, want %v", exists, false) 2243 } 2244 } 2245 2246 func Test_BlobStore_Delete(t *testing.T) { 2247 blob := []byte("hello world") 2248 blobDesc := ocispec.Descriptor{ 2249 MediaType: "test", 2250 Digest: digest.FromBytes(blob), 2251 Size: int64(len(blob)), 2252 } 2253 blobDeleted := false 2254 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2255 if r.Method != http.MethodDelete { 2256 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2257 w.WriteHeader(http.StatusMethodNotAllowed) 2258 return 2259 } 2260 switch r.URL.Path { 2261 case "/v2/test/blobs/" + blobDesc.Digest.String(): 2262 blobDeleted = true 2263 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2264 w.WriteHeader(http.StatusAccepted) 2265 default: 2266 w.WriteHeader(http.StatusNotFound) 2267 } 2268 })) 2269 defer ts.Close() 2270 uri, err := url.Parse(ts.URL) 2271 if err != nil { 2272 t.Fatalf("invalid test http server: %v", err) 2273 } 2274 2275 repo, err := NewRepository(uri.Host + "/test") 2276 if err != nil { 2277 t.Fatalf("NewRepository() error = %v", err) 2278 } 2279 repo.PlainHTTP = true 2280 store := repo.Blobs() 2281 ctx := context.Background() 2282 2283 err = store.Delete(ctx, blobDesc) 2284 if err != nil { 2285 t.Fatalf("Blobs.Delete() error = %v", err) 2286 } 2287 if !blobDeleted { 2288 t.Errorf("Blobs.Delete() = %v, want %v", blobDeleted, true) 2289 } 2290 2291 content := []byte("foobar") 2292 contentDesc := ocispec.Descriptor{ 2293 MediaType: "test", 2294 Digest: digest.FromBytes(content), 2295 Size: int64(len(content)), 2296 } 2297 err = store.Delete(ctx, contentDesc) 2298 if !errors.Is(err, errdef.ErrNotFound) { 2299 t.Errorf("Blobs.Delete() error = %v, wantErr %v", err, errdef.ErrNotFound) 2300 } 2301 } 2302 2303 func Test_BlobStore_Resolve(t *testing.T) { 2304 blob := []byte("hello world") 2305 blobDesc := ocispec.Descriptor{ 2306 MediaType: "test", 2307 Digest: digest.FromBytes(blob), 2308 Size: int64(len(blob)), 2309 } 2310 ref := "foobar" 2311 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2312 if r.Method != http.MethodHead { 2313 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2314 w.WriteHeader(http.StatusMethodNotAllowed) 2315 return 2316 } 2317 switch r.URL.Path { 2318 case "/v2/test/blobs/" + blobDesc.Digest.String(): 2319 w.Header().Set("Content-Type", "application/octet-stream") 2320 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2321 w.Header().Set("Content-Length", strconv.Itoa(int(blobDesc.Size))) 2322 default: 2323 w.WriteHeader(http.StatusNotFound) 2324 } 2325 })) 2326 defer ts.Close() 2327 uri, err := url.Parse(ts.URL) 2328 if err != nil { 2329 t.Fatalf("invalid test http server: %v", err) 2330 } 2331 2332 repoName := uri.Host + "/test" 2333 repo, err := NewRepository(repoName) 2334 if err != nil { 2335 t.Fatalf("NewRepository() error = %v", err) 2336 } 2337 repo.PlainHTTP = true 2338 store := repo.Blobs() 2339 ctx := context.Background() 2340 2341 got, err := store.Resolve(ctx, blobDesc.Digest.String()) 2342 if err != nil { 2343 t.Fatalf("Blobs.Resolve() error = %v", err) 2344 } 2345 if got.Digest != blobDesc.Digest || got.Size != blobDesc.Size { 2346 t.Errorf("Blobs.Resolve() = %v, want %v", got, blobDesc) 2347 } 2348 2349 _, err = store.Resolve(ctx, ref) 2350 if !errors.Is(err, digest.ErrDigestInvalidFormat) { 2351 t.Errorf("Blobs.Resolve() error = %v, wantErr %v", err, digest.ErrDigestInvalidFormat) 2352 } 2353 2354 fqdnRef := repoName + "@" + blobDesc.Digest.String() 2355 got, err = store.Resolve(ctx, fqdnRef) 2356 if err != nil { 2357 t.Fatalf("Blobs.Resolve() error = %v", err) 2358 } 2359 if got.Digest != blobDesc.Digest || got.Size != blobDesc.Size { 2360 t.Errorf("Blobs.Resolve() = %v, want %v", got, blobDesc) 2361 } 2362 2363 content := []byte("foobar") 2364 contentDesc := ocispec.Descriptor{ 2365 MediaType: "test", 2366 Digest: digest.FromBytes(content), 2367 Size: int64(len(content)), 2368 } 2369 _, err = store.Resolve(ctx, contentDesc.Digest.String()) 2370 if !errors.Is(err, errdef.ErrNotFound) { 2371 t.Errorf("Blobs.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound) 2372 } 2373 } 2374 2375 func Test_BlobStore_FetchReference(t *testing.T) { 2376 blob := []byte("hello world") 2377 blobDesc := ocispec.Descriptor{ 2378 MediaType: "test", 2379 Digest: digest.FromBytes(blob), 2380 Size: int64(len(blob)), 2381 } 2382 ref := "foobar" 2383 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2384 if r.Method != http.MethodGet { 2385 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2386 w.WriteHeader(http.StatusMethodNotAllowed) 2387 return 2388 } 2389 switch r.URL.Path { 2390 case "/v2/test/blobs/" + blobDesc.Digest.String(): 2391 w.Header().Set("Content-Type", "application/octet-stream") 2392 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2393 if _, err := w.Write(blob); err != nil { 2394 t.Errorf("failed to write %q: %v", r.URL, err) 2395 } 2396 default: 2397 w.WriteHeader(http.StatusNotFound) 2398 } 2399 })) 2400 defer ts.Close() 2401 uri, err := url.Parse(ts.URL) 2402 if err != nil { 2403 t.Fatalf("invalid test http server: %v", err) 2404 } 2405 2406 repoName := uri.Host + "/test" 2407 repo, err := NewRepository(repoName) 2408 if err != nil { 2409 t.Fatalf("NewRepository() error = %v", err) 2410 } 2411 repo.PlainHTTP = true 2412 store := repo.Blobs() 2413 ctx := context.Background() 2414 2415 // test with digest 2416 gotDesc, rc, err := store.FetchReference(ctx, blobDesc.Digest.String()) 2417 if err != nil { 2418 t.Fatalf("Blobs.FetchReference() error = %v", err) 2419 } 2420 if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size { 2421 t.Errorf("Blobs.FetchReference() = %v, want %v", gotDesc, blobDesc) 2422 } 2423 buf := bytes.NewBuffer(nil) 2424 if _, err := buf.ReadFrom(rc); err != nil { 2425 t.Errorf("fail to read: %v", err) 2426 } 2427 if err := rc.Close(); err != nil { 2428 t.Errorf("fail to close: %v", err) 2429 } 2430 if got := buf.Bytes(); !bytes.Equal(got, blob) { 2431 t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob) 2432 } 2433 2434 // test with non-digest reference 2435 _, _, err = store.FetchReference(ctx, ref) 2436 if !errors.Is(err, digest.ErrDigestInvalidFormat) { 2437 t.Errorf("Blobs.FetchReference() error = %v, wantErr %v", err, digest.ErrDigestInvalidFormat) 2438 } 2439 2440 // test with FQDN reference 2441 fqdnRef := repoName + "@" + blobDesc.Digest.String() 2442 gotDesc, rc, err = store.FetchReference(ctx, fqdnRef) 2443 if err != nil { 2444 t.Fatalf("Blobs.FetchReference() error = %v", err) 2445 } 2446 if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size { 2447 t.Errorf("Blobs.FetchReference() = %v, want %v", gotDesc, blobDesc) 2448 } 2449 buf.Reset() 2450 if _, err := buf.ReadFrom(rc); err != nil { 2451 t.Errorf("fail to read: %v", err) 2452 } 2453 if err := rc.Close(); err != nil { 2454 t.Errorf("fail to close: %v", err) 2455 } 2456 if got := buf.Bytes(); !bytes.Equal(got, blob) { 2457 t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob) 2458 } 2459 2460 content := []byte("foobar") 2461 contentDesc := ocispec.Descriptor{ 2462 MediaType: "test", 2463 Digest: digest.FromBytes(content), 2464 Size: int64(len(content)), 2465 } 2466 2467 // test with other digest 2468 _, _, err = store.FetchReference(ctx, contentDesc.Digest.String()) 2469 if !errors.Is(err, errdef.ErrNotFound) { 2470 t.Errorf("Blobs.FetchReference() error = %v, wantErr %v", err, errdef.ErrNotFound) 2471 } 2472 } 2473 2474 func Test_BlobStore_FetchReference_Seek(t *testing.T) { 2475 blob := []byte("hello world") 2476 blobDesc := ocispec.Descriptor{ 2477 MediaType: "test", 2478 Digest: digest.FromBytes(blob), 2479 Size: int64(len(blob)), 2480 } 2481 seekable := false 2482 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2483 if r.Method != http.MethodGet { 2484 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2485 w.WriteHeader(http.StatusMethodNotAllowed) 2486 return 2487 } 2488 switch r.URL.Path { 2489 case "/v2/test/blobs/" + blobDesc.Digest.String(): 2490 w.Header().Set("Content-Type", "application/octet-stream") 2491 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2492 if seekable { 2493 w.Header().Set("Accept-Ranges", "bytes") 2494 } 2495 rangeHeader := r.Header.Get("Range") 2496 if !seekable || rangeHeader == "" { 2497 w.WriteHeader(http.StatusOK) 2498 if _, err := w.Write(blob); err != nil { 2499 t.Errorf("failed to write %q: %v", r.URL, err) 2500 } 2501 return 2502 } 2503 var start int 2504 _, err := fmt.Sscanf(rangeHeader, "bytes=%d-", &start) 2505 if err != nil { 2506 t.Errorf("invalid range header: %s", rangeHeader) 2507 w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) 2508 return 2509 } 2510 if start < 0 || start >= int(blobDesc.Size) { 2511 t.Errorf("invalid range: %s", rangeHeader) 2512 w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) 2513 return 2514 } 2515 2516 w.WriteHeader(http.StatusPartialContent) 2517 if _, err := w.Write(blob[start:]); err != nil { 2518 t.Errorf("failed to write %q: %v", r.URL, err) 2519 } 2520 default: 2521 w.WriteHeader(http.StatusNotFound) 2522 } 2523 })) 2524 defer ts.Close() 2525 uri, err := url.Parse(ts.URL) 2526 if err != nil { 2527 t.Fatalf("invalid test http server: %v", err) 2528 } 2529 2530 repo, err := NewRepository(uri.Host + "/test") 2531 if err != nil { 2532 t.Fatalf("NewRepository() error = %v", err) 2533 } 2534 repo.PlainHTTP = true 2535 store := repo.Blobs() 2536 ctx := context.Background() 2537 2538 // test non-seekable content 2539 gotDesc, rc, err := store.FetchReference(ctx, blobDesc.Digest.String()) 2540 if err != nil { 2541 t.Fatalf("Blobs.FetchReference() error = %v", err) 2542 } 2543 if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size { 2544 t.Errorf("Blobs.FetchReference() = %v, want %v", gotDesc, blobDesc) 2545 } 2546 if _, ok := rc.(io.Seeker); ok { 2547 t.Errorf("Blobs.FetchReference() returns io.Seeker on non-seekable content") 2548 } 2549 buf := bytes.NewBuffer(nil) 2550 if _, err := buf.ReadFrom(rc); err != nil { 2551 t.Errorf("fail to read: %v", err) 2552 } 2553 if err := rc.Close(); err != nil { 2554 t.Errorf("fail to close: %v", err) 2555 } 2556 if got := buf.Bytes(); !bytes.Equal(got, blob) { 2557 t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob) 2558 } 2559 2560 // test seekable content 2561 seekable = true 2562 gotDesc, rc, err = store.FetchReference(ctx, blobDesc.Digest.String()) 2563 if err != nil { 2564 t.Fatalf("Blobs.FetchReference() error = %v", err) 2565 } 2566 if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size { 2567 t.Errorf("Blobs.FetchReference() = %v, want %v", gotDesc, blobDesc) 2568 } 2569 s, ok := rc.(io.Seeker) 2570 if !ok { 2571 t.Fatalf("Blobs.FetchReference() = %v, want io.Seeker", rc) 2572 } 2573 buf.Reset() 2574 if _, err := buf.ReadFrom(rc); err != nil { 2575 t.Errorf("fail to read: %v", err) 2576 } 2577 if got := buf.Bytes(); !bytes.Equal(got, blob) { 2578 t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob) 2579 } 2580 2581 _, err = s.Seek(3, io.SeekStart) 2582 if err != nil { 2583 t.Errorf("fail to seek: %v", err) 2584 } 2585 buf.Reset() 2586 if _, err := buf.ReadFrom(rc); err != nil { 2587 t.Errorf("fail to read: %v", err) 2588 } 2589 if got := buf.Bytes(); !bytes.Equal(got, blob[3:]) { 2590 t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob[3:]) 2591 } 2592 2593 if err := rc.Close(); err != nil { 2594 t.Errorf("fail to close: %v", err) 2595 } 2596 } 2597 2598 func Test_generateBlobDescriptorWithVariousDockerContentDigestHeaders(t *testing.T) { 2599 reference := registry.Reference{ 2600 Registry: "eastern.haan.com", 2601 Reference: "<calculate>", 2602 Repository: "from25to220ce", 2603 } 2604 tests := getTestIOStructMapForGetDescriptorClass() 2605 for testName, dcdIOStruct := range tests { 2606 if dcdIOStruct.isTag { 2607 continue 2608 } 2609 2610 for i, method := range []string{http.MethodGet, http.MethodHead} { 2611 reference.Reference = dcdIOStruct.clientSuppliedReference 2612 2613 resp := http.Response{ 2614 Header: http.Header{ 2615 "Content-Type": []string{"application/vnd.docker.distribution.manifest.v2+json"}, 2616 dockerContentDigestHeader: []string{dcdIOStruct.serverCalculatedDigest.String()}, 2617 }, 2618 } 2619 if method == http.MethodGet { 2620 resp.Body = io.NopCloser(bytes.NewBufferString(theAmazingBanClan)) 2621 } 2622 resp.Request = &http.Request{ 2623 Method: method, 2624 } 2625 2626 var err error 2627 var d digest.Digest 2628 if d, err = reference.Digest(); err != nil { 2629 t.Errorf( 2630 "[Blob.%v] %v; got digest from a tag reference unexpectedly", 2631 method, testName, 2632 ) 2633 } 2634 2635 errExpected := []bool{dcdIOStruct.errExpectedOnGET, dcdIOStruct.errExpectedOnHEAD}[i] 2636 if len(d) == 0 { 2637 // To avoid an otherwise impossible scenario in the tested code 2638 // path, we set d so that verifyContentDigest does not break. 2639 d = dcdIOStruct.serverCalculatedDigest 2640 } 2641 _, err = generateBlobDescriptor(&resp, d) 2642 if !errExpected && err != nil { 2643 t.Errorf( 2644 "[Blob.%v] %v; expected no error for request, but got err: %v", 2645 method, testName, err, 2646 ) 2647 } else if errExpected && err == nil { 2648 t.Errorf( 2649 "[Blob.%v] %v; expected an error for request, but got none", 2650 method, testName, 2651 ) 2652 } 2653 } 2654 } 2655 } 2656 2657 func TestManifestStoreInterface(t *testing.T) { 2658 var ms interface{} = &manifestStore{} 2659 if _, ok := ms.(interfaces.ReferenceParser); !ok { 2660 t.Error("&manifestStore{} does not conform interfaces.ReferenceParser") 2661 } 2662 } 2663 2664 func Test_ManifestStore_Fetch(t *testing.T) { 2665 manifest := []byte(`{"layers":[]}`) 2666 manifestDesc := ocispec.Descriptor{ 2667 MediaType: ocispec.MediaTypeImageManifest, 2668 Digest: digest.FromBytes(manifest), 2669 Size: int64(len(manifest)), 2670 } 2671 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2672 if r.Method != http.MethodGet { 2673 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2674 w.WriteHeader(http.StatusMethodNotAllowed) 2675 return 2676 } 2677 switch r.URL.Path { 2678 case "/v2/test/manifests/" + manifestDesc.Digest.String(): 2679 if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { 2680 t.Errorf("manifest not convertable: %s", accept) 2681 w.WriteHeader(http.StatusBadRequest) 2682 return 2683 } 2684 w.Header().Set("Content-Type", manifestDesc.MediaType) 2685 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 2686 if _, err := w.Write(manifest); err != nil { 2687 t.Errorf("failed to write %q: %v", r.URL, err) 2688 } 2689 default: 2690 w.WriteHeader(http.StatusNotFound) 2691 } 2692 })) 2693 defer ts.Close() 2694 uri, err := url.Parse(ts.URL) 2695 if err != nil { 2696 t.Fatalf("invalid test http server: %v", err) 2697 } 2698 2699 repo, err := NewRepository(uri.Host + "/test") 2700 if err != nil { 2701 t.Fatalf("NewRepository() error = %v", err) 2702 } 2703 repo.PlainHTTP = true 2704 store := repo.Manifests() 2705 ctx := context.Background() 2706 2707 rc, err := store.Fetch(ctx, manifestDesc) 2708 if err != nil { 2709 t.Fatalf("Manifests.Fetch() error = %v", err) 2710 } 2711 buf := bytes.NewBuffer(nil) 2712 if _, err := buf.ReadFrom(rc); err != nil { 2713 t.Errorf("fail to read: %v", err) 2714 } 2715 if err := rc.Close(); err != nil { 2716 t.Errorf("fail to close: %v", err) 2717 } 2718 if got := buf.Bytes(); !bytes.Equal(got, manifest) { 2719 t.Errorf("Manifests.Fetch() = %v, want %v", got, manifest) 2720 } 2721 2722 content := []byte(`{"manifests":[]}`) 2723 contentDesc := ocispec.Descriptor{ 2724 MediaType: ocispec.MediaTypeImageIndex, 2725 Digest: digest.FromBytes(content), 2726 Size: int64(len(content)), 2727 } 2728 _, err = store.Fetch(ctx, contentDesc) 2729 if !errors.Is(err, errdef.ErrNotFound) { 2730 t.Errorf("Manifests.Fetch() error = %v, wantErr %v", err, errdef.ErrNotFound) 2731 } 2732 } 2733 2734 func Test_ManifestStore_Push(t *testing.T) { 2735 manifest := []byte(`{"layers":[]}`) 2736 manifestDesc := ocispec.Descriptor{ 2737 MediaType: ocispec.MediaTypeImageManifest, 2738 Digest: digest.FromBytes(manifest), 2739 Size: int64(len(manifest)), 2740 } 2741 var gotManifest []byte 2742 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2743 switch { 2744 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 2745 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 2746 w.WriteHeader(http.StatusBadRequest) 2747 break 2748 } 2749 buf := bytes.NewBuffer(nil) 2750 if _, err := buf.ReadFrom(r.Body); err != nil { 2751 t.Errorf("fail to read: %v", err) 2752 } 2753 gotManifest = buf.Bytes() 2754 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 2755 w.WriteHeader(http.StatusCreated) 2756 return 2757 default: 2758 w.WriteHeader(http.StatusForbidden) 2759 } 2760 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2761 })) 2762 defer ts.Close() 2763 uri, err := url.Parse(ts.URL) 2764 if err != nil { 2765 t.Fatalf("invalid test http server: %v", err) 2766 } 2767 2768 repo, err := NewRepository(uri.Host + "/test") 2769 if err != nil { 2770 t.Fatalf("NewRepository() error = %v", err) 2771 } 2772 repo.PlainHTTP = true 2773 store := repo.Manifests() 2774 ctx := context.Background() 2775 2776 err = store.Push(ctx, manifestDesc, bytes.NewReader(manifest)) 2777 if err != nil { 2778 t.Fatalf("Manifests.Push() error = %v", err) 2779 } 2780 if !bytes.Equal(gotManifest, manifest) { 2781 t.Errorf("Manifests.Push() = %v, want %v", gotManifest, manifest) 2782 } 2783 } 2784 2785 func Test_ManifestStore_Push_ReferrersAPIAvailable(t *testing.T) { 2786 // generate test content 2787 subject := []byte(`{"layers":[]}`) 2788 subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) 2789 artifact := ocispec.Artifact{ 2790 MediaType: ocispec.MediaTypeArtifactManifest, 2791 Subject: &subjectDesc, 2792 } 2793 artifactJSON, err := json.Marshal(artifact) 2794 if err != nil { 2795 t.Errorf("failed to marshal manifest: %v", err) 2796 } 2797 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 2798 manifest := ocispec.Manifest{ 2799 MediaType: ocispec.MediaTypeImageManifest, 2800 Subject: &subjectDesc, 2801 } 2802 manifestJSON, err := json.Marshal(manifest) 2803 if err != nil { 2804 t.Errorf("failed to marshal manifest: %v", err) 2805 } 2806 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 2807 2808 var gotManifest []byte 2809 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2810 switch { 2811 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 2812 if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType { 2813 w.WriteHeader(http.StatusBadRequest) 2814 break 2815 } 2816 buf := bytes.NewBuffer(nil) 2817 if _, err := buf.ReadFrom(r.Body); err != nil { 2818 t.Errorf("fail to read: %v", err) 2819 } 2820 gotManifest = buf.Bytes() 2821 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 2822 w.WriteHeader(http.StatusCreated) 2823 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 2824 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 2825 w.WriteHeader(http.StatusBadRequest) 2826 break 2827 } 2828 buf := bytes.NewBuffer(nil) 2829 if _, err := buf.ReadFrom(r.Body); err != nil { 2830 t.Errorf("fail to read: %v", err) 2831 } 2832 gotManifest = buf.Bytes() 2833 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 2834 w.WriteHeader(http.StatusCreated) 2835 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 2836 result := ocispec.Index{ 2837 Versioned: specs.Versioned{ 2838 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 2839 }, 2840 MediaType: ocispec.MediaTypeImageIndex, 2841 Manifests: []ocispec.Descriptor{}, 2842 } 2843 if err := json.NewEncoder(w).Encode(result); err != nil { 2844 t.Errorf("failed to write response: %v", err) 2845 } 2846 default: 2847 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2848 w.WriteHeader(http.StatusNotFound) 2849 } 2850 })) 2851 defer ts.Close() 2852 uri, err := url.Parse(ts.URL) 2853 if err != nil { 2854 t.Fatalf("invalid test http server: %v", err) 2855 } 2856 2857 ctx := context.Background() 2858 repo, err := NewRepository(uri.Host + "/test") 2859 if err != nil { 2860 t.Fatalf("NewRepository() error = %v", err) 2861 } 2862 repo.PlainHTTP = true 2863 2864 // test push artifact with subject 2865 if state := repo.loadReferrersState(); state != referrersStateUnknown { 2866 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 2867 } 2868 err = repo.Push(ctx, artifactDesc, bytes.NewReader(artifactJSON)) 2869 if err != nil { 2870 t.Fatalf("Manifests.Push() error = %v", err) 2871 } 2872 if !bytes.Equal(gotManifest, artifactJSON) { 2873 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON)) 2874 } 2875 2876 // test push image manifest with subject 2877 if state := repo.loadReferrersState(); state != referrersStateSupported { 2878 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 2879 } 2880 err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) 2881 if err != nil { 2882 t.Fatalf("Manifests.Push() error = %v", err) 2883 } 2884 if !bytes.Equal(gotManifest, manifestJSON) { 2885 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) 2886 } 2887 } 2888 2889 func Test_ManifestStore_Push_ReferrersAPIUnavailable(t *testing.T) { 2890 // generate test content 2891 subject := []byte(`{"layers":[]}`) 2892 subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) 2893 referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) 2894 artifact := ocispec.Artifact{ 2895 MediaType: ocispec.MediaTypeArtifactManifest, 2896 Subject: &subjectDesc, 2897 ArtifactType: "application/vnd.test", 2898 Annotations: map[string]string{"foo": "bar"}, 2899 } 2900 artifactJSON, err := json.Marshal(artifact) 2901 if err != nil { 2902 t.Errorf("failed to marshal manifest: %v", err) 2903 } 2904 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 2905 artifactDesc.ArtifactType = artifact.ArtifactType 2906 artifactDesc.Annotations = artifact.Annotations 2907 2908 // test push artifact with subject 2909 index_1 := ocispec.Index{ 2910 Versioned: specs.Versioned{ 2911 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 2912 }, 2913 MediaType: ocispec.MediaTypeImageIndex, 2914 Manifests: []ocispec.Descriptor{ 2915 artifactDesc, 2916 }, 2917 } 2918 indexJSON_1, err := json.Marshal(index_1) 2919 if err != nil { 2920 t.Errorf("failed to marshal manifest: %v", err) 2921 } 2922 indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1) 2923 var gotManifest []byte 2924 var gotReferrerIndex []byte 2925 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2926 switch { 2927 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 2928 if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType { 2929 w.WriteHeader(http.StatusBadRequest) 2930 break 2931 } 2932 buf := bytes.NewBuffer(nil) 2933 if _, err := buf.ReadFrom(r.Body); err != nil { 2934 t.Errorf("fail to read: %v", err) 2935 } 2936 gotManifest = buf.Bytes() 2937 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 2938 w.WriteHeader(http.StatusCreated) 2939 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 2940 w.WriteHeader(http.StatusNotFound) 2941 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 2942 w.WriteHeader(http.StatusNotFound) 2943 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 2944 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 2945 w.WriteHeader(http.StatusBadRequest) 2946 break 2947 } 2948 buf := bytes.NewBuffer(nil) 2949 if _, err := buf.ReadFrom(r.Body); err != nil { 2950 t.Errorf("fail to read: %v", err) 2951 } 2952 gotReferrerIndex = buf.Bytes() 2953 w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String()) 2954 w.WriteHeader(http.StatusCreated) 2955 default: 2956 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2957 w.WriteHeader(http.StatusNotFound) 2958 } 2959 })) 2960 defer ts.Close() 2961 uri, err := url.Parse(ts.URL) 2962 if err != nil { 2963 t.Fatalf("invalid test http server: %v", err) 2964 } 2965 2966 ctx := context.Background() 2967 repo, err := NewRepository(uri.Host + "/test") 2968 if err != nil { 2969 t.Fatalf("NewRepository() error = %v", err) 2970 } 2971 repo.PlainHTTP = true 2972 2973 if state := repo.loadReferrersState(); state != referrersStateUnknown { 2974 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 2975 } 2976 err = repo.Push(ctx, artifactDesc, bytes.NewReader(artifactJSON)) 2977 if err != nil { 2978 t.Fatalf("Manifests.Push() error = %v", err) 2979 } 2980 if !bytes.Equal(gotManifest, artifactJSON) { 2981 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON)) 2982 } 2983 if !bytes.Equal(gotReferrerIndex, indexJSON_1) { 2984 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1)) 2985 } 2986 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 2987 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 2988 } 2989 2990 // test push image manifest with subject, referrer list should be updated 2991 manifest := ocispec.Manifest{ 2992 MediaType: ocispec.MediaTypeImageManifest, 2993 Config: ocispec.Descriptor{ 2994 MediaType: "testconfig", 2995 }, 2996 Subject: &subjectDesc, 2997 Annotations: map[string]string{"foo": "bar"}, 2998 } 2999 manifestJSON, err := json.Marshal(manifest) 3000 if err != nil { 3001 t.Errorf("failed to marshal manifest: %v", err) 3002 } 3003 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 3004 manifestDesc.ArtifactType = manifest.Config.MediaType 3005 manifestDesc.Annotations = manifest.Annotations 3006 index_2 := ocispec.Index{ 3007 Versioned: specs.Versioned{ 3008 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 3009 }, 3010 MediaType: ocispec.MediaTypeImageIndex, 3011 Manifests: []ocispec.Descriptor{ 3012 artifactDesc, 3013 manifestDesc, 3014 }, 3015 } 3016 indexJSON_2, err := json.Marshal(index_2) 3017 if err != nil { 3018 t.Errorf("failed to marshal manifest: %v", err) 3019 } 3020 indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2) 3021 var manifestDeleted bool 3022 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3023 switch { 3024 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 3025 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 3026 w.WriteHeader(http.StatusBadRequest) 3027 break 3028 } 3029 buf := bytes.NewBuffer(nil) 3030 if _, err := buf.ReadFrom(r.Body); err != nil { 3031 t.Errorf("fail to read: %v", err) 3032 } 3033 gotManifest = buf.Bytes() 3034 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 3035 w.WriteHeader(http.StatusCreated) 3036 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 3037 w.WriteHeader(http.StatusNotFound) 3038 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3039 w.Write(indexJSON_1) 3040 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3041 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 3042 w.WriteHeader(http.StatusBadRequest) 3043 break 3044 } 3045 buf := bytes.NewBuffer(nil) 3046 if _, err := buf.ReadFrom(r.Body); err != nil { 3047 t.Errorf("fail to read: %v", err) 3048 } 3049 gotReferrerIndex = buf.Bytes() 3050 w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String()) 3051 w.WriteHeader(http.StatusCreated) 3052 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_1.Digest.String(): 3053 manifestDeleted = true 3054 // no "Docker-Content-Digest" header for manifest deletion 3055 w.WriteHeader(http.StatusAccepted) 3056 default: 3057 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3058 w.WriteHeader(http.StatusNotFound) 3059 } 3060 })) 3061 defer ts.Close() 3062 uri, err = url.Parse(ts.URL) 3063 if err != nil { 3064 t.Fatalf("invalid test http server: %v", err) 3065 } 3066 3067 ctx = context.Background() 3068 repo, err = NewRepository(uri.Host + "/test") 3069 if err != nil { 3070 t.Fatalf("NewRepository() error = %v", err) 3071 } 3072 repo.PlainHTTP = true 3073 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3074 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3075 } 3076 err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) 3077 if err != nil { 3078 t.Fatalf("Manifests.Push() error = %v", err) 3079 } 3080 if !bytes.Equal(gotManifest, manifestJSON) { 3081 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) 3082 } 3083 if !bytes.Equal(gotReferrerIndex, indexJSON_2) { 3084 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) 3085 } 3086 if !manifestDeleted { 3087 t.Errorf("manifestDeleted = %v, want %v", manifestDeleted, true) 3088 } 3089 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 3090 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 3091 } 3092 3093 // test push image manifest with subject again, referrers list should not be changed 3094 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3095 switch { 3096 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 3097 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 3098 w.WriteHeader(http.StatusBadRequest) 3099 break 3100 } 3101 buf := bytes.NewBuffer(nil) 3102 if _, err := buf.ReadFrom(r.Body); err != nil { 3103 t.Errorf("fail to read: %v", err) 3104 } 3105 gotManifest = buf.Bytes() 3106 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 3107 w.WriteHeader(http.StatusCreated) 3108 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 3109 w.WriteHeader(http.StatusNotFound) 3110 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3111 w.Write(indexJSON_2) 3112 default: 3113 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3114 w.WriteHeader(http.StatusNotFound) 3115 } 3116 })) 3117 defer ts.Close() 3118 uri, err = url.Parse(ts.URL) 3119 if err != nil { 3120 t.Fatalf("invalid test http server: %v", err) 3121 } 3122 3123 ctx = context.Background() 3124 repo, err = NewRepository(uri.Host + "/test") 3125 if err != nil { 3126 t.Fatalf("NewRepository() error = %v", err) 3127 } 3128 repo.PlainHTTP = true 3129 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3130 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3131 } 3132 err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) 3133 if err != nil { 3134 t.Fatalf("Manifests.Push() error = %v", err) 3135 } 3136 if !bytes.Equal(gotManifest, manifestJSON) { 3137 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) 3138 } 3139 // referrers list should not be changed 3140 if !bytes.Equal(gotReferrerIndex, indexJSON_2) { 3141 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) 3142 } 3143 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 3144 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 3145 } 3146 } 3147 3148 func Test_ManifestStore_Exists(t *testing.T) { 3149 manifest := []byte(`{"layers":[]}`) 3150 manifestDesc := ocispec.Descriptor{ 3151 MediaType: ocispec.MediaTypeImageManifest, 3152 Digest: digest.FromBytes(manifest), 3153 Size: int64(len(manifest)), 3154 } 3155 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3156 if r.Method != http.MethodHead { 3157 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3158 w.WriteHeader(http.StatusMethodNotAllowed) 3159 return 3160 } 3161 switch r.URL.Path { 3162 case "/v2/test/manifests/" + manifestDesc.Digest.String(): 3163 if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { 3164 t.Errorf("manifest not convertable: %s", accept) 3165 w.WriteHeader(http.StatusBadRequest) 3166 return 3167 } 3168 w.Header().Set("Content-Type", manifestDesc.MediaType) 3169 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 3170 w.Header().Set("Content-Length", strconv.Itoa(int(manifestDesc.Size))) 3171 default: 3172 w.WriteHeader(http.StatusNotFound) 3173 } 3174 })) 3175 defer ts.Close() 3176 uri, err := url.Parse(ts.URL) 3177 if err != nil { 3178 t.Fatalf("invalid test http server: %v", err) 3179 } 3180 3181 repo, err := NewRepository(uri.Host + "/test") 3182 if err != nil { 3183 t.Fatalf("NewRepository() error = %v", err) 3184 } 3185 repo.PlainHTTP = true 3186 store := repo.Manifests() 3187 ctx := context.Background() 3188 3189 exists, err := store.Exists(ctx, manifestDesc) 3190 if err != nil { 3191 t.Fatalf("Manifests.Exists() error = %v", err) 3192 } 3193 if !exists { 3194 t.Errorf("Manifests.Exists() = %v, want %v", exists, true) 3195 } 3196 3197 content := []byte(`{"manifests":[]}`) 3198 contentDesc := ocispec.Descriptor{ 3199 MediaType: ocispec.MediaTypeImageIndex, 3200 Digest: digest.FromBytes(content), 3201 Size: int64(len(content)), 3202 } 3203 exists, err = store.Exists(ctx, contentDesc) 3204 if err != nil { 3205 t.Fatalf("Manifests.Exists() error = %v", err) 3206 } 3207 if exists { 3208 t.Errorf("Manifests.Exists() = %v, want %v", exists, false) 3209 } 3210 } 3211 3212 func Test_ManifestStore_Delete(t *testing.T) { 3213 manifest := []byte(`{"layers":[]}`) 3214 manifestDesc := ocispec.Descriptor{ 3215 MediaType: ocispec.MediaTypeImageManifest, 3216 Digest: digest.FromBytes(manifest), 3217 Size: int64(len(manifest)), 3218 } 3219 manifestDeleted := false 3220 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3221 if r.Method != http.MethodDelete && r.Method != http.MethodGet { 3222 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3223 w.WriteHeader(http.StatusMethodNotAllowed) 3224 } 3225 switch { 3226 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 3227 manifestDeleted = true 3228 // no "Docker-Content-Digest" header for manifest deletion 3229 w.WriteHeader(http.StatusAccepted) 3230 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 3231 if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { 3232 t.Errorf("manifest not convertable: %s", accept) 3233 w.WriteHeader(http.StatusBadRequest) 3234 return 3235 } 3236 w.Header().Set("Content-Type", manifestDesc.MediaType) 3237 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 3238 if _, err := w.Write(manifest); err != nil { 3239 t.Errorf("failed to write %q: %v", r.URL, err) 3240 } 3241 default: 3242 w.WriteHeader(http.StatusNotFound) 3243 } 3244 })) 3245 defer ts.Close() 3246 uri, err := url.Parse(ts.URL) 3247 if err != nil { 3248 t.Fatalf("invalid test http server: %v", err) 3249 } 3250 3251 repo, err := NewRepository(uri.Host + "/test") 3252 if err != nil { 3253 t.Fatalf("NewRepository() error = %v", err) 3254 } 3255 repo.PlainHTTP = true 3256 store := repo.Manifests() 3257 ctx := context.Background() 3258 3259 // test delete manifest without subject 3260 err = store.Delete(ctx, manifestDesc) 3261 if err != nil { 3262 t.Fatalf("Manifests.Delete() error = %v", err) 3263 } 3264 if !manifestDeleted { 3265 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 3266 } 3267 3268 // test delete content that does not exist 3269 content := []byte(`{"manifests":[]}`) 3270 contentDesc := ocispec.Descriptor{ 3271 MediaType: ocispec.MediaTypeImageIndex, 3272 Digest: digest.FromBytes(content), 3273 Size: int64(len(content)), 3274 } 3275 err = store.Delete(ctx, contentDesc) 3276 if !errors.Is(err, errdef.ErrNotFound) { 3277 t.Errorf("Manifests.Delete() error = %v, wantErr %v", err, errdef.ErrNotFound) 3278 } 3279 } 3280 3281 func Test_ManifestStore_Delete_ReferrersAPIAvailable(t *testing.T) { 3282 // generate test content 3283 subject := []byte(`{"layers":[]}`) 3284 subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) 3285 artifact := ocispec.Artifact{ 3286 MediaType: ocispec.MediaTypeArtifactManifest, 3287 Subject: &subjectDesc, 3288 } 3289 artifactJSON, err := json.Marshal(artifact) 3290 if err != nil { 3291 t.Errorf("failed to marshal manifest: %v", err) 3292 } 3293 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 3294 manifest := ocispec.Manifest{ 3295 MediaType: ocispec.MediaTypeImageManifest, 3296 Subject: &subjectDesc, 3297 } 3298 manifestJSON, err := json.Marshal(manifest) 3299 if err != nil { 3300 t.Errorf("failed to marshal manifest: %v", err) 3301 } 3302 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 3303 manifestDeleted := false 3304 artifactDeleted := false 3305 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3306 if r.Method != http.MethodDelete && r.Method != http.MethodGet { 3307 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3308 w.WriteHeader(http.StatusMethodNotAllowed) 3309 } 3310 switch { 3311 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 3312 artifactDeleted = true 3313 // no "Docker-Content-Digest" header for manifest deletion 3314 w.WriteHeader(http.StatusAccepted) 3315 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 3316 manifestDeleted = true 3317 // no "Docker-Content-Digest" header for manifest deletion 3318 w.WriteHeader(http.StatusAccepted) 3319 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 3320 if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { 3321 t.Errorf("manifest not convertable: %s", accept) 3322 w.WriteHeader(http.StatusBadRequest) 3323 return 3324 } 3325 w.Header().Set("Content-Type", artifactDesc.MediaType) 3326 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 3327 if _, err := w.Write(artifactJSON); err != nil { 3328 t.Errorf("failed to write %q: %v", r.URL, err) 3329 } 3330 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 3331 result := ocispec.Index{ 3332 Versioned: specs.Versioned{ 3333 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 3334 }, 3335 MediaType: ocispec.MediaTypeImageIndex, 3336 Manifests: []ocispec.Descriptor{}, 3337 } 3338 if err := json.NewEncoder(w).Encode(result); err != nil { 3339 t.Errorf("failed to write response: %v", err) 3340 } 3341 default: 3342 w.WriteHeader(http.StatusNotFound) 3343 } 3344 })) 3345 defer ts.Close() 3346 uri, err := url.Parse(ts.URL) 3347 if err != nil { 3348 t.Fatalf("invalid test http server: %v", err) 3349 } 3350 repo, err := NewRepository(uri.Host + "/test") 3351 if err != nil { 3352 t.Fatalf("NewRepository() error = %v", err) 3353 } 3354 repo.PlainHTTP = true 3355 store := repo.Manifests() 3356 ctx := context.Background() 3357 // test delete artifact with subject 3358 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3359 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3360 } 3361 err = store.Delete(ctx, artifactDesc) 3362 if err != nil { 3363 t.Fatalf("Manifests.Delete() error = %v", err) 3364 } 3365 if !artifactDeleted { 3366 t.Errorf("Manifests.Delete() = %v, want %v", artifactDeleted, true) 3367 } 3368 3369 // test delete manifest with subject 3370 if state := repo.loadReferrersState(); state != referrersStateSupported { 3371 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 3372 } 3373 err = store.Delete(ctx, manifestDesc) 3374 if err != nil { 3375 t.Fatalf("Manifests.Delete() error = %v", err) 3376 } 3377 if !manifestDeleted { 3378 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 3379 } 3380 3381 // test delete content that does not exist 3382 content := []byte("whatever") 3383 contentDesc := ocispec.Descriptor{ 3384 MediaType: ocispec.MediaTypeImageManifest, 3385 Digest: digest.FromBytes(content), 3386 Size: int64(len(content)), 3387 } 3388 ctx = context.Background() 3389 err = store.Delete(ctx, contentDesc) 3390 if !errors.Is(err, errdef.ErrNotFound) { 3391 t.Errorf("Manifests.Delete() error = %v, wantErr %v", err, errdef.ErrNotFound) 3392 } 3393 } 3394 3395 func Test_ManifestStore_Delete_ReferrersAPIUnavailable(t *testing.T) { 3396 // generate test content 3397 subject := []byte(`{"layers":[]}`) 3398 subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) 3399 referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) 3400 artifact := ocispec.Artifact{ 3401 MediaType: ocispec.MediaTypeArtifactManifest, 3402 Subject: &subjectDesc, 3403 } 3404 artifactJSON, err := json.Marshal(artifact) 3405 if err != nil { 3406 t.Errorf("failed to marshal manifest: %v", err) 3407 } 3408 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 3409 manifest := ocispec.Manifest{ 3410 MediaType: ocispec.MediaTypeImageManifest, 3411 Subject: &subjectDesc, 3412 } 3413 manifestJSON, err := json.Marshal(manifest) 3414 if err != nil { 3415 t.Errorf("failed to marshal manifest: %v", err) 3416 } 3417 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 3418 3419 // test delete artifact with subject 3420 index_1 := ocispec.Index{ 3421 Versioned: specs.Versioned{ 3422 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 3423 }, 3424 MediaType: ocispec.MediaTypeImageIndex, 3425 Manifests: []ocispec.Descriptor{ 3426 artifactDesc, 3427 manifestDesc, 3428 }, 3429 } 3430 indexJSON_1, err := json.Marshal(index_1) 3431 if err != nil { 3432 t.Errorf("failed to marshal manifest: %v", err) 3433 } 3434 indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1) 3435 index_2 := ocispec.Index{ 3436 Versioned: specs.Versioned{ 3437 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 3438 }, 3439 MediaType: ocispec.MediaTypeImageIndex, 3440 Manifests: []ocispec.Descriptor{ 3441 manifestDesc, 3442 }, 3443 } 3444 indexJSON_2, err := json.Marshal(index_2) 3445 if err != nil { 3446 t.Errorf("failed to marshal manifest: %v", err) 3447 } 3448 indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2) 3449 3450 manifestDeleted := false 3451 indexDeleted := false 3452 var gotReferrerIndex []byte 3453 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3454 switch { 3455 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 3456 manifestDeleted = true 3457 // no "Docker-Content-Digest" header for manifest deletion 3458 w.WriteHeader(http.StatusAccepted) 3459 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 3460 if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { 3461 t.Errorf("manifest not convertable: %s", accept) 3462 w.WriteHeader(http.StatusBadRequest) 3463 return 3464 } 3465 w.Header().Set("Content-Type", artifactDesc.MediaType) 3466 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 3467 if _, err := w.Write(artifactJSON); err != nil { 3468 t.Errorf("failed to write %q: %v", r.URL, err) 3469 } 3470 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 3471 w.WriteHeader(http.StatusNotFound) 3472 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3473 w.Write(indexJSON_1) 3474 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3475 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 3476 w.WriteHeader(http.StatusBadRequest) 3477 break 3478 } 3479 buf := bytes.NewBuffer(nil) 3480 if _, err := buf.ReadFrom(r.Body); err != nil { 3481 t.Errorf("fail to read: %v", err) 3482 } 3483 gotReferrerIndex = buf.Bytes() 3484 w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String()) 3485 w.WriteHeader(http.StatusCreated) 3486 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_1.Digest.String(): 3487 indexDeleted = true 3488 // no "Docker-Content-Digest" header for manifest deletion 3489 w.WriteHeader(http.StatusAccepted) 3490 default: 3491 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3492 w.WriteHeader(http.StatusNotFound) 3493 } 3494 })) 3495 defer ts.Close() 3496 uri, err := url.Parse(ts.URL) 3497 if err != nil { 3498 t.Fatalf("invalid test http server: %v", err) 3499 } 3500 repo, err := NewRepository(uri.Host + "/test") 3501 if err != nil { 3502 t.Fatalf("NewRepository() error = %v", err) 3503 } 3504 repo.PlainHTTP = true 3505 store := repo.Manifests() 3506 ctx := context.Background() 3507 3508 // test delete artifact with subject 3509 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3510 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3511 } 3512 err = store.Delete(ctx, artifactDesc) 3513 if err != nil { 3514 t.Fatalf("Manifests.Delete() error = %v", err) 3515 } 3516 if !manifestDeleted { 3517 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 3518 } 3519 if !bytes.Equal(gotReferrerIndex, indexJSON_2) { 3520 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) 3521 } 3522 if !indexDeleted { 3523 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 3524 } 3525 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 3526 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 3527 } 3528 3529 // test delete manifest with subject 3530 manifestDeleted = false 3531 indexDeleted = false 3532 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3533 switch { 3534 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 3535 manifestDeleted = true 3536 // no "Docker-Content-Digest" header for manifest deletion 3537 w.WriteHeader(http.StatusAccepted) 3538 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 3539 if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { 3540 t.Errorf("manifest not convertable: %s", accept) 3541 w.WriteHeader(http.StatusBadRequest) 3542 return 3543 } 3544 w.Header().Set("Content-Type", manifestDesc.MediaType) 3545 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 3546 if _, err := w.Write(manifestJSON); err != nil { 3547 t.Errorf("failed to write %q: %v", r.URL, err) 3548 } 3549 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 3550 w.WriteHeader(http.StatusNotFound) 3551 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3552 w.Write(indexJSON_2) 3553 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_2.Digest.String(): 3554 indexDeleted = true 3555 // no "Docker-Content-Digest" header for manifest deletion 3556 w.WriteHeader(http.StatusAccepted) 3557 default: 3558 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3559 w.WriteHeader(http.StatusNotFound) 3560 } 3561 })) 3562 defer ts.Close() 3563 uri, err = url.Parse(ts.URL) 3564 if err != nil { 3565 t.Fatalf("invalid test http server: %v", err) 3566 } 3567 repo, err = NewRepository(uri.Host + "/test") 3568 if err != nil { 3569 t.Fatalf("NewRepository() error = %v", err) 3570 } 3571 repo.PlainHTTP = true 3572 store = repo.Manifests() 3573 ctx = context.Background() 3574 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3575 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3576 } 3577 err = store.Delete(ctx, manifestDesc) 3578 if err != nil { 3579 t.Fatalf("Manifests.Delete() error = %v", err) 3580 } 3581 if !manifestDeleted { 3582 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 3583 } 3584 if !indexDeleted { 3585 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 3586 } 3587 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 3588 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 3589 } 3590 } 3591 3592 func Test_ManifestStore_Delete_ReferrersAPIUnavailable_InconsistentIndex(t *testing.T) { 3593 // generate test content 3594 subject := []byte(`{"layers":[]}`) 3595 subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) 3596 referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) 3597 artifact := ocispec.Artifact{ 3598 MediaType: ocispec.MediaTypeArtifactManifest, 3599 Subject: &subjectDesc, 3600 } 3601 artifactJSON, err := json.Marshal(artifact) 3602 if err != nil { 3603 t.Errorf("failed to marshal manifest: %v", err) 3604 } 3605 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 3606 3607 // test inconsistent state: index not found 3608 manifestDeleted := true 3609 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3610 switch { 3611 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 3612 manifestDeleted = true 3613 // no "Docker-Content-Digest" header for manifest deletion 3614 w.WriteHeader(http.StatusAccepted) 3615 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 3616 if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { 3617 t.Errorf("manifest not convertable: %s", accept) 3618 w.WriteHeader(http.StatusBadRequest) 3619 return 3620 } 3621 w.Header().Set("Content-Type", artifactDesc.MediaType) 3622 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 3623 if _, err := w.Write(artifactJSON); err != nil { 3624 t.Errorf("failed to write %q: %v", r.URL, err) 3625 } 3626 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 3627 w.WriteHeader(http.StatusNotFound) 3628 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3629 w.WriteHeader(http.StatusNotFound) 3630 default: 3631 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3632 w.WriteHeader(http.StatusNotFound) 3633 } 3634 })) 3635 defer ts.Close() 3636 uri, err := url.Parse(ts.URL) 3637 if err != nil { 3638 t.Fatalf("invalid test http server: %v", err) 3639 } 3640 repo, err := NewRepository(uri.Host + "/test") 3641 if err != nil { 3642 t.Fatalf("NewRepository() error = %v", err) 3643 } 3644 repo.PlainHTTP = true 3645 store := repo.Manifests() 3646 ctx := context.Background() 3647 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3648 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3649 } 3650 err = store.Delete(ctx, artifactDesc) 3651 if err != nil { 3652 t.Fatalf("Manifests.Delete() error = %v", err) 3653 } 3654 if !manifestDeleted { 3655 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 3656 } 3657 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 3658 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 3659 } 3660 3661 // test inconsistent state: empty referrers list 3662 manifestDeleted = true 3663 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3664 switch { 3665 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 3666 manifestDeleted = true 3667 // no "Docker-Content-Digest" header for manifest deletion 3668 w.WriteHeader(http.StatusAccepted) 3669 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 3670 if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { 3671 t.Errorf("manifest not convertable: %s", accept) 3672 w.WriteHeader(http.StatusBadRequest) 3673 return 3674 } 3675 w.Header().Set("Content-Type", artifactDesc.MediaType) 3676 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 3677 if _, err := w.Write(artifactJSON); err != nil { 3678 t.Errorf("failed to write %q: %v", r.URL, err) 3679 } 3680 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 3681 w.WriteHeader(http.StatusNotFound) 3682 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3683 result := ocispec.Index{ 3684 Versioned: specs.Versioned{ 3685 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 3686 }, 3687 MediaType: ocispec.MediaTypeImageIndex, 3688 Manifests: []ocispec.Descriptor{}, 3689 } 3690 if err := json.NewEncoder(w).Encode(result); err != nil { 3691 t.Errorf("failed to write response: %v", err) 3692 } 3693 default: 3694 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3695 w.WriteHeader(http.StatusNotFound) 3696 } 3697 })) 3698 defer ts.Close() 3699 uri, err = url.Parse(ts.URL) 3700 if err != nil { 3701 t.Fatalf("invalid test http server: %v", err) 3702 } 3703 repo, err = NewRepository(uri.Host + "/test") 3704 if err != nil { 3705 t.Fatalf("NewRepository() error = %v", err) 3706 } 3707 repo.PlainHTTP = true 3708 store = repo.Manifests() 3709 ctx = context.Background() 3710 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3711 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3712 } 3713 err = store.Delete(ctx, artifactDesc) 3714 if err != nil { 3715 t.Fatalf("Manifests.Delete() error = %v", err) 3716 } 3717 if !manifestDeleted { 3718 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 3719 } 3720 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 3721 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 3722 } 3723 3724 // test inconsistent state: current referrer is not in referrers list 3725 manifestDeleted = true 3726 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3727 switch { 3728 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 3729 manifestDeleted = true 3730 // no "Docker-Content-Digest" header for manifest deletion 3731 w.WriteHeader(http.StatusAccepted) 3732 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 3733 if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { 3734 t.Errorf("manifest not convertable: %s", accept) 3735 w.WriteHeader(http.StatusBadRequest) 3736 return 3737 } 3738 w.Header().Set("Content-Type", artifactDesc.MediaType) 3739 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 3740 if _, err := w.Write(artifactJSON); err != nil { 3741 t.Errorf("failed to write %q: %v", r.URL, err) 3742 } 3743 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 3744 w.WriteHeader(http.StatusNotFound) 3745 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3746 result := ocispec.Index{ 3747 Versioned: specs.Versioned{ 3748 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 3749 }, 3750 MediaType: ocispec.MediaTypeImageIndex, 3751 Manifests: []ocispec.Descriptor{ 3752 content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, []byte("whaterver")), 3753 }, 3754 } 3755 if err := json.NewEncoder(w).Encode(result); err != nil { 3756 t.Errorf("failed to write response: %v", err) 3757 } 3758 default: 3759 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3760 w.WriteHeader(http.StatusNotFound) 3761 } 3762 })) 3763 defer ts.Close() 3764 uri, err = url.Parse(ts.URL) 3765 if err != nil { 3766 t.Fatalf("invalid test http server: %v", err) 3767 } 3768 repo, err = NewRepository(uri.Host + "/test") 3769 if err != nil { 3770 t.Fatalf("NewRepository() error = %v", err) 3771 } 3772 repo.PlainHTTP = true 3773 store = repo.Manifests() 3774 ctx = context.Background() 3775 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3776 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3777 } 3778 err = store.Delete(ctx, artifactDesc) 3779 if err != nil { 3780 t.Fatalf("Manifests.Delete() error = %v", err) 3781 } 3782 if !manifestDeleted { 3783 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 3784 } 3785 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 3786 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 3787 } 3788 } 3789 3790 func Test_ManifestStore_Resolve(t *testing.T) { 3791 manifest := []byte(`{"layers":[]}`) 3792 manifestDesc := ocispec.Descriptor{ 3793 MediaType: ocispec.MediaTypeImageIndex, 3794 Digest: digest.FromBytes(manifest), 3795 Size: int64(len(manifest)), 3796 } 3797 ref := "foobar" 3798 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3799 if r.Method != http.MethodHead { 3800 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3801 w.WriteHeader(http.StatusMethodNotAllowed) 3802 return 3803 } 3804 switch r.URL.Path { 3805 case "/v2/test/manifests/" + manifestDesc.Digest.String(), 3806 "/v2/test/manifests/" + ref: 3807 if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { 3808 t.Errorf("manifest not convertable: %s", accept) 3809 w.WriteHeader(http.StatusBadRequest) 3810 return 3811 } 3812 w.Header().Set("Content-Type", manifestDesc.MediaType) 3813 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 3814 w.Header().Set("Content-Length", strconv.Itoa(int(manifestDesc.Size))) 3815 default: 3816 w.WriteHeader(http.StatusNotFound) 3817 } 3818 })) 3819 defer ts.Close() 3820 uri, err := url.Parse(ts.URL) 3821 if err != nil { 3822 t.Fatalf("invalid test http server: %v", err) 3823 } 3824 3825 repoName := uri.Host + "/test" 3826 repo, err := NewRepository(repoName) 3827 if err != nil { 3828 t.Fatalf("NewRepository() error = %v", err) 3829 } 3830 repo.PlainHTTP = true 3831 store := repo.Manifests() 3832 ctx := context.Background() 3833 3834 got, err := store.Resolve(ctx, manifestDesc.Digest.String()) 3835 if err != nil { 3836 t.Fatalf("Manifests.Resolve() error = %v", err) 3837 } 3838 if !reflect.DeepEqual(got, manifestDesc) { 3839 t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) 3840 } 3841 3842 got, err = store.Resolve(ctx, ref) 3843 if err != nil { 3844 t.Fatalf("Manifests.Resolve() error = %v", err) 3845 } 3846 if !reflect.DeepEqual(got, manifestDesc) { 3847 t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) 3848 } 3849 3850 tagDigestRef := "whatever" + "@" + manifestDesc.Digest.String() 3851 got, err = repo.Resolve(ctx, tagDigestRef) 3852 if err != nil { 3853 t.Fatalf("Manifests.Resolve() error = %v", err) 3854 } 3855 if !reflect.DeepEqual(got, manifestDesc) { 3856 t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) 3857 } 3858 3859 fqdnRef := repoName + ":" + tagDigestRef 3860 got, err = repo.Resolve(ctx, fqdnRef) 3861 if err != nil { 3862 t.Fatalf("Manifests.Resolve() error = %v", err) 3863 } 3864 if !reflect.DeepEqual(got, manifestDesc) { 3865 t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) 3866 } 3867 3868 content := []byte(`{"manifests":[]}`) 3869 contentDesc := ocispec.Descriptor{ 3870 MediaType: ocispec.MediaTypeImageIndex, 3871 Digest: digest.FromBytes(content), 3872 Size: int64(len(content)), 3873 } 3874 _, err = store.Resolve(ctx, contentDesc.Digest.String()) 3875 if !errors.Is(err, errdef.ErrNotFound) { 3876 t.Errorf("Manifests.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound) 3877 } 3878 } 3879 3880 func Test_ManifestStore_FetchReference(t *testing.T) { 3881 manifest := []byte(`{"layers":[]}`) 3882 manifestDesc := ocispec.Descriptor{ 3883 MediaType: ocispec.MediaTypeImageIndex, 3884 Digest: digest.FromBytes(manifest), 3885 Size: int64(len(manifest)), 3886 } 3887 ref := "foobar" 3888 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3889 if r.Method != http.MethodGet { 3890 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3891 w.WriteHeader(http.StatusMethodNotAllowed) 3892 return 3893 } 3894 switch r.URL.Path { 3895 case "/v2/test/manifests/" + manifestDesc.Digest.String(), 3896 "/v2/test/manifests/" + ref: 3897 if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { 3898 t.Errorf("manifest not convertable: %s", accept) 3899 w.WriteHeader(http.StatusBadRequest) 3900 return 3901 } 3902 w.Header().Set("Content-Type", manifestDesc.MediaType) 3903 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 3904 if _, err := w.Write(manifest); err != nil { 3905 t.Errorf("failed to write %q: %v", r.URL, err) 3906 } 3907 default: 3908 w.WriteHeader(http.StatusNotFound) 3909 } 3910 })) 3911 defer ts.Close() 3912 uri, err := url.Parse(ts.URL) 3913 if err != nil { 3914 t.Fatalf("invalid test http server: %v", err) 3915 } 3916 3917 repoName := uri.Host + "/test" 3918 repo, err := NewRepository(repoName) 3919 if err != nil { 3920 t.Fatalf("NewRepository() error = %v", err) 3921 } 3922 repo.PlainHTTP = true 3923 store := repo.Manifests() 3924 ctx := context.Background() 3925 3926 // test with tag 3927 gotDesc, rc, err := store.FetchReference(ctx, ref) 3928 if err != nil { 3929 t.Fatalf("Manifests.FetchReference() error = %v", err) 3930 } 3931 if !reflect.DeepEqual(gotDesc, manifestDesc) { 3932 t.Errorf("Manifests.FetchReference() = %v, want %v", gotDesc, manifestDesc) 3933 } 3934 buf := bytes.NewBuffer(nil) 3935 if _, err := buf.ReadFrom(rc); err != nil { 3936 t.Errorf("fail to read: %v", err) 3937 } 3938 if err := rc.Close(); err != nil { 3939 t.Errorf("fail to close: %v", err) 3940 } 3941 if got := buf.Bytes(); !bytes.Equal(got, manifest) { 3942 t.Errorf("Manifests.FetchReference() = %v, want %v", got, manifest) 3943 } 3944 3945 // test with other tag 3946 randomRef := "whatever" 3947 _, _, err = store.FetchReference(ctx, randomRef) 3948 if !errors.Is(err, errdef.ErrNotFound) { 3949 t.Errorf("Manifests.FetchReference() error = %v, wantErr %v", err, errdef.ErrNotFound) 3950 } 3951 3952 // test with digest 3953 gotDesc, rc, err = store.FetchReference(ctx, manifestDesc.Digest.String()) 3954 if err != nil { 3955 t.Fatalf("Manifests.FetchReference() error = %v", err) 3956 } 3957 if !reflect.DeepEqual(gotDesc, manifestDesc) { 3958 t.Errorf("Manifests.FetchReference() = %v, want %v", gotDesc, manifestDesc) 3959 } 3960 buf.Reset() 3961 if _, err := buf.ReadFrom(rc); err != nil { 3962 t.Errorf("fail to read: %v", err) 3963 } 3964 if err := rc.Close(); err != nil { 3965 t.Errorf("fail to close: %v", err) 3966 } 3967 if got := buf.Bytes(); !bytes.Equal(got, manifest) { 3968 t.Errorf("Manifests.FetchReference() = %v, want %v", got, manifest) 3969 } 3970 3971 // test with other digest 3972 randomContent := []byte("whatever") 3973 randomContentDigest := digest.FromBytes(randomContent) 3974 _, _, err = store.FetchReference(ctx, randomContentDigest.String()) 3975 if !errors.Is(err, errdef.ErrNotFound) { 3976 t.Errorf("Manifests.FetchReference() error = %v, wantErr %v", err, errdef.ErrNotFound) 3977 } 3978 3979 // test with tag@digest 3980 tagDigestRef := randomRef + "@" + manifestDesc.Digest.String() 3981 gotDesc, rc, err = store.FetchReference(ctx, tagDigestRef) 3982 if err != nil { 3983 t.Fatalf("Manifests.FetchReference() error = %v", err) 3984 } 3985 if !reflect.DeepEqual(gotDesc, manifestDesc) { 3986 t.Errorf("Manifests.FetchReference() = %v, want %v", gotDesc, manifestDesc) 3987 } 3988 buf.Reset() 3989 if _, err := buf.ReadFrom(rc); err != nil { 3990 t.Errorf("fail to read: %v", err) 3991 } 3992 if err := rc.Close(); err != nil { 3993 t.Errorf("fail to close: %v", err) 3994 } 3995 if got := buf.Bytes(); !bytes.Equal(got, manifest) { 3996 t.Errorf("Manifests.FetchReference() = %v, want %v", got, manifest) 3997 } 3998 3999 // test with FQDN 4000 fqdnRef := repoName + ":" + tagDigestRef 4001 gotDesc, rc, err = store.FetchReference(ctx, fqdnRef) 4002 if err != nil { 4003 t.Fatalf("Manifests.FetchReference() error = %v", err) 4004 } 4005 if !reflect.DeepEqual(gotDesc, manifestDesc) { 4006 t.Errorf("Manifests.FetchReference() = %v, want %v", gotDesc, manifestDesc) 4007 } 4008 buf.Reset() 4009 if _, err := buf.ReadFrom(rc); err != nil { 4010 t.Errorf("fail to read: %v", err) 4011 } 4012 if err := rc.Close(); err != nil { 4013 t.Errorf("fail to close: %v", err) 4014 } 4015 if got := buf.Bytes(); !bytes.Equal(got, manifest) { 4016 t.Errorf("Manifests.FetchReference() = %v, want %v", got, manifest) 4017 } 4018 } 4019 4020 func Test_ManifestStore_Tag(t *testing.T) { 4021 blob := []byte("hello world") 4022 blobDesc := ocispec.Descriptor{ 4023 MediaType: "test", 4024 Digest: digest.FromBytes(blob), 4025 Size: int64(len(blob)), 4026 } 4027 index := []byte(`{"manifests":[]}`) 4028 indexDesc := ocispec.Descriptor{ 4029 MediaType: ocispec.MediaTypeImageIndex, 4030 Digest: digest.FromBytes(index), 4031 Size: int64(len(index)), 4032 } 4033 var gotIndex []byte 4034 ref := "foobar" 4035 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4036 switch { 4037 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+blobDesc.Digest.String(): 4038 w.WriteHeader(http.StatusNotFound) 4039 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 4040 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { 4041 t.Errorf("manifest not convertable: %s", accept) 4042 w.WriteHeader(http.StatusBadRequest) 4043 return 4044 } 4045 w.Header().Set("Content-Type", indexDesc.MediaType) 4046 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 4047 if _, err := w.Write(index); err != nil { 4048 t.Errorf("failed to write %q: %v", r.URL, err) 4049 } 4050 case r.Method == http.MethodPut && 4051 r.URL.Path == "/v2/test/manifests/"+ref || r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 4052 if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { 4053 w.WriteHeader(http.StatusBadRequest) 4054 break 4055 } 4056 buf := bytes.NewBuffer(nil) 4057 if _, err := buf.ReadFrom(r.Body); err != nil { 4058 t.Errorf("fail to read: %v", err) 4059 } 4060 gotIndex = buf.Bytes() 4061 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 4062 w.WriteHeader(http.StatusCreated) 4063 return 4064 default: 4065 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4066 w.WriteHeader(http.StatusForbidden) 4067 } 4068 })) 4069 defer ts.Close() 4070 uri, err := url.Parse(ts.URL) 4071 if err != nil { 4072 t.Fatalf("invalid test http server: %v", err) 4073 } 4074 4075 repo, err := NewRepository(uri.Host + "/test") 4076 if err != nil { 4077 t.Fatalf("NewRepository() error = %v", err) 4078 } 4079 store := repo.Manifests() 4080 repo.PlainHTTP = true 4081 ctx := context.Background() 4082 4083 err = store.Tag(ctx, blobDesc, ref) 4084 if err == nil { 4085 t.Errorf("Repository.Tag() error = %v, wantErr %v", err, true) 4086 } 4087 4088 err = store.Tag(ctx, indexDesc, ref) 4089 if err != nil { 4090 t.Fatalf("Repository.Tag() error = %v", err) 4091 } 4092 if !bytes.Equal(gotIndex, index) { 4093 t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index) 4094 } 4095 4096 gotIndex = nil 4097 err = store.Tag(ctx, indexDesc, indexDesc.Digest.String()) 4098 if err != nil { 4099 t.Fatalf("Repository.Tag() error = %v", err) 4100 } 4101 if !bytes.Equal(gotIndex, index) { 4102 t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index) 4103 } 4104 } 4105 4106 func Test_ManifestStore_PushReference(t *testing.T) { 4107 index := []byte(`{"manifests":[]}`) 4108 indexDesc := ocispec.Descriptor{ 4109 MediaType: ocispec.MediaTypeImageIndex, 4110 Digest: digest.FromBytes(index), 4111 Size: int64(len(index)), 4112 } 4113 var gotIndex []byte 4114 ref := "foobar" 4115 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4116 switch { 4117 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+ref: 4118 if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { 4119 w.WriteHeader(http.StatusBadRequest) 4120 break 4121 } 4122 buf := bytes.NewBuffer(nil) 4123 if _, err := buf.ReadFrom(r.Body); err != nil { 4124 t.Errorf("fail to read: %v", err) 4125 } 4126 gotIndex = buf.Bytes() 4127 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 4128 w.WriteHeader(http.StatusCreated) 4129 return 4130 default: 4131 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4132 w.WriteHeader(http.StatusForbidden) 4133 } 4134 })) 4135 defer ts.Close() 4136 uri, err := url.Parse(ts.URL) 4137 if err != nil { 4138 t.Fatalf("invalid test http server: %v", err) 4139 } 4140 4141 repo, err := NewRepository(uri.Host + "/test") 4142 if err != nil { 4143 t.Fatalf("NewRepository() error = %v", err) 4144 } 4145 store := repo.Manifests() 4146 repo.PlainHTTP = true 4147 ctx := context.Background() 4148 err = store.PushReference(ctx, indexDesc, bytes.NewReader(index), ref) 4149 if err != nil { 4150 t.Fatalf("Repository.PushReference() error = %v", err) 4151 } 4152 if !bytes.Equal(gotIndex, index) { 4153 t.Errorf("Repository.PushReference() = %v, want %v", gotIndex, index) 4154 } 4155 } 4156 4157 func Test_ManifestStore_PushReference_ReferrersAPIAvailable(t *testing.T) { 4158 // generate test content 4159 subject := []byte(`{"layers":[]}`) 4160 subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) 4161 artifact := ocispec.Artifact{ 4162 MediaType: ocispec.MediaTypeArtifactManifest, 4163 Subject: &subjectDesc, 4164 } 4165 artifactJSON, err := json.Marshal(artifact) 4166 if err != nil { 4167 t.Errorf("failed to marshal manifest: %v", err) 4168 } 4169 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 4170 artifactRef := "foo" 4171 4172 manifest := ocispec.Manifest{ 4173 MediaType: ocispec.MediaTypeImageManifest, 4174 Subject: &subjectDesc, 4175 } 4176 manifestJSON, err := json.Marshal(manifest) 4177 if err != nil { 4178 t.Errorf("failed to marshal manifest: %v", err) 4179 } 4180 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 4181 manifestRef := "bar" 4182 4183 var gotManifest []byte 4184 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4185 switch { 4186 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactRef: 4187 if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType { 4188 w.WriteHeader(http.StatusBadRequest) 4189 break 4190 } 4191 buf := bytes.NewBuffer(nil) 4192 if _, err := buf.ReadFrom(r.Body); err != nil { 4193 t.Errorf("fail to read: %v", err) 4194 } 4195 gotManifest = buf.Bytes() 4196 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 4197 w.WriteHeader(http.StatusCreated) 4198 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestRef: 4199 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 4200 w.WriteHeader(http.StatusBadRequest) 4201 break 4202 } 4203 buf := bytes.NewBuffer(nil) 4204 if _, err := buf.ReadFrom(r.Body); err != nil { 4205 t.Errorf("fail to read: %v", err) 4206 } 4207 gotManifest = buf.Bytes() 4208 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 4209 w.WriteHeader(http.StatusCreated) 4210 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 4211 result := ocispec.Index{ 4212 Versioned: specs.Versioned{ 4213 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 4214 }, 4215 MediaType: ocispec.MediaTypeImageIndex, 4216 Manifests: []ocispec.Descriptor{}, 4217 } 4218 if err := json.NewEncoder(w).Encode(result); err != nil { 4219 t.Errorf("failed to write response: %v", err) 4220 } 4221 default: 4222 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4223 w.WriteHeader(http.StatusNotFound) 4224 } 4225 })) 4226 defer ts.Close() 4227 uri, err := url.Parse(ts.URL) 4228 if err != nil { 4229 t.Fatalf("invalid test http server: %v", err) 4230 } 4231 4232 ctx := context.Background() 4233 repo, err := NewRepository(uri.Host + "/test") 4234 if err != nil { 4235 t.Fatalf("NewRepository() error = %v", err) 4236 } 4237 repo.PlainHTTP = true 4238 4239 // test push artifact with subject 4240 if state := repo.loadReferrersState(); state != referrersStateUnknown { 4241 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 4242 } 4243 err = repo.PushReference(ctx, artifactDesc, bytes.NewReader(artifactJSON), artifactRef) 4244 if err != nil { 4245 t.Fatalf("Manifests.Push() error = %v", err) 4246 } 4247 if !bytes.Equal(gotManifest, artifactJSON) { 4248 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON)) 4249 } 4250 4251 // test push image manifest with subject 4252 if state := repo.loadReferrersState(); state != referrersStateSupported { 4253 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 4254 } 4255 err = repo.PushReference(ctx, manifestDesc, bytes.NewReader(manifestJSON), manifestRef) 4256 if err != nil { 4257 t.Fatalf("Manifests.Push() error = %v", err) 4258 } 4259 if !bytes.Equal(gotManifest, manifestJSON) { 4260 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) 4261 } 4262 } 4263 4264 func Test_ManifestStore_PushReference_ReferrersAPIUnavailable(t *testing.T) { 4265 // generate test content 4266 subject := []byte(`{"layers":[]}`) 4267 subjectDesc := content.NewDescriptorFromBytes(ocispec.MediaTypeArtifactManifest, subject) 4268 referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) 4269 artifact := ocispec.Artifact{ 4270 MediaType: ocispec.MediaTypeArtifactManifest, 4271 Subject: &subjectDesc, 4272 ArtifactType: "application/vnd.test", 4273 Annotations: map[string]string{"foo": "bar"}, 4274 } 4275 artifactJSON, err := json.Marshal(artifact) 4276 if err != nil { 4277 t.Errorf("failed to marshal manifest: %v", err) 4278 } 4279 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 4280 artifactDesc.ArtifactType = artifact.ArtifactType 4281 artifactDesc.Annotations = artifact.Annotations 4282 artifactRef := "foo" 4283 4284 // test push artifact with subject 4285 index_1 := ocispec.Index{ 4286 Versioned: specs.Versioned{ 4287 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 4288 }, 4289 MediaType: ocispec.MediaTypeImageIndex, 4290 Manifests: []ocispec.Descriptor{ 4291 artifactDesc, 4292 }, 4293 } 4294 indexJSON_1, err := json.Marshal(index_1) 4295 if err != nil { 4296 t.Errorf("failed to marshal manifest: %v", err) 4297 } 4298 indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1) 4299 var gotManifest []byte 4300 var gotReferrerIndex []byte 4301 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4302 switch { 4303 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactRef: 4304 if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType { 4305 w.WriteHeader(http.StatusBadRequest) 4306 break 4307 } 4308 buf := bytes.NewBuffer(nil) 4309 if _, err := buf.ReadFrom(r.Body); err != nil { 4310 t.Errorf("fail to read: %v", err) 4311 } 4312 gotManifest = buf.Bytes() 4313 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 4314 w.WriteHeader(http.StatusCreated) 4315 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 4316 w.WriteHeader(http.StatusNotFound) 4317 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4318 w.WriteHeader(http.StatusNotFound) 4319 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4320 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 4321 w.WriteHeader(http.StatusBadRequest) 4322 break 4323 } 4324 buf := bytes.NewBuffer(nil) 4325 if _, err := buf.ReadFrom(r.Body); err != nil { 4326 t.Errorf("fail to read: %v", err) 4327 } 4328 gotReferrerIndex = buf.Bytes() 4329 w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String()) 4330 w.WriteHeader(http.StatusCreated) 4331 default: 4332 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4333 w.WriteHeader(http.StatusNotFound) 4334 } 4335 })) 4336 defer ts.Close() 4337 uri, err := url.Parse(ts.URL) 4338 if err != nil { 4339 t.Fatalf("invalid test http server: %v", err) 4340 } 4341 4342 ctx := context.Background() 4343 repo, err := NewRepository(uri.Host + "/test") 4344 if err != nil { 4345 t.Fatalf("NewRepository() error = %v", err) 4346 } 4347 repo.PlainHTTP = true 4348 4349 if state := repo.loadReferrersState(); state != referrersStateUnknown { 4350 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 4351 } 4352 err = repo.PushReference(ctx, artifactDesc, bytes.NewReader(artifactJSON), artifactRef) 4353 if err != nil { 4354 t.Fatalf("Manifests.Push() error = %v", err) 4355 } 4356 if !bytes.Equal(gotManifest, artifactJSON) { 4357 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON)) 4358 } 4359 if !bytes.Equal(gotReferrerIndex, indexJSON_1) { 4360 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1)) 4361 } 4362 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 4363 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 4364 } 4365 4366 // test push image manifest with subject, referrers list should be updated 4367 manifest := ocispec.Manifest{ 4368 MediaType: ocispec.MediaTypeImageManifest, 4369 Config: ocispec.Descriptor{ 4370 MediaType: "testconfig", 4371 }, 4372 Subject: &subjectDesc, 4373 Annotations: map[string]string{"foo": "bar"}, 4374 } 4375 manifestJSON, err := json.Marshal(manifest) 4376 if err != nil { 4377 t.Errorf("failed to marshal manifest: %v", err) 4378 } 4379 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 4380 manifestDesc.ArtifactType = manifest.Config.MediaType 4381 manifestDesc.Annotations = manifest.Annotations 4382 manifestRef := "bar" 4383 4384 index_2 := ocispec.Index{ 4385 Versioned: specs.Versioned{ 4386 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 4387 }, 4388 MediaType: ocispec.MediaTypeImageIndex, 4389 Manifests: []ocispec.Descriptor{ 4390 artifactDesc, 4391 manifestDesc, 4392 }, 4393 } 4394 indexJSON_2, err := json.Marshal(index_2) 4395 if err != nil { 4396 t.Errorf("failed to marshal manifest: %v", err) 4397 } 4398 indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2) 4399 var manifestDeleted bool 4400 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4401 switch { 4402 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestRef: 4403 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 4404 w.WriteHeader(http.StatusBadRequest) 4405 break 4406 } 4407 buf := bytes.NewBuffer(nil) 4408 if _, err := buf.ReadFrom(r.Body); err != nil { 4409 t.Errorf("fail to read: %v", err) 4410 } 4411 gotManifest = buf.Bytes() 4412 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 4413 w.WriteHeader(http.StatusCreated) 4414 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 4415 w.WriteHeader(http.StatusNotFound) 4416 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4417 w.Write(indexJSON_1) 4418 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4419 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 4420 w.WriteHeader(http.StatusBadRequest) 4421 break 4422 } 4423 buf := bytes.NewBuffer(nil) 4424 if _, err := buf.ReadFrom(r.Body); err != nil { 4425 t.Errorf("fail to read: %v", err) 4426 } 4427 gotReferrerIndex = buf.Bytes() 4428 w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String()) 4429 w.WriteHeader(http.StatusCreated) 4430 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_1.Digest.String(): 4431 manifestDeleted = true 4432 // no "Docker-Content-Digest" header for manifest deletion 4433 w.WriteHeader(http.StatusAccepted) 4434 default: 4435 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4436 w.WriteHeader(http.StatusNotFound) 4437 } 4438 })) 4439 defer ts.Close() 4440 uri, err = url.Parse(ts.URL) 4441 if err != nil { 4442 t.Fatalf("invalid test http server: %v", err) 4443 } 4444 4445 ctx = context.Background() 4446 repo, err = NewRepository(uri.Host + "/test") 4447 if err != nil { 4448 t.Fatalf("NewRepository() error = %v", err) 4449 } 4450 repo.PlainHTTP = true 4451 if state := repo.loadReferrersState(); state != referrersStateUnknown { 4452 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 4453 } 4454 err = repo.PushReference(ctx, manifestDesc, bytes.NewReader(manifestJSON), manifestRef) 4455 if err != nil { 4456 t.Fatalf("Manifests.PushReference() error = %v", err) 4457 } 4458 if !bytes.Equal(gotManifest, manifestJSON) { 4459 t.Errorf("Manifests.PushReference() = %v, want %v", string(gotManifest), string(manifestJSON)) 4460 } 4461 if !bytes.Equal(gotReferrerIndex, indexJSON_2) { 4462 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) 4463 } 4464 if !manifestDeleted { 4465 t.Errorf("manifestDeleted = %v, want %v", manifestDeleted, true) 4466 } 4467 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 4468 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 4469 } 4470 4471 // test push image manifest with subject again, referrers list should not be changed 4472 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4473 switch { 4474 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 4475 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 4476 w.WriteHeader(http.StatusBadRequest) 4477 break 4478 } 4479 buf := bytes.NewBuffer(nil) 4480 if _, err := buf.ReadFrom(r.Body); err != nil { 4481 t.Errorf("fail to read: %v", err) 4482 } 4483 gotManifest = buf.Bytes() 4484 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 4485 w.WriteHeader(http.StatusCreated) 4486 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 4487 w.WriteHeader(http.StatusNotFound) 4488 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4489 w.Write(indexJSON_2) 4490 default: 4491 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4492 w.WriteHeader(http.StatusNotFound) 4493 } 4494 })) 4495 defer ts.Close() 4496 uri, err = url.Parse(ts.URL) 4497 if err != nil { 4498 t.Fatalf("invalid test http server: %v", err) 4499 } 4500 4501 ctx = context.Background() 4502 repo, err = NewRepository(uri.Host + "/test") 4503 if err != nil { 4504 t.Fatalf("NewRepository() error = %v", err) 4505 } 4506 repo.PlainHTTP = true 4507 if state := repo.loadReferrersState(); state != referrersStateUnknown { 4508 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 4509 } 4510 err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) 4511 if err != nil { 4512 t.Fatalf("Manifests.Push() error = %v", err) 4513 } 4514 if !bytes.Equal(gotManifest, manifestJSON) { 4515 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) 4516 } 4517 // referrers list should not be changed 4518 if !bytes.Equal(gotReferrerIndex, indexJSON_2) { 4519 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) 4520 } 4521 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 4522 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 4523 } 4524 } 4525 4526 func Test_ManifestStore_generateDescriptorWithVariousDockerContentDigestHeaders(t *testing.T) { 4527 reference := registry.Reference{ 4528 Registry: "eastern.haan.com", 4529 Reference: "<calculate>", 4530 Repository: "from25to220ce", 4531 } 4532 4533 tests := getTestIOStructMapForGetDescriptorClass() 4534 for testName, dcdIOStruct := range tests { 4535 repo, err := NewRepository(fmt.Sprintf("%s/%s", reference.Repository, reference.Repository)) 4536 if err != nil { 4537 t.Fatalf("failed to initialize repository") 4538 } 4539 4540 s := manifestStore{repo: repo} 4541 4542 for i, method := range []string{http.MethodGet, http.MethodHead} { 4543 reference.Reference = dcdIOStruct.clientSuppliedReference 4544 4545 resp := http.Response{ 4546 Header: http.Header{ 4547 "Content-Type": []string{"application/vnd.docker.distribution.manifest.v2+json"}, 4548 dockerContentDigestHeader: []string{dcdIOStruct.serverCalculatedDigest.String()}, 4549 }, 4550 } 4551 if method == http.MethodGet { 4552 resp.Body = io.NopCloser(bytes.NewBufferString(theAmazingBanClan)) 4553 } 4554 resp.Request = &http.Request{ 4555 Method: method, 4556 } 4557 4558 errExpected := []bool{dcdIOStruct.errExpectedOnGET, dcdIOStruct.errExpectedOnHEAD}[i] 4559 _, err = s.generateDescriptor(&resp, reference, method) 4560 if !errExpected && err != nil { 4561 t.Errorf( 4562 "[Manifest.%v] %v; expected no error for request, but got err: %v", 4563 method, testName, err, 4564 ) 4565 } else if errExpected && err == nil { 4566 t.Errorf( 4567 "[Manifest.%v] %v; expected an error for request, but got none", 4568 method, testName, 4569 ) 4570 } 4571 } 4572 } 4573 } 4574 4575 type testTransport struct { 4576 proxyHost string 4577 underlyingTransport http.RoundTripper 4578 mockHost string 4579 } 4580 4581 func (t *testTransport) RoundTrip(originalReq *http.Request) (*http.Response, error) { 4582 req := originalReq.Clone(originalReq.Context()) 4583 mockHostName, mockPort, err := net.SplitHostPort(t.mockHost) 4584 // when t.mockHost is as form host:port 4585 if err == nil && (req.URL.Hostname() != mockHostName || req.URL.Port() != mockPort) { 4586 return nil, errors.New("bad request") 4587 } 4588 // when t.mockHost does not have specified port, in this case, 4589 // err is not nil 4590 if err != nil && req.URL.Hostname() != t.mockHost { 4591 return nil, errors.New("bad request") 4592 } 4593 req.Host = t.proxyHost 4594 req.URL.Host = t.proxyHost 4595 resp, err := t.underlyingTransport.RoundTrip(req) 4596 if err != nil { 4597 return nil, err 4598 } 4599 resp.Request.Host = t.mockHost 4600 resp.Request.URL.Host = t.mockHost 4601 return resp, nil 4602 } 4603 4604 // Helper function to create a registry.BlobStore for 4605 // Test_BlobStore_Push_Port443 4606 func blobStore_Push_Port443_create_store(uri *url.URL, testRegistry string) (registry.BlobStore, error) { 4607 repo, err := NewRepository(testRegistry + "/test") 4608 repo.Client = &auth.Client{ 4609 Client: &http.Client{ 4610 Transport: &testTransport{ 4611 proxyHost: uri.Host, 4612 underlyingTransport: http.DefaultTransport, 4613 mockHost: testRegistry, 4614 }, 4615 }, 4616 Cache: auth.NewCache(), 4617 } 4618 repo.PlainHTTP = true 4619 store := repo.Blobs() 4620 return store, err 4621 } 4622 4623 func Test_BlobStore_Push_Port443(t *testing.T) { 4624 blob := []byte("hello world") 4625 blobDesc := ocispec.Descriptor{ 4626 MediaType: "test", 4627 Digest: digest.FromBytes(blob), 4628 Size: int64(len(blob)), 4629 } 4630 uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c" 4631 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4632 switch { 4633 case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/": 4634 w.Header().Set("Location", "http://registry.wabbit-networks.io/v2/test/blobs/uploads/"+uuid) 4635 w.WriteHeader(http.StatusAccepted) 4636 return 4637 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid: 4638 if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" { 4639 w.WriteHeader(http.StatusBadRequest) 4640 break 4641 } 4642 if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() { 4643 w.WriteHeader(http.StatusBadRequest) 4644 break 4645 } 4646 buf := bytes.NewBuffer(nil) 4647 if _, err := buf.ReadFrom(r.Body); err != nil { 4648 t.Errorf("fail to read: %v", err) 4649 } 4650 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 4651 w.WriteHeader(http.StatusCreated) 4652 return 4653 default: 4654 w.WriteHeader(http.StatusForbidden) 4655 } 4656 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4657 })) 4658 defer ts.Close() 4659 uri, err := url.Parse(ts.URL) 4660 if err != nil { 4661 t.Fatalf("invalid test http server: %v", err) 4662 } 4663 4664 // Test case with Host: "registry.wabbit-networks.io:443", 4665 // Location: "registry.wabbit-networks.io" 4666 testRegistry := "registry.wabbit-networks.io:443" 4667 store, err := blobStore_Push_Port443_create_store(uri, testRegistry) 4668 if err != nil { 4669 t.Fatalf("blobStore_Push_Port443_create_store() error = %v", err) 4670 } 4671 ctx := context.Background() 4672 4673 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 4674 if err != nil { 4675 t.Fatalf("Blobs.Push() error = %v", err) 4676 } 4677 4678 // Test case with Host: "registry.wabbit-networks.io", 4679 // Location: "registry.wabbit-networks.io" 4680 testRegistry = "registry.wabbit-networks.io" 4681 store, err = blobStore_Push_Port443_create_store(uri, testRegistry) 4682 if err != nil { 4683 t.Fatalf("blobStore_Push_Port443_create_store() error = %v", err) 4684 } 4685 4686 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 4687 if err != nil { 4688 t.Fatalf("Blobs.Push() error = %v", err) 4689 } 4690 } 4691 4692 // Helper function to create a registry.BlobStore for 4693 // Test_BlobStore_Push_Port443_HTTPS 4694 func blobStore_Push_Port443_HTTPS_create_store(uri *url.URL, testRegistry string) (registry.BlobStore, error) { 4695 repo, err := NewRepository(testRegistry + "/test") 4696 tlsConfig := &tls.Config{ 4697 InsecureSkipVerify: true, 4698 } 4699 transport := &http.Transport{ 4700 TLSClientConfig: tlsConfig, 4701 } 4702 repo.Client = &auth.Client{ 4703 Client: &http.Client{ 4704 Transport: &testTransport{ 4705 proxyHost: uri.Host, 4706 underlyingTransport: transport, 4707 mockHost: testRegistry, 4708 }, 4709 }, 4710 Cache: auth.NewCache(), 4711 } 4712 repo.PlainHTTP = false 4713 store := repo.Blobs() 4714 return store, err 4715 } 4716 4717 func Test_BlobStore_Push_Port443_HTTPS(t *testing.T) { 4718 blob := []byte("hello world") 4719 blobDesc := ocispec.Descriptor{ 4720 MediaType: "test", 4721 Digest: digest.FromBytes(blob), 4722 Size: int64(len(blob)), 4723 } 4724 uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c" 4725 ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4726 switch { 4727 case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/": 4728 w.Header().Set("Location", "https://registry.wabbit-networks.io/v2/test/blobs/uploads/"+uuid) 4729 w.WriteHeader(http.StatusAccepted) 4730 return 4731 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid: 4732 if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" { 4733 w.WriteHeader(http.StatusBadRequest) 4734 break 4735 } 4736 if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() { 4737 w.WriteHeader(http.StatusBadRequest) 4738 break 4739 } 4740 buf := bytes.NewBuffer(nil) 4741 if _, err := buf.ReadFrom(r.Body); err != nil { 4742 t.Errorf("fail to read: %v", err) 4743 } 4744 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 4745 w.WriteHeader(http.StatusCreated) 4746 return 4747 default: 4748 w.WriteHeader(http.StatusForbidden) 4749 } 4750 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4751 })) 4752 defer ts.Close() 4753 uri, err := url.Parse(ts.URL) 4754 if err != nil { 4755 t.Fatalf("invalid test https server: %v", err) 4756 } 4757 4758 ctx := context.Background() 4759 // Test case with Host: "registry.wabbit-networks.io:443", 4760 // Location: "registry.wabbit-networks.io" 4761 testRegistry := "registry.wabbit-networks.io:443" 4762 store, err := blobStore_Push_Port443_HTTPS_create_store(uri, testRegistry) 4763 if err != nil { 4764 t.Fatalf("blobStore_Push_Port443_HTTPS_create_store() error = %v", err) 4765 } 4766 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 4767 if err != nil { 4768 t.Fatalf("Blobs.Push() error = %v", err) 4769 } 4770 4771 // Test case with Host: "registry.wabbit-networks.io", 4772 // Location: "registry.wabbit-networks.io" 4773 testRegistry = "registry.wabbit-networks.io" 4774 store, err = blobStore_Push_Port443_HTTPS_create_store(uri, testRegistry) 4775 if err != nil { 4776 t.Fatalf("blobStore_Push_Port443_HTTPS_create_store() error = %v", err) 4777 } 4778 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 4779 if err != nil { 4780 t.Fatalf("Blobs.Push() error = %v", err) 4781 } 4782 4783 ts = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4784 switch { 4785 case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/": 4786 w.Header().Set("Location", "https://registry.wabbit-networks.io:443/v2/test/blobs/uploads/"+uuid) 4787 w.WriteHeader(http.StatusAccepted) 4788 return 4789 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid: 4790 if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" { 4791 w.WriteHeader(http.StatusBadRequest) 4792 break 4793 } 4794 if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() { 4795 w.WriteHeader(http.StatusBadRequest) 4796 break 4797 } 4798 buf := bytes.NewBuffer(nil) 4799 if _, err := buf.ReadFrom(r.Body); err != nil { 4800 t.Errorf("fail to read: %v", err) 4801 } 4802 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 4803 w.WriteHeader(http.StatusCreated) 4804 return 4805 default: 4806 w.WriteHeader(http.StatusForbidden) 4807 } 4808 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4809 })) 4810 defer ts.Close() 4811 uri, err = url.Parse(ts.URL) 4812 if err != nil { 4813 t.Fatalf("invalid test https server: %v", err) 4814 } 4815 4816 // Test case with Host: "registry.wabbit-networks.io:443", 4817 // Location: "registry.wabbit-networks.io:443" 4818 testRegistry = "registry.wabbit-networks.io:443" 4819 store, err = blobStore_Push_Port443_HTTPS_create_store(uri, testRegistry) 4820 if err != nil { 4821 t.Fatalf("blobStore_Push_Port443_HTTPS_create_store() error = %v", err) 4822 } 4823 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 4824 if err != nil { 4825 t.Fatalf("Blobs.Push() error = %v", err) 4826 } 4827 4828 // Test case with Host: "registry.wabbit-networks.io", 4829 // Location: "registry.wabbit-networks.io:443" 4830 testRegistry = "registry.wabbit-networks.io" 4831 store, err = blobStore_Push_Port443_HTTPS_create_store(uri, testRegistry) 4832 if err != nil { 4833 t.Fatalf("blobStore_Push_Port443_HTTPS_create_store() error = %v", err) 4834 } 4835 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 4836 if err != nil { 4837 t.Fatalf("Blobs.Push() error = %v", err) 4838 } 4839 } 4840 4841 // Testing `last` parameter for Tags list 4842 func TestRepository_Tags_WithLastParam(t *testing.T) { 4843 tagSet := strings.Split("abcdefghijklmnopqrstuvwxyz", "") 4844 var offset int 4845 var ts *httptest.Server 4846 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4847 if r.Method != http.MethodGet || r.URL.Path != "/v2/test/tags/list" { 4848 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4849 w.WriteHeader(http.StatusNotFound) 4850 return 4851 } 4852 q := r.URL.Query() 4853 n, err := strconv.Atoi(q.Get("n")) 4854 if err != nil || n != 4 { 4855 t.Errorf("bad page size: %s", q.Get("n")) 4856 w.WriteHeader(http.StatusBadRequest) 4857 return 4858 } 4859 last := q.Get("last") 4860 if last != "" { 4861 offset = indexOf(last, tagSet) + 1 4862 } 4863 var tags []string 4864 switch q.Get("test") { 4865 case "foo": 4866 tags = tagSet[offset : offset+n] 4867 offset += n 4868 w.Header().Set("Link", fmt.Sprintf(`<%s/v2/test/tags/list?n=4&last=v&test=bar>; rel="next"`, ts.URL)) 4869 case "bar": 4870 tags = tagSet[offset : offset+n] 4871 default: 4872 tags = tagSet[offset : offset+n] 4873 offset += n 4874 w.Header().Set("Link", fmt.Sprintf(`<%s/v2/test/tags/list?n=4&last=r&test=foo>; rel="next"`, ts.URL)) 4875 } 4876 result := struct { 4877 Tags []string `json:"tags"` 4878 }{ 4879 Tags: tags, 4880 } 4881 if err := json.NewEncoder(w).Encode(result); err != nil { 4882 t.Errorf("failed to write response: %v", err) 4883 } 4884 })) 4885 defer ts.Close() 4886 uri, err := url.Parse(ts.URL) 4887 if err != nil { 4888 t.Fatalf("invalid test http server: %v", err) 4889 } 4890 4891 repo, err := NewRepository(uri.Host + "/test") 4892 if err != nil { 4893 t.Fatalf("NewRepository() error = %v", err) 4894 } 4895 repo.PlainHTTP = true 4896 repo.TagListPageSize = 4 4897 last := "n" 4898 startInd := indexOf(last, tagSet) + 1 4899 4900 ctx := context.Background() 4901 if err := repo.Tags(ctx, last, func(got []string) error { 4902 want := tagSet[startInd : startInd+repo.TagListPageSize] 4903 startInd += repo.TagListPageSize 4904 if !reflect.DeepEqual(got, want) { 4905 t.Errorf("Registry.Repositories() = %v, want %v", got, want) 4906 } 4907 return nil 4908 }); err != nil { 4909 t.Errorf("Repository.Tags() error = %v", err) 4910 } 4911 } 4912 4913 func TestRepository_ParseReference(t *testing.T) { 4914 type args struct { 4915 reference string 4916 } 4917 tests := []struct { 4918 name string 4919 repoRef registry.Reference 4920 args args 4921 want registry.Reference 4922 wantErr error 4923 }{ 4924 { 4925 name: "parse tag", 4926 repoRef: registry.Reference{ 4927 Registry: "registry.example.com", 4928 Repository: "hello-world", 4929 }, 4930 args: args{ 4931 reference: "foobar", 4932 }, 4933 want: registry.Reference{ 4934 Registry: "registry.example.com", 4935 Repository: "hello-world", 4936 Reference: "foobar", 4937 }, 4938 wantErr: nil, 4939 }, 4940 { 4941 name: "parse digest", 4942 repoRef: registry.Reference{ 4943 Registry: "registry.example.com", 4944 Repository: "hello-world", 4945 }, 4946 args: args{ 4947 reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 4948 }, 4949 want: registry.Reference{ 4950 Registry: "registry.example.com", 4951 Repository: "hello-world", 4952 Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 4953 }, 4954 wantErr: nil, 4955 }, 4956 { 4957 name: "parse tag@digest", 4958 repoRef: registry.Reference{ 4959 Registry: "registry.example.com", 4960 Repository: "hello-world", 4961 }, 4962 args: args{ 4963 reference: "foobar@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 4964 }, 4965 want: registry.Reference{ 4966 Registry: "registry.example.com", 4967 Repository: "hello-world", 4968 Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 4969 }, 4970 wantErr: nil, 4971 }, 4972 { 4973 name: "parse FQDN tag", 4974 repoRef: registry.Reference{ 4975 Registry: "registry.example.com", 4976 Repository: "hello-world", 4977 }, 4978 args: args{ 4979 reference: "registry.example.com/hello-world:foobar", 4980 }, 4981 want: registry.Reference{ 4982 Registry: "registry.example.com", 4983 Repository: "hello-world", 4984 Reference: "foobar", 4985 }, 4986 wantErr: nil, 4987 }, 4988 { 4989 name: "parse FQDN digest", 4990 repoRef: registry.Reference{ 4991 Registry: "registry.example.com", 4992 Repository: "hello-world", 4993 }, 4994 args: args{ 4995 reference: "registry.example.com/hello-world@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 4996 }, 4997 want: registry.Reference{ 4998 Registry: "registry.example.com", 4999 Repository: "hello-world", 5000 Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 5001 }, 5002 wantErr: nil, 5003 }, 5004 { 5005 name: "parse FQDN tag@digest", 5006 repoRef: registry.Reference{ 5007 Registry: "registry.example.com", 5008 Repository: "hello-world", 5009 }, 5010 args: args{ 5011 reference: "registry.example.com/hello-world:foobar@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 5012 }, 5013 want: registry.Reference{ 5014 Registry: "registry.example.com", 5015 Repository: "hello-world", 5016 Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 5017 }, 5018 wantErr: nil, 5019 }, 5020 { 5021 name: "empty reference", 5022 repoRef: registry.Reference{ 5023 Registry: "registry.example.com", 5024 Repository: "hello-world", 5025 }, 5026 args: args{ 5027 reference: "", 5028 }, 5029 want: registry.Reference{}, 5030 wantErr: errdef.ErrInvalidReference, 5031 }, 5032 { 5033 name: "missing repository", 5034 repoRef: registry.Reference{ 5035 Registry: "registry.example.com", 5036 Repository: "hello-world", 5037 }, 5038 args: args{ 5039 reference: "myregistry.example.com:hello-world", 5040 }, 5041 want: registry.Reference{}, 5042 wantErr: errdef.ErrInvalidReference, 5043 }, 5044 { 5045 name: "missing reference", 5046 repoRef: registry.Reference{ 5047 Registry: "registry.example.com", 5048 Repository: "hello-world", 5049 }, 5050 args: args{ 5051 reference: "registry.example.com/hello-world", 5052 }, 5053 want: registry.Reference{}, 5054 wantErr: errdef.ErrInvalidReference, 5055 }, 5056 { 5057 name: "registry mismatch", 5058 repoRef: registry.Reference{ 5059 Registry: "registry.example.com", 5060 Repository: "hello-world", 5061 }, 5062 args: args{ 5063 reference: "myregistry.example.com/hello-world:foobar@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 5064 }, 5065 want: registry.Reference{}, 5066 wantErr: errdef.ErrInvalidReference, 5067 }, 5068 { 5069 name: "repository mismatch", 5070 repoRef: registry.Reference{ 5071 Registry: "registry.example.com", 5072 Repository: "hello-world", 5073 }, 5074 args: args{ 5075 reference: "registry.example.com/goodbye-world:foobar@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 5076 }, 5077 want: registry.Reference{}, 5078 wantErr: errdef.ErrInvalidReference, 5079 }, 5080 { 5081 name: "digest posing as a tag", 5082 repoRef: registry.Reference{ 5083 Registry: "registry.example.com", 5084 Repository: "hello-world", 5085 }, 5086 args: args{ 5087 reference: "registry.example.com:5000/hello-world:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 5088 }, 5089 want: registry.Reference{}, 5090 wantErr: errdef.ErrInvalidReference, 5091 }, 5092 { 5093 name: "missing reference after the at sign", 5094 repoRef: registry.Reference{ 5095 Registry: "registry.example.com", 5096 Repository: "hello-world", 5097 }, 5098 args: args{ 5099 reference: "registry.example.com/hello-world@", 5100 }, 5101 want: registry.Reference{}, 5102 wantErr: errdef.ErrInvalidReference, 5103 }, 5104 { 5105 name: "missing reference after the colon", 5106 repoRef: registry.Reference{ 5107 Registry: "localhost", 5108 }, 5109 args: args{ 5110 reference: "localhost:5000/hello:", 5111 }, 5112 want: registry.Reference{}, 5113 wantErr: errdef.ErrInvalidReference, 5114 }, 5115 { 5116 name: "zero-size tag, zero-size digest", 5117 repoRef: registry.Reference{}, 5118 args: args{ 5119 reference: "localhost:5000/hello:@", 5120 }, 5121 want: registry.Reference{}, 5122 wantErr: errdef.ErrInvalidReference, 5123 }, 5124 { 5125 name: "zero-size tag with valid digest", 5126 repoRef: registry.Reference{}, 5127 args: args{ 5128 reference: "localhost:5000/hello:@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 5129 }, 5130 want: registry.Reference{}, 5131 wantErr: errdef.ErrInvalidReference, 5132 }, 5133 { 5134 name: "valid tag with zero-size digest", 5135 repoRef: registry.Reference{}, 5136 args: args{ 5137 reference: "localhost:5000/hello:foobar@", 5138 }, 5139 want: registry.Reference{}, 5140 wantErr: errdef.ErrInvalidReference, 5141 }, 5142 } 5143 for _, tt := range tests { 5144 t.Run(tt.name, func(t *testing.T) { 5145 r := &Repository{ 5146 Reference: tt.repoRef, 5147 } 5148 got, err := r.ParseReference(tt.args.reference) 5149 if !errors.Is(err, tt.wantErr) { 5150 t.Errorf("Repository.ParseReference() error = %v, wantErr %v", err, tt.wantErr) 5151 return 5152 } 5153 if !reflect.DeepEqual(got, tt.want) { 5154 t.Errorf("Repository.ParseReference() = %v, want %v", got, tt.want) 5155 } 5156 }) 5157 } 5158 } 5159 5160 func TestRepository_SetReferrersCapability(t *testing.T) { 5161 repo, err := NewRepository("registry.example.com/test") 5162 if err != nil { 5163 t.Fatalf("NewRepository() error = %v", err) 5164 } 5165 // initial state 5166 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5167 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5168 } 5169 5170 // valid first time set 5171 if err := repo.SetReferrersCapability(true); err != nil { 5172 t.Errorf("Repository.SetReferrersCapability() error = %v", err) 5173 } 5174 if state := repo.loadReferrersState(); state != referrersStateSupported { 5175 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 5176 } 5177 5178 // invalid second time set, state should be no changed 5179 if err := repo.SetReferrersCapability(false); !errors.Is(err, ErrReferrersCapabilityAlreadySet) { 5180 t.Errorf("Repository.SetReferrersCapability() error = %v, wantErr %v", err, ErrReferrersCapabilityAlreadySet) 5181 } 5182 if state := repo.loadReferrersState(); state != referrersStateSupported { 5183 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 5184 } 5185 } 5186 5187 func Test_generateIndex(t *testing.T) { 5188 referrer_1 := ocispec.Artifact{ 5189 MediaType: ocispec.MediaTypeArtifactManifest, 5190 ArtifactType: "foo", 5191 } 5192 referrerJSON_1, err := json.Marshal(referrer_1) 5193 if err != nil { 5194 t.Fatal("failed to marshal manifest:", err) 5195 } 5196 referrer_2 := ocispec.Artifact{ 5197 MediaType: ocispec.MediaTypeArtifactManifest, 5198 ArtifactType: "bar", 5199 } 5200 referrerJSON_2, err := json.Marshal(referrer_2) 5201 if err != nil { 5202 t.Fatal("failed to marshal manifest:", err) 5203 } 5204 referrers := []ocispec.Descriptor{ 5205 content.NewDescriptorFromBytes(referrer_1.MediaType, referrerJSON_1), 5206 content.NewDescriptorFromBytes(referrer_2.MediaType, referrerJSON_2), 5207 } 5208 5209 wantIndex := ocispec.Index{ 5210 Versioned: specs.Versioned{ 5211 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 5212 }, 5213 MediaType: ocispec.MediaTypeImageIndex, 5214 Manifests: referrers, 5215 } 5216 wantIndexJSON, err := json.Marshal(wantIndex) 5217 if err != nil { 5218 t.Fatal("failed to marshal index:", err) 5219 } 5220 wantIndexDesc := content.NewDescriptorFromBytes(wantIndex.MediaType, wantIndexJSON) 5221 5222 wantEmptyIndex := ocispec.Index{ 5223 Versioned: specs.Versioned{ 5224 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 5225 }, 5226 MediaType: ocispec.MediaTypeImageIndex, 5227 Manifests: []ocispec.Descriptor{}, 5228 } 5229 wantEmptyIndexJSON, err := json.Marshal(wantEmptyIndex) 5230 if err != nil { 5231 t.Fatal("failed to marshal index:", err) 5232 } 5233 wantEmptyIndexDesc := content.NewDescriptorFromBytes(wantEmptyIndex.MediaType, wantEmptyIndexJSON) 5234 5235 tests := []struct { 5236 name string 5237 manifests []ocispec.Descriptor 5238 wantDesc ocispec.Descriptor 5239 wantBytes []byte 5240 wantErr bool 5241 }{ 5242 { 5243 name: "non-empty referrers list", 5244 manifests: referrers, 5245 wantDesc: wantIndexDesc, 5246 wantBytes: wantIndexJSON, 5247 wantErr: false, 5248 }, 5249 { 5250 name: "nil referrers list", 5251 manifests: nil, 5252 wantDesc: wantEmptyIndexDesc, 5253 wantBytes: wantEmptyIndexJSON, 5254 wantErr: false, 5255 }, 5256 { 5257 name: "empty referrers list", 5258 manifests: nil, 5259 wantDesc: wantEmptyIndexDesc, 5260 wantBytes: wantEmptyIndexJSON, 5261 wantErr: false, 5262 }, 5263 } 5264 for _, tt := range tests { 5265 t.Run(tt.name, func(t *testing.T) { 5266 got, got1, err := generateIndex(tt.manifests) 5267 if (err != nil) != tt.wantErr { 5268 t.Errorf("generateReferrersIndex() error = %v, wantErr %v", err, tt.wantErr) 5269 return 5270 } 5271 if !reflect.DeepEqual(got, tt.wantDesc) { 5272 t.Errorf("generateReferrersIndex() got = %v, want %v", got, tt.wantDesc) 5273 } 5274 if !reflect.DeepEqual(got1, tt.wantBytes) { 5275 t.Errorf("generateReferrersIndex() got1 = %v, want %v", got1, tt.wantBytes) 5276 } 5277 }) 5278 } 5279 } 5280 5281 func TestRepository_pingReferrers(t *testing.T) { 5282 // referrers available 5283 count := 0 5284 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5285 switch { 5286 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 5287 count++ 5288 w.WriteHeader(http.StatusOK) 5289 default: 5290 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5291 w.WriteHeader(http.StatusNotFound) 5292 } 5293 5294 })) 5295 defer ts.Close() 5296 uri, err := url.Parse(ts.URL) 5297 if err != nil { 5298 t.Fatalf("invalid test http server: %v", err) 5299 } 5300 5301 ctx := context.Background() 5302 repo, err := NewRepository(uri.Host + "/test") 5303 if err != nil { 5304 t.Fatalf("NewRepository() error = %v", err) 5305 } 5306 repo.PlainHTTP = true 5307 5308 // 1st call 5309 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5310 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5311 } 5312 got, err := repo.pingReferrers(ctx) 5313 if err != nil { 5314 t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 5315 } 5316 if got != true { 5317 t.Errorf("Repository.pingReferrers() = %v, want %v", got, true) 5318 } 5319 if state := repo.loadReferrersState(); state != referrersStateSupported { 5320 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 5321 } 5322 if count != 1 { 5323 t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) 5324 } 5325 5326 // 2nd call 5327 if state := repo.loadReferrersState(); state != referrersStateSupported { 5328 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 5329 } 5330 got, err = repo.pingReferrers(ctx) 5331 if err != nil { 5332 t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 5333 } 5334 if got != true { 5335 t.Errorf("Repository.pingReferrers() = %v, want %v", got, true) 5336 } 5337 if state := repo.loadReferrersState(); state != referrersStateSupported { 5338 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 5339 } 5340 if count != 1 { 5341 t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) 5342 } 5343 5344 // referrers unavailable 5345 count = 0 5346 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5347 switch { 5348 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 5349 count++ 5350 w.WriteHeader(http.StatusNotFound) 5351 default: 5352 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5353 w.WriteHeader(http.StatusNotFound) 5354 } 5355 5356 })) 5357 defer ts.Close() 5358 uri, err = url.Parse(ts.URL) 5359 if err != nil { 5360 t.Fatalf("invalid test http server: %v", err) 5361 } 5362 5363 ctx = context.Background() 5364 repo, err = NewRepository(uri.Host + "/test") 5365 if err != nil { 5366 t.Fatalf("NewRepository() error = %v", err) 5367 } 5368 repo.PlainHTTP = true 5369 5370 // 1st call 5371 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5372 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5373 } 5374 got, err = repo.pingReferrers(ctx) 5375 if err != nil { 5376 t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 5377 } 5378 if got != false { 5379 t.Errorf("Repository.pingReferrers() = %v, want %v", got, false) 5380 } 5381 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 5382 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 5383 } 5384 if count != 1 { 5385 t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) 5386 } 5387 5388 // 2nd call 5389 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 5390 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 5391 } 5392 got, err = repo.pingReferrers(ctx) 5393 if err != nil { 5394 t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 5395 } 5396 if got != false { 5397 t.Errorf("Repository.pingReferrers() = %v, want %v", got, false) 5398 } 5399 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 5400 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 5401 } 5402 if count != 1 { 5403 t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) 5404 } 5405 } 5406 5407 func TestRepository_pingReferrers_RepositoryNotFound(t *testing.T) { 5408 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5409 if r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest { 5410 w.WriteHeader(http.StatusNotFound) 5411 w.Write([]byte(`{ "errors": [ { "code": "NAME_UNKNOWN", "message": "repository name not known to registry" } ] }`)) 5412 return 5413 } 5414 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 5415 w.WriteHeader(http.StatusNotFound) 5416 })) 5417 defer ts.Close() 5418 uri, err := url.Parse(ts.URL) 5419 if err != nil { 5420 t.Fatalf("invalid test http server: %v", err) 5421 } 5422 ctx := context.Background() 5423 5424 // test referrers state unknown 5425 repo, err := NewRepository(uri.Host + "/test") 5426 if err != nil { 5427 t.Fatalf("NewRepository() error = %v", err) 5428 } 5429 repo.PlainHTTP = true 5430 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5431 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5432 } 5433 if _, err = repo.pingReferrers(ctx); err == nil { 5434 t.Fatalf("Repository.pingReferrers() error = %v, wantErr %v", err, true) 5435 } 5436 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5437 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5438 } 5439 5440 // test referrers state supported 5441 repo, err = NewRepository(uri.Host + "/test") 5442 if err != nil { 5443 t.Fatalf("NewRepository() error = %v", err) 5444 } 5445 repo.PlainHTTP = true 5446 repo.SetReferrersCapability(true) 5447 if state := repo.loadReferrersState(); state != referrersStateSupported { 5448 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 5449 } 5450 got, err := repo.pingReferrers(ctx) 5451 if err != nil { 5452 t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 5453 } 5454 if got != true { 5455 t.Errorf("Repository.pingReferrers() = %v, want %v", got, true) 5456 } 5457 if state := repo.loadReferrersState(); state != referrersStateSupported { 5458 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 5459 } 5460 5461 // test referrers state unsupported 5462 repo, err = NewRepository(uri.Host + "/test") 5463 if err != nil { 5464 t.Fatalf("NewRepository() error = %v", err) 5465 } 5466 repo.PlainHTTP = true 5467 repo.SetReferrersCapability(false) 5468 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 5469 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 5470 } 5471 got, err = repo.pingReferrers(ctx) 5472 if err != nil { 5473 t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 5474 } 5475 if got != false { 5476 t.Errorf("Repository.pingReferrers() = %v, want %v", got, false) 5477 } 5478 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 5479 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 5480 } 5481 } 5482 5483 func TestRepository_pingReferrers_Concurrent(t *testing.T) { 5484 // referrers available 5485 var count int32 5486 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5487 switch { 5488 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 5489 atomic.AddInt32(&count, 1) 5490 w.WriteHeader(http.StatusOK) 5491 default: 5492 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5493 w.WriteHeader(http.StatusNotFound) 5494 } 5495 5496 })) 5497 defer ts.Close() 5498 uri, err := url.Parse(ts.URL) 5499 if err != nil { 5500 t.Fatalf("invalid test http server: %v", err) 5501 } 5502 5503 ctx := context.Background() 5504 repo, err := NewRepository(uri.Host + "/test") 5505 if err != nil { 5506 t.Fatalf("NewRepository() error = %v", err) 5507 } 5508 repo.PlainHTTP = true 5509 5510 concurrency := 64 5511 eg, egCtx := errgroup.WithContext(ctx) 5512 5513 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5514 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5515 } 5516 for i := 0; i < concurrency; i++ { 5517 eg.Go(func() func() error { 5518 return func() error { 5519 got, err := repo.pingReferrers(egCtx) 5520 if err != nil { 5521 t.Fatalf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 5522 } 5523 if got != true { 5524 t.Errorf("Repository.pingReferrers() = %v, want %v", got, true) 5525 } 5526 return nil 5527 } 5528 }()) 5529 } 5530 if err := eg.Wait(); err != nil { 5531 t.Fatal(err) 5532 } 5533 5534 if got := atomic.LoadInt32(&count); got != 1 { 5535 t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) 5536 } 5537 if state := repo.loadReferrersState(); state != referrersStateSupported { 5538 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 5539 } 5540 }