github.com/dtroyer-salad/og2/v2@v2.0.0-20240412154159-c47231610877/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/opencontainers/go-digest" 37 specs "github.com/opencontainers/image-spec/specs-go" 38 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 39 "golang.org/x/sync/errgroup" 40 "oras.land/oras-go/v2/content" 41 "oras.land/oras-go/v2/errdef" 42 "oras.land/oras-go/v2/internal/interfaces" 43 "oras.land/oras-go/v2/internal/spec" 44 "oras.land/oras-go/v2/registry" 45 "oras.land/oras-go/v2/registry/remote/auth" 46 "oras.land/oras-go/v2/registry/remote/errcode" 47 ) 48 49 type testIOStruct struct { 50 isTag bool 51 clientSuppliedReference string 52 serverCalculatedDigest digest.Digest // for non-HEAD (body-containing) requests only 53 errExpectedOnHEAD bool 54 errExpectedOnGET bool 55 } 56 57 const theAmazingBanClan = "Ban Gu, Ban Chao, Ban Zhao" 58 const theAmazingBanDigest = "b526a4f2be963a2f9b0990c001255669eab8a254ab1a6e3f84f1820212ac7078" 59 60 // The following truth table aims to cover the expected GET/HEAD request outcome 61 // for all possible permutations of the client/server "containing a digest", for 62 // both Manifests and Blobs. Where the results between the two differ, the index 63 // of the first column has an exclamation mark. 64 // 65 // The client is said to "contain a digest" if the user-supplied reference string 66 // is of the form that contains a digest rather than a tag. The server, on the 67 // other hand, is said to "contain a digest" if the server responded with the 68 // special header `Docker-Content-Digest`. 69 // 70 // In this table, anything denoted with an asterisk indicates that the true 71 // response should actually be the opposite of what's expected; for example, 72 // `*PASS` means we will get a `PASS`, even though the true answer would be its 73 // diametric opposite--a `FAIL`. This may seem odd, and deserves an explanation. 74 // This function has blind-spots, and while it can expend power to gain sight, 75 // i.e., perform the expensive validation, we chose not to. The reason is two- 76 // fold: a) we "know" that even if we say "!PASS", it will eventually fail later 77 // when checks are performed, and with that assumption, we have the luxury for 78 // the second point, which is b) performance. 79 // 80 // _______________________________________________________________________________________________________________ 81 // | ID | CLIENT | SERVER | Manifest.GET | Blob.GET | Manifest.HEAD | Blob.HEAD | 82 // |----+-----------------+------------------+-----------------------+-----------+---------------------+-----------+ 83 // | 1 | tag | missing | CALCULATE,PASS | n/a | FAIL | n/a | 84 // | 2 | tag | presentCorrect | TRUST,PASS | n/a | TRUST,PASS | n/a | 85 // | 3 | tag | presentIncorrect | TRUST,*PASS | n/a | TRUST,*PASS | n/a | 86 // | 4 | correctDigest | missing | TRUST,PASS | PASS | TRUST,PASS | PASS | 87 // | 5 | correctDigest | presentCorrect | TRUST,COMPARE,PASS | PASS | TRUST,COMPARE,PASS | PASS | 88 // | 6 | correctDigest | presentIncorrect | TRUST,COMPARE,FAIL | FAIL | TRUST,COMPARE,FAIL | FAIL | 89 // --------------------------------------------------------------------------------------------------------------- 90 func getTestIOStructMapForGetDescriptorClass() map[string]testIOStruct { 91 correctDigest := fmt.Sprintf("sha256:%v", theAmazingBanDigest) 92 incorrectDigest := fmt.Sprintf("sha256:%v", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff") 93 94 return map[string]testIOStruct{ 95 "1. Client:Tag & Server:DigestMissing": { 96 isTag: true, 97 errExpectedOnHEAD: true, 98 }, 99 "2. Client:Tag & Server:DigestValid": { 100 isTag: true, 101 serverCalculatedDigest: digest.Digest(correctDigest), 102 }, 103 "3. Client:Tag & Server:DigestWrongButSyntacticallyValid": { 104 isTag: true, 105 serverCalculatedDigest: digest.Digest(incorrectDigest), 106 }, 107 "4. Client:DigestValid & Server:DigestMissing": { 108 clientSuppliedReference: correctDigest, 109 }, 110 "5. Client:DigestValid & Server:DigestValid": { 111 clientSuppliedReference: correctDigest, 112 serverCalculatedDigest: digest.Digest(correctDigest), 113 }, 114 "6. Client:DigestValid & Server:DigestWrongButSyntacticallyValid": { 115 clientSuppliedReference: correctDigest, 116 serverCalculatedDigest: digest.Digest(incorrectDigest), 117 errExpectedOnHEAD: true, 118 errExpectedOnGET: true, 119 }, 120 } 121 } 122 123 func TestRepository_Fetch(t *testing.T) { 124 blob := []byte("hello world") 125 blobDesc := ocispec.Descriptor{ 126 MediaType: "test", 127 Digest: digest.FromBytes(blob), 128 Size: int64(len(blob)), 129 } 130 index := []byte(`{"manifests":[]}`) 131 indexDesc := ocispec.Descriptor{ 132 MediaType: ocispec.MediaTypeImageIndex, 133 Digest: digest.FromBytes(index), 134 Size: int64(len(index)), 135 } 136 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 137 if r.Method != http.MethodGet { 138 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 139 w.WriteHeader(http.StatusMethodNotAllowed) 140 return 141 } 142 switch r.URL.Path { 143 case "/v2/test/blobs/" + blobDesc.Digest.String(): 144 w.Header().Set("Content-Type", "application/octet-stream") 145 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 146 if _, err := w.Write(blob); err != nil { 147 t.Errorf("failed to write %q: %v", r.URL, err) 148 } 149 case "/v2/test/manifests/" + indexDesc.Digest.String(): 150 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { 151 t.Errorf("manifest not convertable: %s", accept) 152 w.WriteHeader(http.StatusBadRequest) 153 return 154 } 155 w.Header().Set("Content-Type", indexDesc.MediaType) 156 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 157 if _, err := w.Write(index); err != nil { 158 t.Errorf("failed to write %q: %v", r.URL, err) 159 } 160 default: 161 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 162 w.WriteHeader(http.StatusNotFound) 163 } 164 })) 165 defer ts.Close() 166 uri, err := url.Parse(ts.URL) 167 if err != nil { 168 t.Fatalf("invalid test http server: %v", err) 169 } 170 171 repo, err := NewRepository(uri.Host + "/test") 172 if err != nil { 173 t.Fatalf("NewRepository() error = %v", err) 174 } 175 repo.PlainHTTP = true 176 ctx := context.Background() 177 178 rc, err := repo.Fetch(ctx, blobDesc) 179 if err != nil { 180 t.Fatalf("Repository.Fetch() error = %v", err) 181 } 182 buf := bytes.NewBuffer(nil) 183 if _, err := buf.ReadFrom(rc); err != nil { 184 t.Errorf("fail to read: %v", err) 185 } 186 if err := rc.Close(); err != nil { 187 t.Errorf("fail to close: %v", err) 188 } 189 if got := buf.Bytes(); !bytes.Equal(got, blob) { 190 t.Errorf("Repository.Fetch() = %v, want %v", got, blob) 191 } 192 193 rc, err = repo.Fetch(ctx, indexDesc) 194 if err != nil { 195 t.Fatalf("Repository.Fetch() error = %v", err) 196 } 197 buf.Reset() 198 if _, err := buf.ReadFrom(rc); err != nil { 199 t.Errorf("fail to read: %v", err) 200 } 201 if err := rc.Close(); err != nil { 202 t.Errorf("fail to close: %v", err) 203 } 204 if got := buf.Bytes(); !bytes.Equal(got, index) { 205 t.Errorf("Repository.Fetch() = %v, want %v", got, index) 206 } 207 } 208 209 func TestRepository_Push(t *testing.T) { 210 blob := []byte("hello world") 211 blobDesc := ocispec.Descriptor{ 212 MediaType: "test", 213 Digest: digest.FromBytes(blob), 214 Size: int64(len(blob)), 215 } 216 var gotBlob []byte 217 index := []byte(`{"manifests":[]}`) 218 indexDesc := ocispec.Descriptor{ 219 MediaType: ocispec.MediaTypeImageIndex, 220 Digest: digest.FromBytes(index), 221 Size: int64(len(index)), 222 } 223 var gotIndex []byte 224 uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c" 225 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 226 switch { 227 case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/": 228 w.Header().Set("Location", "/v2/test/blobs/uploads/"+uuid) 229 w.WriteHeader(http.StatusAccepted) 230 return 231 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid: 232 if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" { 233 w.WriteHeader(http.StatusBadRequest) 234 break 235 } 236 if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() { 237 w.WriteHeader(http.StatusBadRequest) 238 break 239 } 240 buf := bytes.NewBuffer(nil) 241 if _, err := buf.ReadFrom(r.Body); err != nil { 242 t.Errorf("fail to read: %v", err) 243 } 244 gotBlob = buf.Bytes() 245 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 246 w.WriteHeader(http.StatusCreated) 247 return 248 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 249 if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { 250 w.WriteHeader(http.StatusBadRequest) 251 break 252 } 253 buf := bytes.NewBuffer(nil) 254 if _, err := buf.ReadFrom(r.Body); err != nil { 255 t.Errorf("fail to read: %v", err) 256 } 257 gotIndex = buf.Bytes() 258 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 259 w.WriteHeader(http.StatusCreated) 260 return 261 default: 262 w.WriteHeader(http.StatusForbidden) 263 } 264 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 265 })) 266 defer ts.Close() 267 uri, err := url.Parse(ts.URL) 268 if err != nil { 269 t.Fatalf("invalid test http server: %v", err) 270 } 271 272 repo, err := NewRepository(uri.Host + "/test") 273 if err != nil { 274 t.Fatalf("NewRepository() error = %v", err) 275 } 276 repo.PlainHTTP = true 277 ctx := context.Background() 278 279 err = repo.Push(ctx, blobDesc, bytes.NewReader(blob)) 280 if err != nil { 281 t.Fatalf("Repository.Push() error = %v", err) 282 } 283 if !bytes.Equal(gotBlob, blob) { 284 t.Errorf("Repository.Push() = %v, want %v", gotBlob, blob) 285 } 286 287 err = repo.Push(ctx, indexDesc, bytes.NewReader(index)) 288 if err != nil { 289 t.Fatalf("Repository.Push() error = %v", err) 290 } 291 if !bytes.Equal(gotIndex, index) { 292 t.Errorf("Repository.Push() = %v, want %v", gotIndex, index) 293 } 294 } 295 296 func TestRepository_Mount(t *testing.T) { 297 blob := []byte("hello world") 298 blobDesc := ocispec.Descriptor{ 299 MediaType: "test", 300 Digest: digest.FromBytes(blob), 301 Size: int64(len(blob)), 302 } 303 gotMount := 0 304 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 305 if got, want := r.Method, "POST"; got != want { 306 t.Errorf("unexpected HTTP method; got %q want %q", got, want) 307 w.WriteHeader(http.StatusInternalServerError) 308 return 309 } 310 if err := r.ParseForm(); err != nil { 311 t.Errorf("invalid form in HTTP request: %v", err) 312 w.WriteHeader(http.StatusInternalServerError) 313 return 314 } 315 switch r.URL.Path { 316 case "/v2/test2/blobs/uploads/": 317 if got, want := r.Form.Get("mount"), blobDesc.Digest; digest.Digest(got) != want { 318 t.Errorf("unexpected value for 'mount' parameter; got %q want %q", got, want) 319 } 320 if got, want := r.Form.Get("from"), "test"; got != want { 321 t.Errorf("unexpected value for 'from' parameter; got %q want %q", got, want) 322 } 323 gotMount++ 324 w.Header().Set(headerDockerContentDigest, blobDesc.Digest.String()) 325 w.WriteHeader(201) 326 return 327 default: 328 t.Errorf("unexpected URL for mount request %q", r.URL) 329 w.WriteHeader(http.StatusInternalServerError) 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 repo, err := NewRepository(uri.Host + "/test2") 338 if err != nil { 339 t.Fatalf("NewRepository() error = %v", err) 340 } 341 repo.PlainHTTP = true 342 ctx := context.Background() 343 344 err = repo.Mount(ctx, blobDesc, "test", nil) 345 if err != nil { 346 t.Fatalf("Repository.Push() error = %v", err) 347 } 348 if gotMount != 1 { 349 t.Errorf("did not get expected mount request") 350 } 351 } 352 353 func TestRepository_Mount_Fallback(t *testing.T) { 354 // This test checks the case where the server does not know 355 // about the mount query parameters, so the call falls back to 356 // the regular push flow. This test is thus very similar to TestPush, 357 // except that it doesn't push a manifest because mounts aren't 358 // documented to be supported for manifests. 359 360 blob := []byte("hello world") 361 blobDesc := ocispec.Descriptor{ 362 MediaType: "test", 363 Digest: digest.FromBytes(blob), 364 Size: int64(len(blob)), 365 } 366 var sequence string 367 var gotBlob []byte 368 uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c" 369 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 370 switch { 371 case r.Method == http.MethodPost && r.URL.Path == "/v2/test2/blobs/uploads/": 372 w.Header().Set("Location", "/v2/test2/blobs/uploads/"+uuid) 373 w.WriteHeader(http.StatusAccepted) 374 sequence += "post " 375 return 376 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/blobs/"+blobDesc.Digest.String(): 377 w.Header().Set("Content-Type", "application/octet-stream") 378 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 379 if _, err := w.Write(blob); err != nil { 380 t.Errorf("failed to write %q: %v", r.URL, err) 381 } 382 sequence += "get " 383 return 384 case r.Method == http.MethodPut && r.URL.Path == "/v2/test2/blobs/uploads/"+uuid: 385 if got, want := r.Header.Get("Content-Type"), "application/octet-stream"; got != want { 386 t.Errorf("unexpected content type; got %q want %q", got, want) 387 w.WriteHeader(http.StatusBadRequest) 388 return 389 } 390 if got, want := r.URL.Query().Get("digest"), blobDesc.Digest.String(); got != want { 391 t.Errorf("unexpected content digest; got %q want %q", got, want) 392 w.WriteHeader(http.StatusBadRequest) 393 return 394 } 395 data, err := io.ReadAll(r.Body) 396 if err != nil { 397 t.Errorf("error reading body: %v", err) 398 w.WriteHeader(http.StatusInternalServerError) 399 return 400 } 401 gotBlob = data 402 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 403 w.WriteHeader(http.StatusCreated) 404 sequence += "put " 405 return 406 default: 407 w.WriteHeader(http.StatusForbidden) 408 } 409 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 410 })) 411 defer ts.Close() 412 uri, err := url.Parse(ts.URL) 413 if err != nil { 414 t.Fatalf("invalid test http server: %v", err) 415 } 416 417 repo, err := NewRepository(uri.Host + "/test2") 418 if err != nil { 419 t.Fatalf("NewRepository() error = %v", err) 420 } 421 repo.PlainHTTP = true 422 ctx := context.Background() 423 424 t.Run("getContent is nil", func(t *testing.T) { 425 sequence = "" 426 427 err = repo.Mount(ctx, blobDesc, "test", nil) 428 if err != nil { 429 t.Fatalf("Repository.Push() error = %v", err) 430 } 431 if !bytes.Equal(gotBlob, blob) { 432 t.Errorf("Repository.Mount() = %v, want %v", gotBlob, blob) 433 } 434 if got, want := sequence, "post get put "; got != want { 435 t.Errorf("unexpected request sequence; got %q want %q", got, want) 436 } 437 }) 438 439 t.Run("getContent is non nil", func(t *testing.T) { 440 sequence = "" 441 442 err = repo.Mount(ctx, blobDesc, "test", func() (io.ReadCloser, error) { 443 return io.NopCloser(bytes.NewReader(blob)), nil 444 }) 445 if err != nil { 446 t.Fatalf("Repository.Push() error = %v", err) 447 } 448 if !bytes.Equal(gotBlob, blob) { 449 t.Errorf("Repository.Mount() = %v, want %v", gotBlob, blob) 450 } 451 if got, want := sequence, "post put "; got != want { 452 t.Errorf("unexpected request sequence; got %q want %q", got, want) 453 } 454 }) 455 } 456 457 func TestRepository_Mount_Error(t *testing.T) { 458 blob := []byte("hello world") 459 blobDesc := ocispec.Descriptor{ 460 MediaType: "test", 461 Digest: digest.FromBytes(blob), 462 Size: int64(len(blob)), 463 } 464 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 465 if got, want := r.Method, "POST"; got != want { 466 t.Errorf("unexpected HTTP method; got %q want %q", got, want) 467 w.WriteHeader(http.StatusInternalServerError) 468 return 469 } 470 if err := r.ParseForm(); err != nil { 471 t.Errorf("invalid form in HTTP request: %v", err) 472 w.WriteHeader(http.StatusInternalServerError) 473 return 474 } 475 switch r.URL.Path { 476 case "/v2/test/blobs/uploads/": 477 w.WriteHeader(400) 478 w.Write([]byte(`{ "errors": [ { "code": "NAME_UNKNOWN", "message": "some error" } ] }`)) 479 default: 480 t.Errorf("unexpected URL for mount request %q", r.URL) 481 w.WriteHeader(http.StatusInternalServerError) 482 } 483 })) 484 defer ts.Close() 485 uri, err := url.Parse(ts.URL) 486 if err != nil { 487 t.Fatalf("invalid test http server: %v", err) 488 } 489 repo, err := NewRepository(uri.Host + "/test") 490 if err != nil { 491 t.Fatalf("NewRepository() error = %v", err) 492 } 493 repo.PlainHTTP = true 494 495 err = repo.Mount(context.Background(), blobDesc, "foo", nil) 496 if err == nil { 497 t.Fatalf("expected error but got success instead") 498 } 499 var errResp *errcode.ErrorResponse 500 if !errors.As(err, &errResp) { 501 t.Fatalf("unexpected error type %#v", err) 502 } 503 if !reflect.DeepEqual(errResp.Errors, errcode.Errors{{ 504 Code: "NAME_UNKNOWN", 505 Message: "some error", 506 }}) { 507 t.Errorf("unexpected errors %#v", errResp.Errors) 508 } 509 } 510 511 func TestRepository_Mount_Fallback_GetContent(t *testing.T) { 512 // This test checks the case where the server does not know 513 // about the mount query parameters, so the call falls back to 514 // the regular push flow, but using the getContent function 515 // parameter to get the content to push. 516 517 blob := []byte("hello world") 518 blobDesc := ocispec.Descriptor{ 519 MediaType: "test", 520 Digest: digest.FromBytes(blob), 521 Size: int64(len(blob)), 522 } 523 var sequence string 524 var gotBlob []byte 525 uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c" 526 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 527 switch { 528 case r.Method == http.MethodPost && r.URL.Path == "/v2/test2/blobs/uploads/": 529 w.Header().Set("Location", "/v2/test2/blobs/uploads/"+uuid) 530 w.WriteHeader(http.StatusAccepted) 531 sequence += "post " 532 return 533 case r.Method == http.MethodPut && r.URL.Path == "/v2/test2/blobs/uploads/"+uuid: 534 if got, want := r.Header.Get("Content-Type"), "application/octet-stream"; got != want { 535 t.Errorf("unexpected content type; got %q want %q", got, want) 536 w.WriteHeader(http.StatusBadRequest) 537 return 538 } 539 if got, want := r.URL.Query().Get("digest"), blobDesc.Digest.String(); got != want { 540 t.Errorf("unexpected content digest; got %q want %q", got, want) 541 w.WriteHeader(http.StatusBadRequest) 542 return 543 } 544 data, err := io.ReadAll(r.Body) 545 if err != nil { 546 t.Errorf("error reading body: %v", err) 547 w.WriteHeader(http.StatusInternalServerError) 548 return 549 } 550 gotBlob = data 551 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 552 w.WriteHeader(http.StatusCreated) 553 sequence += "put " 554 return 555 default: 556 w.WriteHeader(http.StatusForbidden) 557 } 558 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 559 })) 560 defer ts.Close() 561 uri, err := url.Parse(ts.URL) 562 if err != nil { 563 t.Fatalf("invalid test http server: %v", err) 564 } 565 566 repo, err := NewRepository(uri.Host + "/test2") 567 if err != nil { 568 t.Fatalf("NewRepository() error = %v", err) 569 } 570 repo.PlainHTTP = true 571 ctx := context.Background() 572 573 err = repo.Mount(ctx, blobDesc, "test", func() (io.ReadCloser, error) { 574 return io.NopCloser(bytes.NewReader(blob)), nil 575 }) 576 if err != nil { 577 t.Fatalf("Repository.Push() error = %v", err) 578 } 579 if !bytes.Equal(gotBlob, blob) { 580 t.Errorf("Repository.Mount() = %v, want %v", gotBlob, blob) 581 } 582 if got, want := sequence, "post put "; got != want { 583 t.Errorf("unexpected request sequence; got %q want %q", got, want) 584 } 585 } 586 587 func TestRepository_Mount_Fallback_GetContentError(t *testing.T) { 588 // This test checks the case where the server does not know 589 // about the mount query parameters, so the call falls back to 590 // the regular push flow, but it's possible the caller wants to 591 // avoid the pull/push pattern so returns an error from getContent 592 // and checks it to find out that's happened. 593 594 blob := []byte("hello world") 595 blobDesc := ocispec.Descriptor{ 596 MediaType: "test", 597 Digest: digest.FromBytes(blob), 598 Size: int64(len(blob)), 599 } 600 var sequence string 601 uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c" 602 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 603 switch { 604 case r.Method == http.MethodPost && r.URL.Path == "/v2/test2/blobs/uploads/": 605 w.Header().Set("Location", "/v2/test2/blobs/uploads/"+uuid) 606 w.WriteHeader(http.StatusAccepted) 607 sequence += "post " 608 return 609 default: 610 w.WriteHeader(http.StatusForbidden) 611 } 612 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 613 })) 614 defer ts.Close() 615 uri, err := url.Parse(ts.URL) 616 if err != nil { 617 t.Fatalf("invalid test http server: %v", err) 618 } 619 620 repo, err := NewRepository(uri.Host + "/test2") 621 if err != nil { 622 t.Fatalf("NewRepository() error = %v", err) 623 } 624 repo.PlainHTTP = true 625 ctx := context.Background() 626 627 testErr := errors.New("test error") 628 err = repo.Mount(ctx, blobDesc, "test", func() (io.ReadCloser, error) { 629 return nil, testErr 630 }) 631 if err == nil { 632 t.Fatalf("expected error but found no error") 633 } 634 if !errors.Is(err, testErr) { 635 t.Fatalf("expected getContent error to be wrapped") 636 } 637 if got, want := sequence, "post "; got != want { 638 t.Errorf("unexpected request sequence; got %q want %q", got, want) 639 } 640 } 641 642 func TestRepository_Exists(t *testing.T) { 643 blob := []byte("hello world") 644 blobDesc := ocispec.Descriptor{ 645 MediaType: "test", 646 Digest: digest.FromBytes(blob), 647 Size: int64(len(blob)), 648 } 649 index := []byte(`{"manifests":[]}`) 650 indexDesc := ocispec.Descriptor{ 651 MediaType: ocispec.MediaTypeImageIndex, 652 Digest: digest.FromBytes(index), 653 Size: int64(len(index)), 654 } 655 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 656 if r.Method != http.MethodHead { 657 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 658 w.WriteHeader(http.StatusMethodNotAllowed) 659 return 660 } 661 switch r.URL.Path { 662 case "/v2/test/blobs/" + blobDesc.Digest.String(): 663 w.Header().Set("Content-Type", "application/octet-stream") 664 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 665 w.Header().Set("Content-Length", strconv.Itoa(int(blobDesc.Size))) 666 case "/v2/test/manifests/" + indexDesc.Digest.String(): 667 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { 668 t.Errorf("manifest not convertable: %s", accept) 669 w.WriteHeader(http.StatusBadRequest) 670 return 671 } 672 w.Header().Set("Content-Type", indexDesc.MediaType) 673 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 674 w.Header().Set("Content-Length", strconv.Itoa(int(indexDesc.Size))) 675 default: 676 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 677 w.WriteHeader(http.StatusNotFound) 678 } 679 })) 680 defer ts.Close() 681 uri, err := url.Parse(ts.URL) 682 if err != nil { 683 t.Fatalf("invalid test http server: %v", err) 684 } 685 686 repo, err := NewRepository(uri.Host + "/test") 687 if err != nil { 688 t.Fatalf("NewRepository() error = %v", err) 689 } 690 repo.PlainHTTP = true 691 ctx := context.Background() 692 693 exists, err := repo.Exists(ctx, blobDesc) 694 if err != nil { 695 t.Fatalf("Repository.Exists() error = %v", err) 696 } 697 if !exists { 698 t.Errorf("Repository.Exists() = %v, want %v", exists, true) 699 } 700 701 exists, err = repo.Exists(ctx, indexDesc) 702 if err != nil { 703 t.Fatalf("Repository.Exists() error = %v", err) 704 } 705 if !exists { 706 t.Errorf("Repository.Exists() = %v, want %v", exists, true) 707 } 708 } 709 710 func TestRepository_Delete(t *testing.T) { 711 blob := []byte("hello world") 712 blobDesc := ocispec.Descriptor{ 713 MediaType: "test", 714 Digest: digest.FromBytes(blob), 715 Size: int64(len(blob)), 716 } 717 index := []byte(`{"manifests":[]}`) 718 indexDesc := ocispec.Descriptor{ 719 MediaType: ocispec.MediaTypeImageIndex, 720 Digest: digest.FromBytes(index), 721 Size: int64(len(index)), 722 } 723 724 var blobDeleted bool 725 var indexDeleted bool 726 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 727 switch { 728 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/blobs/"+blobDesc.Digest.String(): 729 blobDeleted = true 730 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 731 w.WriteHeader(http.StatusAccepted) 732 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 733 indexDeleted = true 734 // no "Docker-Content-Digest" header for manifest deletion 735 w.WriteHeader(http.StatusAccepted) 736 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 737 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { 738 t.Errorf("manifest not convertable: %s", accept) 739 w.WriteHeader(http.StatusBadRequest) 740 return 741 } 742 w.Header().Set("Content-Type", indexDesc.MediaType) 743 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 744 if _, err := w.Write(index); err != nil { 745 t.Errorf("failed to write %q: %v", r.URL, err) 746 } 747 default: 748 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 749 w.WriteHeader(http.StatusNotFound) 750 } 751 })) 752 defer ts.Close() 753 uri, err := url.Parse(ts.URL) 754 if err != nil { 755 t.Fatalf("invalid test http server: %v", err) 756 } 757 758 repo, err := NewRepository(uri.Host + "/test") 759 if err != nil { 760 t.Fatalf("NewRepository() error = %v", err) 761 } 762 repo.PlainHTTP = true 763 ctx := context.Background() 764 765 err = repo.Delete(ctx, blobDesc) 766 if err != nil { 767 t.Fatalf("Repository.Delete() error = %v", err) 768 } 769 if !blobDeleted { 770 t.Errorf("Repository.Delete() = %v, want %v", blobDeleted, true) 771 } 772 773 err = repo.Delete(ctx, indexDesc) 774 if err != nil { 775 t.Fatalf("Repository.Delete() error = %v", err) 776 } 777 if !indexDeleted { 778 t.Errorf("Repository.Delete() = %v, want %v", indexDeleted, true) 779 } 780 } 781 782 func TestRepository_Resolve(t *testing.T) { 783 blob := []byte("hello world") 784 blobDesc := ocispec.Descriptor{ 785 MediaType: "test", 786 Digest: digest.FromBytes(blob), 787 Size: int64(len(blob)), 788 } 789 index := []byte(`{"manifests":[]}`) 790 indexDesc := ocispec.Descriptor{ 791 MediaType: ocispec.MediaTypeImageIndex, 792 Digest: digest.FromBytes(index), 793 Size: int64(len(index)), 794 } 795 ref := "foobar" 796 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 797 if r.Method != http.MethodHead { 798 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 799 w.WriteHeader(http.StatusMethodNotAllowed) 800 return 801 } 802 switch r.URL.Path { 803 case "/v2/test/manifests/" + blobDesc.Digest.String(): 804 w.WriteHeader(http.StatusNotFound) 805 case "/v2/test/manifests/" + indexDesc.Digest.String(), 806 "/v2/test/manifests/" + ref: 807 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { 808 t.Errorf("manifest not convertable: %s", accept) 809 w.WriteHeader(http.StatusBadRequest) 810 return 811 } 812 w.Header().Set("Content-Type", indexDesc.MediaType) 813 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 814 w.Header().Set("Content-Length", strconv.Itoa(int(indexDesc.Size))) 815 default: 816 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 817 w.WriteHeader(http.StatusNotFound) 818 } 819 })) 820 defer ts.Close() 821 uri, err := url.Parse(ts.URL) 822 if err != nil { 823 t.Fatalf("invalid test http server: %v", err) 824 } 825 826 repoName := uri.Host + "/test" 827 repo, err := NewRepository(repoName) 828 if err != nil { 829 t.Fatalf("NewRepository() error = %v", err) 830 } 831 repo.PlainHTTP = true 832 ctx := context.Background() 833 834 _, err = repo.Resolve(ctx, blobDesc.Digest.String()) 835 if !errors.Is(err, errdef.ErrNotFound) { 836 t.Errorf("Repository.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound) 837 } 838 839 got, err := repo.Resolve(ctx, indexDesc.Digest.String()) 840 if err != nil { 841 t.Fatalf("Repository.Resolve() error = %v", err) 842 } 843 if !reflect.DeepEqual(got, indexDesc) { 844 t.Errorf("Repository.Resolve() = %v, want %v", got, indexDesc) 845 } 846 847 got, err = repo.Resolve(ctx, ref) 848 if err != nil { 849 t.Fatalf("Repository.Resolve() error = %v", err) 850 } 851 if !reflect.DeepEqual(got, indexDesc) { 852 t.Errorf("Repository.Resolve() = %v, want %v", got, indexDesc) 853 } 854 855 tagDigestRef := "whatever" + "@" + indexDesc.Digest.String() 856 got, err = repo.Resolve(ctx, tagDigestRef) 857 if err != nil { 858 t.Fatalf("Repository.Resolve() error = %v", err) 859 } 860 if !reflect.DeepEqual(got, indexDesc) { 861 t.Errorf("Repository.Resolve() = %v, want %v", got, indexDesc) 862 } 863 864 fqdnRef := repoName + ":" + tagDigestRef 865 got, err = repo.Resolve(ctx, fqdnRef) 866 if err != nil { 867 t.Fatalf("Repository.Resolve() error = %v", err) 868 } 869 if !reflect.DeepEqual(got, indexDesc) { 870 t.Errorf("Repository.Resolve() = %v, want %v", got, indexDesc) 871 } 872 } 873 874 func TestRepository_Tag(t *testing.T) { 875 blob := []byte("hello world") 876 blobDesc := ocispec.Descriptor{ 877 MediaType: "test", 878 Digest: digest.FromBytes(blob), 879 Size: int64(len(blob)), 880 } 881 index := []byte(`{"manifests":[]}`) 882 indexDesc := ocispec.Descriptor{ 883 MediaType: ocispec.MediaTypeImageIndex, 884 Digest: digest.FromBytes(index), 885 Size: int64(len(index)), 886 } 887 var gotIndex []byte 888 ref := "foobar" 889 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 890 switch { 891 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+blobDesc.Digest.String(): 892 w.WriteHeader(http.StatusNotFound) 893 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 894 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { 895 t.Errorf("manifest not convertable: %s", accept) 896 w.WriteHeader(http.StatusBadRequest) 897 return 898 } 899 w.Header().Set("Content-Type", indexDesc.MediaType) 900 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 901 if _, err := w.Write(index); err != nil { 902 t.Errorf("failed to write %q: %v", r.URL, err) 903 } 904 case r.Method == http.MethodPut && 905 r.URL.Path == "/v2/test/manifests/"+ref || r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 906 if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { 907 w.WriteHeader(http.StatusBadRequest) 908 break 909 } 910 buf := bytes.NewBuffer(nil) 911 if _, err := buf.ReadFrom(r.Body); err != nil { 912 t.Errorf("fail to read: %v", err) 913 } 914 gotIndex = buf.Bytes() 915 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 916 w.WriteHeader(http.StatusCreated) 917 return 918 default: 919 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 920 w.WriteHeader(http.StatusForbidden) 921 } 922 })) 923 defer ts.Close() 924 uri, err := url.Parse(ts.URL) 925 if err != nil { 926 t.Fatalf("invalid test http server: %v", err) 927 } 928 929 repo, err := NewRepository(uri.Host + "/test") 930 if err != nil { 931 t.Fatalf("NewRepository() error = %v", err) 932 } 933 repo.PlainHTTP = true 934 ctx := context.Background() 935 936 err = repo.Tag(ctx, blobDesc, ref) 937 if err == nil { 938 t.Errorf("Repository.Tag() error = %v, wantErr %v", err, true) 939 } 940 941 err = repo.Tag(ctx, indexDesc, ref) 942 if err != nil { 943 t.Fatalf("Repository.Tag() error = %v", err) 944 } 945 if !bytes.Equal(gotIndex, index) { 946 t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index) 947 } 948 949 gotIndex = nil 950 err = repo.Tag(ctx, indexDesc, indexDesc.Digest.String()) 951 if err != nil { 952 t.Fatalf("Repository.Tag() error = %v", err) 953 } 954 if !bytes.Equal(gotIndex, index) { 955 t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index) 956 } 957 } 958 959 func TestRepository_PushReference(t *testing.T) { 960 index := []byte(`{"manifests":[]}`) 961 indexDesc := ocispec.Descriptor{ 962 MediaType: ocispec.MediaTypeImageIndex, 963 Digest: digest.FromBytes(index), 964 Size: int64(len(index)), 965 } 966 var gotIndex []byte 967 ref := "foobar" 968 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 969 switch { 970 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+ref: 971 if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { 972 w.WriteHeader(http.StatusBadRequest) 973 break 974 } 975 buf := bytes.NewBuffer(nil) 976 if _, err := buf.ReadFrom(r.Body); err != nil { 977 t.Errorf("fail to read: %v", err) 978 } 979 gotIndex = buf.Bytes() 980 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 981 w.WriteHeader(http.StatusCreated) 982 return 983 default: 984 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 985 w.WriteHeader(http.StatusForbidden) 986 } 987 })) 988 defer ts.Close() 989 uri, err := url.Parse(ts.URL) 990 if err != nil { 991 t.Fatalf("invalid test http server: %v", err) 992 } 993 994 repo, err := NewRepository(uri.Host + "/test") 995 if err != nil { 996 t.Fatalf("NewRepository() error = %v", err) 997 } 998 repo.PlainHTTP = true 999 ctx := context.Background() 1000 err = repo.PushReference(ctx, indexDesc, bytes.NewReader(index), ref) 1001 if err != nil { 1002 t.Fatalf("Repository.PushReference() error = %v", err) 1003 } 1004 if !bytes.Equal(gotIndex, index) { 1005 t.Errorf("Repository.PushReference() = %v, want %v", gotIndex, index) 1006 } 1007 } 1008 1009 func TestRepository_FetchReference(t *testing.T) { 1010 blob := []byte("hello world") 1011 blobDesc := ocispec.Descriptor{ 1012 MediaType: "test", 1013 Digest: digest.FromBytes(blob), 1014 Size: int64(len(blob)), 1015 } 1016 index := []byte(`{"manifests":[]}`) 1017 indexDesc := ocispec.Descriptor{ 1018 MediaType: ocispec.MediaTypeImageIndex, 1019 Digest: digest.FromBytes(index), 1020 Size: int64(len(index)), 1021 } 1022 ref := "foobar" 1023 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1024 if r.Method != http.MethodGet { 1025 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 1026 w.WriteHeader(http.StatusMethodNotAllowed) 1027 return 1028 } 1029 switch r.URL.Path { 1030 case "/v2/test/manifests/" + blobDesc.Digest.String(): 1031 w.WriteHeader(http.StatusNotFound) 1032 case "/v2/test/manifests/" + indexDesc.Digest.String(), 1033 "/v2/test/manifests/" + ref: 1034 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { 1035 t.Errorf("manifest not convertable: %s", accept) 1036 w.WriteHeader(http.StatusBadRequest) 1037 return 1038 } 1039 w.Header().Set("Content-Type", indexDesc.MediaType) 1040 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 1041 if _, err := w.Write(index); err != nil { 1042 t.Errorf("failed to write %q: %v", r.URL, err) 1043 } 1044 default: 1045 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 1046 w.WriteHeader(http.StatusNotFound) 1047 } 1048 })) 1049 defer ts.Close() 1050 uri, err := url.Parse(ts.URL) 1051 if err != nil { 1052 t.Fatalf("invalid test http server: %v", err) 1053 } 1054 1055 repoName := uri.Host + "/test" 1056 repo, err := NewRepository(repoName) 1057 if err != nil { 1058 t.Fatalf("NewRepository() error = %v", err) 1059 } 1060 repo.PlainHTTP = true 1061 ctx := context.Background() 1062 1063 // test with blob digest 1064 _, _, err = repo.FetchReference(ctx, blobDesc.Digest.String()) 1065 if !errors.Is(err, errdef.ErrNotFound) { 1066 t.Errorf("Repository.FetchReference() error = %v, wantErr %v", err, errdef.ErrNotFound) 1067 } 1068 1069 // test with manifest digest 1070 gotDesc, rc, err := repo.FetchReference(ctx, indexDesc.Digest.String()) 1071 if err != nil { 1072 t.Fatalf("Repository.FetchReference() error = %v", err) 1073 } 1074 if !reflect.DeepEqual(gotDesc, indexDesc) { 1075 t.Errorf("Repository.FetchReference() = %v, want %v", gotDesc, indexDesc) 1076 } 1077 buf := bytes.NewBuffer(nil) 1078 if _, err := buf.ReadFrom(rc); err != nil { 1079 t.Errorf("fail to read: %v", err) 1080 } 1081 if err := rc.Close(); err != nil { 1082 t.Errorf("fail to close: %v", err) 1083 } 1084 if got := buf.Bytes(); !bytes.Equal(got, index) { 1085 t.Errorf("Repository.FetchReference() = %v, want %v", got, index) 1086 } 1087 1088 // test with manifest tag 1089 gotDesc, rc, err = repo.FetchReference(ctx, ref) 1090 if err != nil { 1091 t.Fatalf("Repository.FetchReference() error = %v", err) 1092 } 1093 if !reflect.DeepEqual(gotDesc, indexDesc) { 1094 t.Errorf("Repository.FetchReference() = %v, want %v", gotDesc, indexDesc) 1095 } 1096 buf.Reset() 1097 if _, err := buf.ReadFrom(rc); err != nil { 1098 t.Errorf("fail to read: %v", err) 1099 } 1100 if err := rc.Close(); err != nil { 1101 t.Errorf("fail to close: %v", err) 1102 } 1103 if got := buf.Bytes(); !bytes.Equal(got, index) { 1104 t.Errorf("Repository.FetchReference() = %v, want %v", got, index) 1105 } 1106 1107 // test with manifest tag@digest 1108 tagDigestRef := "whatever" + "@" + indexDesc.Digest.String() 1109 gotDesc, rc, err = repo.FetchReference(ctx, tagDigestRef) 1110 if err != nil { 1111 t.Fatalf("Repository.FetchReference() error = %v", err) 1112 } 1113 if !reflect.DeepEqual(gotDesc, indexDesc) { 1114 t.Errorf("Repository.FetchReference() = %v, want %v", gotDesc, indexDesc) 1115 } 1116 buf.Reset() 1117 if _, err := buf.ReadFrom(rc); err != nil { 1118 t.Errorf("fail to read: %v", err) 1119 } 1120 if err := rc.Close(); err != nil { 1121 t.Errorf("fail to close: %v", err) 1122 } 1123 if got := buf.Bytes(); !bytes.Equal(got, index) { 1124 t.Errorf("Repository.FetchReference() = %v, want %v", got, index) 1125 } 1126 1127 // test with manifest FQDN 1128 fqdnRef := repoName + ":" + tagDigestRef 1129 gotDesc, rc, err = repo.FetchReference(ctx, fqdnRef) 1130 if err != nil { 1131 t.Fatalf("Repository.FetchReference() error = %v", err) 1132 } 1133 if !reflect.DeepEqual(gotDesc, indexDesc) { 1134 t.Errorf("Repository.FetchReference() = %v, want %v", gotDesc, indexDesc) 1135 } 1136 buf.Reset() 1137 if _, err := buf.ReadFrom(rc); err != nil { 1138 t.Errorf("fail to read: %v", err) 1139 } 1140 if err := rc.Close(); err != nil { 1141 t.Errorf("fail to close: %v", err) 1142 } 1143 if got := buf.Bytes(); !bytes.Equal(got, index) { 1144 t.Errorf("Repository.FetchReference() = %v, want %v", got, index) 1145 } 1146 } 1147 1148 func TestRepository_Tags(t *testing.T) { 1149 tagSet := [][]string{ 1150 {"the", "quick", "brown", "fox"}, 1151 {"jumps", "over", "the", "lazy"}, 1152 {"dog"}, 1153 } 1154 var ts *httptest.Server 1155 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1156 if r.Method != http.MethodGet || r.URL.Path != "/v2/test/tags/list" { 1157 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 1158 w.WriteHeader(http.StatusNotFound) 1159 return 1160 } 1161 q := r.URL.Query() 1162 n, err := strconv.Atoi(q.Get("n")) 1163 if err != nil || n != 4 { 1164 t.Errorf("bad page size: %s", q.Get("n")) 1165 w.WriteHeader(http.StatusBadRequest) 1166 return 1167 } 1168 var tags []string 1169 switch q.Get("test") { 1170 case "foo": 1171 tags = tagSet[1] 1172 w.Header().Set("Link", fmt.Sprintf(`<%s/v2/test/tags/list?n=4&test=bar>; rel="next"`, ts.URL)) 1173 case "bar": 1174 tags = tagSet[2] 1175 default: 1176 tags = tagSet[0] 1177 w.Header().Set("Link", `</v2/test/tags/list?n=4&test=foo>; rel="next"`) 1178 } 1179 result := struct { 1180 Tags []string `json:"tags"` 1181 }{ 1182 Tags: tags, 1183 } 1184 if err := json.NewEncoder(w).Encode(result); err != nil { 1185 t.Errorf("failed to write response: %v", err) 1186 } 1187 })) 1188 defer ts.Close() 1189 uri, err := url.Parse(ts.URL) 1190 if err != nil { 1191 t.Fatalf("invalid test http server: %v", err) 1192 } 1193 1194 repo, err := NewRepository(uri.Host + "/test") 1195 if err != nil { 1196 t.Fatalf("NewRepository() error = %v", err) 1197 } 1198 repo.PlainHTTP = true 1199 repo.TagListPageSize = 4 1200 1201 ctx := context.Background() 1202 index := 0 1203 if err := repo.Tags(ctx, "", func(got []string) error { 1204 if index > 2 { 1205 t.Fatalf("out of index bound: %d", index) 1206 } 1207 tags := tagSet[index] 1208 index++ 1209 if !reflect.DeepEqual(got, tags) { 1210 t.Errorf("Repository.Tags() = %v, want %v", got, tags) 1211 } 1212 return nil 1213 }); err != nil { 1214 t.Errorf("Repository.Tags() error = %v", err) 1215 } 1216 } 1217 1218 func TestRepository_Predecessors(t *testing.T) { 1219 manifest := []byte(`{"layers":[]}`) 1220 manifestDesc := ocispec.Descriptor{ 1221 MediaType: ocispec.MediaTypeImageManifest, 1222 Digest: digest.FromBytes(manifest), 1223 Size: int64(len(manifest)), 1224 } 1225 referrerSet := [][]ocispec.Descriptor{ 1226 { 1227 { 1228 MediaType: spec.MediaTypeArtifactManifest, 1229 Size: 1, 1230 Digest: digest.FromString("1"), 1231 ArtifactType: "application/vnd.test", 1232 }, 1233 { 1234 MediaType: spec.MediaTypeArtifactManifest, 1235 Size: 2, 1236 Digest: digest.FromString("2"), 1237 ArtifactType: "application/vnd.test", 1238 }, 1239 }, 1240 { 1241 { 1242 MediaType: spec.MediaTypeArtifactManifest, 1243 Size: 3, 1244 Digest: digest.FromString("3"), 1245 ArtifactType: "application/vnd.test", 1246 }, 1247 { 1248 MediaType: spec.MediaTypeArtifactManifest, 1249 Size: 4, 1250 Digest: digest.FromString("4"), 1251 ArtifactType: "application/vnd.test", 1252 }, 1253 }, 1254 { 1255 { 1256 MediaType: spec.MediaTypeArtifactManifest, 1257 Size: 5, 1258 Digest: digest.FromString("5"), 1259 ArtifactType: "application/vnd.test", 1260 }, 1261 }, 1262 } 1263 var ts *httptest.Server 1264 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1265 path := "/v2/test/referrers/" + manifestDesc.Digest.String() 1266 if r.Method != http.MethodGet || r.URL.Path != path { 1267 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 1268 w.WriteHeader(http.StatusNotFound) 1269 return 1270 } 1271 q := r.URL.Query() 1272 n, err := strconv.Atoi(q.Get("n")) 1273 if err != nil || n != 2 { 1274 t.Errorf("bad page size: %s", q.Get("n")) 1275 w.WriteHeader(http.StatusBadRequest) 1276 return 1277 } 1278 var referrers []ocispec.Descriptor 1279 switch q.Get("test") { 1280 case "foo": 1281 referrers = referrerSet[1] 1282 w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path)) 1283 case "bar": 1284 referrers = referrerSet[2] 1285 default: 1286 referrers = referrerSet[0] 1287 w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path)) 1288 } 1289 result := ocispec.Index{ 1290 Versioned: specs.Versioned{ 1291 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 1292 }, 1293 MediaType: ocispec.MediaTypeImageIndex, 1294 Manifests: referrers, 1295 } 1296 w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex) 1297 if err := json.NewEncoder(w).Encode(result); err != nil { 1298 t.Errorf("failed to write response: %v", err) 1299 } 1300 })) 1301 defer ts.Close() 1302 uri, err := url.Parse(ts.URL) 1303 if err != nil { 1304 t.Fatalf("invalid test http server: %v", err) 1305 } 1306 1307 repo, err := NewRepository(uri.Host + "/test") 1308 if err != nil { 1309 t.Fatalf("NewRepository() error = %v", err) 1310 } 1311 repo.PlainHTTP = true 1312 repo.ReferrerListPageSize = 2 1313 1314 ctx := context.Background() 1315 got, err := repo.Predecessors(ctx, manifestDesc) 1316 if err != nil { 1317 t.Fatalf("Repository.Predecessors() error = %v", err) 1318 } 1319 var want []ocispec.Descriptor 1320 for _, referrers := range referrerSet { 1321 want = append(want, referrers...) 1322 } 1323 if !reflect.DeepEqual(got, want) { 1324 t.Errorf("Repository.Predecessors() = %v, want %v", got, want) 1325 } 1326 } 1327 1328 func TestRepository_Referrers(t *testing.T) { 1329 manifest := []byte(`{"layers":[]}`) 1330 manifestDesc := ocispec.Descriptor{ 1331 MediaType: ocispec.MediaTypeImageManifest, 1332 Digest: digest.FromBytes(manifest), 1333 Size: int64(len(manifest)), 1334 } 1335 referrerSet := [][]ocispec.Descriptor{ 1336 { 1337 { 1338 MediaType: spec.MediaTypeArtifactManifest, 1339 Size: 1, 1340 Digest: digest.FromString("1"), 1341 ArtifactType: "application/vnd.test", 1342 }, 1343 { 1344 MediaType: spec.MediaTypeArtifactManifest, 1345 Size: 2, 1346 Digest: digest.FromString("2"), 1347 ArtifactType: "application/vnd.test", 1348 }, 1349 }, 1350 { 1351 { 1352 MediaType: spec.MediaTypeArtifactManifest, 1353 Size: 3, 1354 Digest: digest.FromString("3"), 1355 ArtifactType: "application/vnd.test", 1356 }, 1357 { 1358 MediaType: spec.MediaTypeArtifactManifest, 1359 Size: 4, 1360 Digest: digest.FromString("4"), 1361 ArtifactType: "application/vnd.test", 1362 }, 1363 }, 1364 { 1365 { 1366 MediaType: spec.MediaTypeArtifactManifest, 1367 Size: 5, 1368 Digest: digest.FromString("5"), 1369 ArtifactType: "application/vnd.test", 1370 }, 1371 }, 1372 } 1373 var ts *httptest.Server 1374 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1375 path := "/v2/test/referrers/" + manifestDesc.Digest.String() 1376 if r.Method != http.MethodGet || r.URL.Path != path { 1377 referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) 1378 if r.URL.Path != "/v2/test/manifests/"+referrersTag { 1379 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1380 } 1381 w.WriteHeader(http.StatusNotFound) 1382 return 1383 } 1384 q := r.URL.Query() 1385 n, err := strconv.Atoi(q.Get("n")) 1386 if err != nil || n != 2 { 1387 t.Errorf("bad page size: %s", q.Get("n")) 1388 w.WriteHeader(http.StatusBadRequest) 1389 return 1390 } 1391 var referrers []ocispec.Descriptor 1392 switch q.Get("test") { 1393 case "foo": 1394 referrers = referrerSet[1] 1395 w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path)) 1396 case "bar": 1397 referrers = referrerSet[2] 1398 default: 1399 referrers = referrerSet[0] 1400 w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path)) 1401 } 1402 result := ocispec.Index{ 1403 Versioned: specs.Versioned{ 1404 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 1405 }, 1406 MediaType: ocispec.MediaTypeImageIndex, 1407 Manifests: referrers, 1408 } 1409 w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex) 1410 if err := json.NewEncoder(w).Encode(result); err != nil { 1411 t.Errorf("failed to write response: %v", err) 1412 } 1413 })) 1414 defer ts.Close() 1415 uri, err := url.Parse(ts.URL) 1416 if err != nil { 1417 t.Fatalf("invalid test http server: %v", err) 1418 } 1419 1420 ctx := context.Background() 1421 1422 // test auto detect 1423 // remote server supports Referrers, should be no error 1424 repo, err := NewRepository(uri.Host + "/test") 1425 if err != nil { 1426 t.Fatalf("NewRepository() error = %v", err) 1427 } 1428 repo.PlainHTTP = true 1429 repo.ReferrerListPageSize = 2 1430 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1431 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1432 } 1433 index := 0 1434 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1435 if index >= len(referrerSet) { 1436 t.Fatalf("out of index bound: %d", index) 1437 } 1438 referrers := referrerSet[index] 1439 index++ 1440 if !reflect.DeepEqual(got, referrers) { 1441 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 1442 } 1443 return nil 1444 }); err != nil { 1445 t.Errorf("Repository.Referrers() error = %v", err) 1446 } 1447 if state := repo.loadReferrersState(); state != referrersStateSupported { 1448 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1449 } 1450 1451 // test force attempt Referrers 1452 // remote server supports Referrers, should be no error 1453 repo, err = NewRepository(uri.Host + "/test") 1454 if err != nil { 1455 t.Fatalf("NewRepository() error = %v", err) 1456 } 1457 repo.PlainHTTP = true 1458 repo.ReferrerListPageSize = 2 1459 repo.SetReferrersCapability(true) 1460 if state := repo.loadReferrersState(); state != referrersStateSupported { 1461 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1462 } 1463 index = 0 1464 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1465 if index >= len(referrerSet) { 1466 t.Fatalf("out of index bound: %d", index) 1467 } 1468 referrers := referrerSet[index] 1469 index++ 1470 if !reflect.DeepEqual(got, referrers) { 1471 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 1472 } 1473 return nil 1474 }); err != nil { 1475 t.Errorf("Repository.Referrers() error = %v", err) 1476 } 1477 if state := repo.loadReferrersState(); state != referrersStateSupported { 1478 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1479 } 1480 1481 // test force attempt tag schema 1482 // request tag schema but got 404, should be no error 1483 repo, err = NewRepository(uri.Host + "/test") 1484 if err != nil { 1485 t.Fatalf("NewRepository() error = %v", err) 1486 } 1487 repo.PlainHTTP = true 1488 repo.ReferrerListPageSize = 2 1489 repo.SetReferrersCapability(false) 1490 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1491 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 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", err) 1497 } 1498 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1499 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1500 } 1501 } 1502 1503 func TestRepository_Referrers_TagSchemaFallback(t *testing.T) { 1504 manifest := []byte(`{"layers":[]}`) 1505 manifestDesc := ocispec.Descriptor{ 1506 MediaType: ocispec.MediaTypeImageManifest, 1507 Digest: digest.FromBytes(manifest), 1508 Size: int64(len(manifest)), 1509 } 1510 1511 referrers := []ocispec.Descriptor{ 1512 { 1513 MediaType: spec.MediaTypeArtifactManifest, 1514 Size: 1, 1515 Digest: digest.FromString("1"), 1516 ArtifactType: "application/vnd.test", 1517 }, 1518 { 1519 MediaType: spec.MediaTypeArtifactManifest, 1520 Size: 2, 1521 Digest: digest.FromString("2"), 1522 ArtifactType: "application/vnd.test", 1523 }, 1524 { 1525 MediaType: spec.MediaTypeArtifactManifest, 1526 Size: 3, 1527 Digest: digest.FromString("3"), 1528 ArtifactType: "application/vnd.test", 1529 }, 1530 { 1531 MediaType: spec.MediaTypeArtifactManifest, 1532 Size: 4, 1533 Digest: digest.FromString("4"), 1534 ArtifactType: "application/vnd.test", 1535 }, 1536 { 1537 MediaType: spec.MediaTypeArtifactManifest, 1538 Size: 5, 1539 Digest: digest.FromString("5"), 1540 ArtifactType: "application/vnd.test", 1541 }, 1542 } 1543 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1544 referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) 1545 path := "/v2/test/manifests/" + referrersTag 1546 if r.Method != http.MethodGet || r.URL.Path != path { 1547 if r.URL.Path != "/v2/test/referrers/"+manifestDesc.Digest.String() { 1548 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1549 } 1550 w.WriteHeader(http.StatusNotFound) 1551 return 1552 } 1553 1554 result := ocispec.Index{ 1555 Versioned: specs.Versioned{ 1556 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 1557 }, 1558 MediaType: ocispec.MediaTypeImageIndex, 1559 Manifests: referrers, 1560 } 1561 if err := json.NewEncoder(w).Encode(result); err != nil { 1562 t.Errorf("failed to write response: %v", err) 1563 } 1564 })) 1565 defer ts.Close() 1566 uri, err := url.Parse(ts.URL) 1567 if err != nil { 1568 t.Fatalf("invalid test http server: %v", err) 1569 } 1570 ctx := context.Background() 1571 1572 // test auto detect 1573 // remote server does not support Referrers, should fallback to tag schema 1574 repo, err := NewRepository(uri.Host + "/test") 1575 if err != nil { 1576 t.Fatalf("NewRepository() error = %v", err) 1577 } 1578 repo.PlainHTTP = true 1579 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1580 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1581 } 1582 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1583 if !reflect.DeepEqual(got, referrers) { 1584 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 1585 } 1586 return nil 1587 }); err != nil { 1588 t.Errorf("Repository.Referrers() error = %v", err) 1589 } 1590 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1591 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1592 } 1593 1594 // test force attempt Referrers 1595 // remote server does not support Referrers, should return error 1596 repo, err = NewRepository(uri.Host + "/test") 1597 if err != nil { 1598 t.Fatalf("NewRepository() error = %v", err) 1599 } 1600 repo.PlainHTTP = true 1601 repo.SetReferrersCapability(true) 1602 if state := repo.loadReferrersState(); state != referrersStateSupported { 1603 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1604 } 1605 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1606 return nil 1607 }); err == nil { 1608 t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, true) 1609 } 1610 if state := repo.loadReferrersState(); state != referrersStateSupported { 1611 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1612 } 1613 1614 // test force attempt tag schema 1615 // should request tag schema 1616 repo, err = NewRepository(uri.Host + "/test") 1617 if err != nil { 1618 t.Fatalf("NewRepository() error = %v", err) 1619 } 1620 repo.PlainHTTP = true 1621 repo.SetReferrersCapability(false) 1622 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1623 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1624 } 1625 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1626 if !reflect.DeepEqual(got, referrers) { 1627 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 1628 } 1629 return nil 1630 }); err != nil { 1631 t.Errorf("Repository.Referrers() error = %v", err) 1632 } 1633 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1634 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1635 } 1636 } 1637 1638 func TestRepository_Referrers_TagSchemaFallback_NotFound(t *testing.T) { 1639 manifest := []byte(`{"layers":[]}`) 1640 manifestDesc := ocispec.Descriptor{ 1641 MediaType: ocispec.MediaTypeImageManifest, 1642 Digest: digest.FromBytes(manifest), 1643 Size: int64(len(manifest)), 1644 } 1645 1646 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1647 referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String() 1648 referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) 1649 tagSchemaUrl := "/v2/test/manifests/" + referrersTag 1650 if r.Method == http.MethodGet || 1651 r.URL.Path == referrersUrl || 1652 r.URL.Path == tagSchemaUrl { 1653 w.WriteHeader(http.StatusNotFound) 1654 return 1655 } 1656 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1657 w.WriteHeader(http.StatusNotFound) 1658 })) 1659 defer ts.Close() 1660 uri, err := url.Parse(ts.URL) 1661 if err != nil { 1662 t.Fatalf("invalid test http server: %v", err) 1663 } 1664 ctx := context.Background() 1665 1666 // test auto detect 1667 // tag schema referrers not found, should be no error 1668 repo, err := NewRepository(uri.Host + "/test") 1669 if err != nil { 1670 t.Fatalf("NewRepository() error = %v", err) 1671 } 1672 repo.PlainHTTP = true 1673 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1674 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1675 } 1676 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1677 return nil 1678 }); err != nil { 1679 t.Errorf("Repository.Referrers() error = %v", err) 1680 } 1681 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1682 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1683 } 1684 1685 // test force attempt tag schema 1686 // tag schema referrers not found, should be no error 1687 repo, err = NewRepository(uri.Host + "/test") 1688 if err != nil { 1689 t.Fatalf("NewRepository() error = %v", err) 1690 } 1691 repo.PlainHTTP = true 1692 repo.SetReferrersCapability(false) 1693 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1694 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1695 } 1696 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1697 return nil 1698 }); err != nil { 1699 t.Errorf("Repository.Referrers() error = %v", err) 1700 } 1701 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1702 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1703 } 1704 } 1705 1706 func TestRepository_Referrers_TagSchemaFallback_ContentType(t *testing.T) { 1707 manifest := []byte(`{"layers":[]}`) 1708 manifestDesc := ocispec.Descriptor{ 1709 MediaType: ocispec.MediaTypeImageManifest, 1710 Digest: digest.FromBytes(manifest), 1711 Size: int64(len(manifest)), 1712 } 1713 1714 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1715 referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String() 1716 referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) 1717 tagSchemaUrl := "/v2/test/manifests/" + referrersTag 1718 if r.URL.Path == referrersUrl { 1719 w.Header().Set("Content-Type", "application/json") // not an OCI image index 1720 w.WriteHeader(http.StatusOK) 1721 return 1722 } 1723 if r.Method == http.MethodGet || 1724 r.URL.Path == tagSchemaUrl { 1725 w.WriteHeader(http.StatusNotFound) 1726 return 1727 } 1728 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1729 w.WriteHeader(http.StatusNotFound) 1730 })) 1731 defer ts.Close() 1732 uri, err := url.Parse(ts.URL) 1733 if err != nil { 1734 t.Fatalf("invalid test http server: %v", err) 1735 } 1736 ctx := context.Background() 1737 1738 // test auto detect 1739 // tag schema referrers not found, should be no error 1740 repo, err := NewRepository(uri.Host + "/test") 1741 if err != nil { 1742 t.Fatalf("NewRepository() error = %v", err) 1743 } 1744 repo.PlainHTTP = true 1745 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1746 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1747 } 1748 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1749 return nil 1750 }); err != nil { 1751 t.Errorf("Repository.Referrers() error = %v", err) 1752 } 1753 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1754 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1755 } 1756 } 1757 1758 func TestRepository_Referrers_BadRequest(t *testing.T) { 1759 manifest := []byte(`{"layers":[]}`) 1760 manifestDesc := ocispec.Descriptor{ 1761 MediaType: ocispec.MediaTypeImageManifest, 1762 Digest: digest.FromBytes(manifest), 1763 Size: int64(len(manifest)), 1764 } 1765 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1766 referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String() 1767 referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) 1768 tagSchemaUrl := "/v2/test/manifests/" + referrersTag 1769 if r.Method == http.MethodGet || 1770 r.URL.Path == referrersUrl || 1771 r.URL.Path == tagSchemaUrl { 1772 w.WriteHeader(http.StatusBadRequest) 1773 return 1774 } 1775 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1776 w.WriteHeader(http.StatusNotFound) 1777 })) 1778 defer ts.Close() 1779 uri, err := url.Parse(ts.URL) 1780 if err != nil { 1781 t.Fatalf("invalid test http server: %v", err) 1782 } 1783 ctx := context.Background() 1784 1785 // test auto detect 1786 // Referrers returns error 1787 repo, err := NewRepository(uri.Host + "/test") 1788 if err != nil { 1789 t.Fatalf("NewRepository() error = %v", err) 1790 } 1791 repo.PlainHTTP = true 1792 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1793 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1794 } 1795 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1796 return nil 1797 }); err == nil { 1798 t.Errorf("Repository.Referrers() error = nil, wantErr %v", true) 1799 } 1800 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1801 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1802 } 1803 1804 // test force attempt Referrers 1805 // Referrers returns error 1806 repo, err = NewRepository(uri.Host + "/test") 1807 if err != nil { 1808 t.Fatalf("NewRepository() error = %v", err) 1809 } 1810 repo.PlainHTTP = true 1811 repo.SetReferrersCapability(true) 1812 if state := repo.loadReferrersState(); state != referrersStateSupported { 1813 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1814 } 1815 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1816 return nil 1817 }); err == nil { 1818 t.Errorf("Repository.Referrers() error = nil, wantErr %v", true) 1819 } 1820 if state := repo.loadReferrersState(); state != referrersStateSupported { 1821 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1822 } 1823 1824 // test force attempt tag schema 1825 // Referrers returns error 1826 repo, err = NewRepository(uri.Host + "/test") 1827 if err != nil { 1828 t.Fatalf("NewRepository() error = %v", err) 1829 } 1830 repo.PlainHTTP = true 1831 repo.SetReferrersCapability(false) 1832 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1833 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1834 } 1835 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1836 return nil 1837 }); err == nil { 1838 t.Errorf("Repository.Referrers() error = nil, wantErr %v", true) 1839 } 1840 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1841 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1842 } 1843 } 1844 1845 func TestRepository_Referrers_RepositoryNotFound(t *testing.T) { 1846 manifest := []byte(`{"layers":[]}`) 1847 manifestDesc := ocispec.Descriptor{ 1848 MediaType: ocispec.MediaTypeImageManifest, 1849 Digest: digest.FromBytes(manifest), 1850 Size: int64(len(manifest)), 1851 } 1852 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1853 referrersUrl := "/v2/test/referrers/" + manifestDesc.Digest.String() 1854 referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) 1855 tagSchemaUrl := "/v2/test/manifests/" + referrersTag 1856 if r.Method == http.MethodGet && 1857 (r.URL.Path == referrersUrl || r.URL.Path == tagSchemaUrl) { 1858 w.WriteHeader(http.StatusNotFound) 1859 w.Write([]byte(`{ "errors": [ { "code": "NAME_UNKNOWN", "message": "repository name not known to registry" } ] }`)) 1860 return 1861 } 1862 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1863 w.WriteHeader(http.StatusNotFound) 1864 })) 1865 defer ts.Close() 1866 uri, err := url.Parse(ts.URL) 1867 if err != nil { 1868 t.Fatalf("invalid test http server: %v", err) 1869 } 1870 ctx := context.Background() 1871 1872 // test auto detect 1873 // repository not found, should return error 1874 repo, err := NewRepository(uri.Host + "/test") 1875 if err != nil { 1876 t.Fatalf("NewRepository() error = %v", err) 1877 } 1878 repo.PlainHTTP = true 1879 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1880 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1881 } 1882 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1883 return nil 1884 }); err == nil { 1885 t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, true) 1886 } 1887 if state := repo.loadReferrersState(); state != referrersStateUnknown { 1888 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 1889 } 1890 1891 // test force attempt Referrers 1892 // repository not found, should return error 1893 repo, err = NewRepository(uri.Host + "/test") 1894 if err != nil { 1895 t.Fatalf("NewRepository() error = %v", err) 1896 } 1897 repo.PlainHTTP = true 1898 repo.SetReferrersCapability(true) 1899 if state := repo.loadReferrersState(); state != referrersStateSupported { 1900 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1901 } 1902 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1903 return nil 1904 }); err == nil { 1905 t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, true) 1906 } 1907 if state := repo.loadReferrersState(); state != referrersStateSupported { 1908 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 1909 } 1910 1911 // test force attempt tag schema 1912 // repository not found, but should not return error 1913 repo, err = NewRepository(uri.Host + "/test") 1914 if err != nil { 1915 t.Fatalf("NewRepository() error = %v", err) 1916 } 1917 repo.PlainHTTP = true 1918 repo.SetReferrersCapability(false) 1919 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1920 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1921 } 1922 if err := repo.Referrers(ctx, manifestDesc, "", func(got []ocispec.Descriptor) error { 1923 return nil 1924 }); err != nil { 1925 t.Errorf("Repository.Referrers() error = %v, wantErr %v", err, nil) 1926 } 1927 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 1928 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 1929 } 1930 } 1931 1932 func TestRepository_Referrers_ServerFiltering(t *testing.T) { 1933 manifest := []byte(`{"layers":[]}`) 1934 manifestDesc := ocispec.Descriptor{ 1935 MediaType: ocispec.MediaTypeImageManifest, 1936 Digest: digest.FromBytes(manifest), 1937 Size: int64(len(manifest)), 1938 } 1939 referrerSet := [][]ocispec.Descriptor{ 1940 { 1941 { 1942 MediaType: spec.MediaTypeArtifactManifest, 1943 Size: 1, 1944 Digest: digest.FromString("1"), 1945 ArtifactType: "application/vnd.test", 1946 }, 1947 { 1948 MediaType: spec.MediaTypeArtifactManifest, 1949 Size: 2, 1950 Digest: digest.FromString("2"), 1951 ArtifactType: "application/vnd.test", 1952 }, 1953 }, 1954 { 1955 { 1956 MediaType: spec.MediaTypeArtifactManifest, 1957 Size: 3, 1958 Digest: digest.FromString("3"), 1959 ArtifactType: "application/vnd.test", 1960 }, 1961 { 1962 MediaType: spec.MediaTypeArtifactManifest, 1963 Size: 4, 1964 Digest: digest.FromString("4"), 1965 ArtifactType: "application/vnd.test", 1966 }, 1967 }, 1968 { 1969 { 1970 MediaType: spec.MediaTypeArtifactManifest, 1971 Size: 5, 1972 Digest: digest.FromString("5"), 1973 ArtifactType: "application/vnd.test", 1974 }, 1975 }, 1976 } 1977 1978 // Test with filter annotations only 1979 var ts *httptest.Server 1980 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1981 path := "/v2/test/referrers/" + manifestDesc.Digest.String() 1982 queryParams, err := url.ParseQuery(r.URL.RawQuery) 1983 if err != nil { 1984 t.Fatal("failed to parse url query") 1985 } 1986 if r.Method != http.MethodGet || 1987 r.URL.Path != path || 1988 reflect.DeepEqual(queryParams["artifactType"], []string{"application%2Fvnd.test"}) { 1989 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 1990 w.WriteHeader(http.StatusNotFound) 1991 return 1992 } 1993 q := r.URL.Query() 1994 n, err := strconv.Atoi(q.Get("n")) 1995 if err != nil || n != 2 { 1996 t.Errorf("bad page size: %s", q.Get("n")) 1997 w.WriteHeader(http.StatusBadRequest) 1998 return 1999 } 2000 var referrers []ocispec.Descriptor 2001 switch q.Get("test") { 2002 case "foo": 2003 referrers = referrerSet[1] 2004 w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path)) 2005 case "bar": 2006 referrers = referrerSet[2] 2007 default: 2008 referrers = referrerSet[0] 2009 w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path)) 2010 } 2011 result := ocispec.Index{ 2012 Versioned: specs.Versioned{ 2013 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 2014 }, 2015 MediaType: ocispec.MediaTypeImageIndex, 2016 Manifests: referrers, 2017 // set filter annotations 2018 Annotations: map[string]string{ 2019 spec.AnnotationReferrersFiltersApplied: "artifactType", 2020 }, 2021 } 2022 w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex) 2023 if err := json.NewEncoder(w).Encode(result); err != nil { 2024 t.Errorf("failed to write response: %v", err) 2025 } 2026 })) 2027 defer ts.Close() 2028 uri, err := url.Parse(ts.URL) 2029 if err != nil { 2030 t.Fatalf("invalid test http server: %v", err) 2031 } 2032 2033 repo, err := NewRepository(uri.Host + "/test") 2034 if err != nil { 2035 t.Fatalf("NewRepository() error = %v", err) 2036 } 2037 repo.PlainHTTP = true 2038 repo.ReferrerListPageSize = 2 2039 2040 ctx := context.Background() 2041 index := 0 2042 if err := repo.Referrers(ctx, manifestDesc, "application/vnd.test", func(got []ocispec.Descriptor) error { 2043 if index >= len(referrerSet) { 2044 t.Fatalf("out of index bound: %d", index) 2045 } 2046 referrers := referrerSet[index] 2047 index++ 2048 if !reflect.DeepEqual(got, referrers) { 2049 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 2050 } 2051 return nil 2052 }); err != nil { 2053 t.Errorf("Repository.Referrers() error = %v", err) 2054 } 2055 if index != len(referrerSet) { 2056 t.Errorf("fn invoked %d time(s), want %d", index, len(referrerSet)) 2057 } 2058 2059 // Test with filter header only 2060 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2061 path := "/v2/test/referrers/" + manifestDesc.Digest.String() 2062 queryParams, err := url.ParseQuery(r.URL.RawQuery) 2063 if err != nil { 2064 t.Fatal("failed to parse url query") 2065 } 2066 if r.Method != http.MethodGet || 2067 r.URL.Path != path || 2068 reflect.DeepEqual(queryParams["artifactType"], []string{"application%2Fvnd.test"}) { 2069 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 2070 w.WriteHeader(http.StatusNotFound) 2071 return 2072 } 2073 q := r.URL.Query() 2074 n, err := strconv.Atoi(q.Get("n")) 2075 if err != nil || n != 2 { 2076 t.Errorf("bad page size: %s", q.Get("n")) 2077 w.WriteHeader(http.StatusBadRequest) 2078 return 2079 } 2080 var referrers []ocispec.Descriptor 2081 switch q.Get("test") { 2082 case "foo": 2083 referrers = referrerSet[1] 2084 w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path)) 2085 case "bar": 2086 referrers = referrerSet[2] 2087 default: 2088 referrers = referrerSet[0] 2089 w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path)) 2090 } 2091 result := ocispec.Index{ 2092 Versioned: specs.Versioned{ 2093 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 2094 }, 2095 MediaType: ocispec.MediaTypeImageIndex, 2096 Manifests: referrers, 2097 } 2098 w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex) 2099 // set filter header 2100 w.Header().Set("OCI-Filters-Applied", "artifactType") 2101 if err := json.NewEncoder(w).Encode(result); err != nil { 2102 t.Errorf("failed to write response: %v", err) 2103 } 2104 })) 2105 defer ts.Close() 2106 uri, err = url.Parse(ts.URL) 2107 if err != nil { 2108 t.Fatalf("invalid test http server: %v", err) 2109 } 2110 2111 repo, err = NewRepository(uri.Host + "/test") 2112 if err != nil { 2113 t.Fatalf("NewRepository() error = %v", err) 2114 } 2115 repo.PlainHTTP = true 2116 repo.ReferrerListPageSize = 2 2117 2118 ctx = context.Background() 2119 index = 0 2120 if err := repo.Referrers(ctx, manifestDesc, "application/vnd.test", func(got []ocispec.Descriptor) error { 2121 if index >= len(referrerSet) { 2122 t.Fatalf("out of index bound: %d", index) 2123 } 2124 referrers := referrerSet[index] 2125 index++ 2126 if !reflect.DeepEqual(got, referrers) { 2127 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 2128 } 2129 return nil 2130 }); err != nil { 2131 t.Errorf("Repository.Referrers() error = %v", err) 2132 } 2133 if index != len(referrerSet) { 2134 t.Errorf("fn invoked %d time(s), want %d", index, len(referrerSet)) 2135 } 2136 2137 // Test with both filter annotation and filter header 2138 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2139 path := "/v2/test/referrers/" + manifestDesc.Digest.String() 2140 queryParams, err := url.ParseQuery(r.URL.RawQuery) 2141 if err != nil { 2142 t.Fatal("failed to parse url query") 2143 } 2144 if r.Method != http.MethodGet || 2145 r.URL.Path != path || 2146 reflect.DeepEqual(queryParams["artifactType"], []string{"application%2Fvnd.test"}) { 2147 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 2148 w.WriteHeader(http.StatusNotFound) 2149 return 2150 } 2151 q := r.URL.Query() 2152 n, err := strconv.Atoi(q.Get("n")) 2153 if err != nil || n != 2 { 2154 t.Errorf("bad page size: %s", q.Get("n")) 2155 w.WriteHeader(http.StatusBadRequest) 2156 return 2157 } 2158 var referrers []ocispec.Descriptor 2159 switch q.Get("test") { 2160 case "foo": 2161 referrers = referrerSet[1] 2162 w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path)) 2163 case "bar": 2164 referrers = referrerSet[2] 2165 default: 2166 referrers = referrerSet[0] 2167 w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path)) 2168 } 2169 result := ocispec.Index{ 2170 Versioned: specs.Versioned{ 2171 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 2172 }, 2173 MediaType: ocispec.MediaTypeImageIndex, 2174 Manifests: referrers, 2175 // set filter annotation 2176 Annotations: map[string]string{ 2177 spec.AnnotationReferrersFiltersApplied: "artifactType", 2178 }, 2179 } 2180 w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex) 2181 // set filter header 2182 w.Header().Set("OCI-Filters-Applied", "artifactType") 2183 if err := json.NewEncoder(w).Encode(result); err != nil { 2184 t.Errorf("failed to write response: %v", err) 2185 } 2186 })) 2187 defer ts.Close() 2188 uri, err = url.Parse(ts.URL) 2189 if err != nil { 2190 t.Fatalf("invalid test http server: %v", err) 2191 } 2192 2193 repo, err = NewRepository(uri.Host + "/test") 2194 if err != nil { 2195 t.Fatalf("NewRepository() error = %v", err) 2196 } 2197 repo.PlainHTTP = true 2198 repo.ReferrerListPageSize = 2 2199 2200 ctx = context.Background() 2201 index = 0 2202 if err := repo.Referrers(ctx, manifestDesc, "application/vnd.test", func(got []ocispec.Descriptor) error { 2203 if index >= len(referrerSet) { 2204 t.Fatalf("out of index bound: %d", index) 2205 } 2206 referrers := referrerSet[index] 2207 index++ 2208 if !reflect.DeepEqual(got, referrers) { 2209 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 2210 } 2211 return nil 2212 }); err != nil { 2213 t.Errorf("Repository.Referrers() error = %v", err) 2214 } 2215 if index != len(referrerSet) { 2216 t.Errorf("fn invoked %d time(s), want %d", index, len(referrerSet)) 2217 } 2218 } 2219 2220 func TestRepository_Referrers_ClientFiltering(t *testing.T) { 2221 manifest := []byte(`{"layers":[]}`) 2222 manifestDesc := ocispec.Descriptor{ 2223 MediaType: ocispec.MediaTypeImageManifest, 2224 Digest: digest.FromBytes(manifest), 2225 Size: int64(len(manifest)), 2226 } 2227 referrerSet := [][]ocispec.Descriptor{ 2228 { 2229 { 2230 MediaType: spec.MediaTypeArtifactManifest, 2231 Size: 1, 2232 Digest: digest.FromString("1"), 2233 ArtifactType: "application/vnd.test", 2234 }, 2235 { 2236 MediaType: spec.MediaTypeArtifactManifest, 2237 Size: 2, 2238 Digest: digest.FromString("2"), 2239 ArtifactType: "application/vnd.foo", 2240 }, 2241 }, 2242 { 2243 { 2244 MediaType: spec.MediaTypeArtifactManifest, 2245 Size: 3, 2246 Digest: digest.FromString("3"), 2247 ArtifactType: "application/vnd.test", 2248 }, 2249 { 2250 MediaType: spec.MediaTypeArtifactManifest, 2251 Size: 4, 2252 Digest: digest.FromString("4"), 2253 ArtifactType: "application/vnd.bar", 2254 }, 2255 }, 2256 { 2257 { 2258 MediaType: spec.MediaTypeArtifactManifest, 2259 Size: 5, 2260 Digest: digest.FromString("5"), 2261 ArtifactType: "application/vnd.baz", 2262 }, 2263 }, 2264 } 2265 filteredReferrerSet := [][]ocispec.Descriptor{ 2266 { 2267 { 2268 MediaType: spec.MediaTypeArtifactManifest, 2269 Size: 1, 2270 Digest: digest.FromString("1"), 2271 ArtifactType: "application/vnd.test", 2272 }, 2273 }, 2274 { 2275 { 2276 MediaType: spec.MediaTypeArtifactManifest, 2277 Size: 3, 2278 Digest: digest.FromString("3"), 2279 ArtifactType: "application/vnd.test", 2280 }, 2281 }, 2282 } 2283 var ts *httptest.Server 2284 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2285 path := "/v2/test/referrers/" + manifestDesc.Digest.String() 2286 queryParams, err := url.ParseQuery(r.URL.RawQuery) 2287 if err != nil { 2288 t.Fatal("failed to parse url query") 2289 } 2290 if r.Method != http.MethodGet || 2291 r.URL.Path != path || 2292 reflect.DeepEqual(queryParams["artifactType"], []string{"application%2Fvnd.test"}) { 2293 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 2294 w.WriteHeader(http.StatusNotFound) 2295 return 2296 } 2297 q := r.URL.Query() 2298 n, err := strconv.Atoi(q.Get("n")) 2299 if err != nil || n != 2 { 2300 t.Errorf("bad page size: %s", q.Get("n")) 2301 w.WriteHeader(http.StatusBadRequest) 2302 return 2303 } 2304 var referrers []ocispec.Descriptor 2305 switch q.Get("test") { 2306 case "foo": 2307 referrers = referrerSet[1] 2308 w.Header().Set("Link", fmt.Sprintf(`<%s%s?n=2&test=bar>; rel="next"`, ts.URL, path)) 2309 case "bar": 2310 referrers = referrerSet[2] 2311 default: 2312 referrers = referrerSet[0] 2313 w.Header().Set("Link", fmt.Sprintf(`<%s?n=2&test=foo>; rel="next"`, path)) 2314 } 2315 result := ocispec.Index{ 2316 Versioned: specs.Versioned{ 2317 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 2318 }, 2319 MediaType: ocispec.MediaTypeImageIndex, 2320 Manifests: referrers, 2321 } 2322 w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex) 2323 if err := json.NewEncoder(w).Encode(result); err != nil { 2324 t.Errorf("failed to write response: %v", err) 2325 } 2326 })) 2327 defer ts.Close() 2328 uri, err := url.Parse(ts.URL) 2329 if err != nil { 2330 t.Fatalf("invalid test http server: %v", err) 2331 } 2332 2333 repo, err := NewRepository(uri.Host + "/test") 2334 if err != nil { 2335 t.Fatalf("NewRepository() error = %v", err) 2336 } 2337 repo.PlainHTTP = true 2338 repo.ReferrerListPageSize = 2 2339 2340 ctx := context.Background() 2341 index := 0 2342 if err := repo.Referrers(ctx, manifestDesc, "application/vnd.test", func(got []ocispec.Descriptor) error { 2343 if index >= len(filteredReferrerSet) { 2344 t.Fatalf("out of index bound: %d", index) 2345 } 2346 referrers := filteredReferrerSet[index] 2347 index++ 2348 if !reflect.DeepEqual(got, referrers) { 2349 t.Errorf("Repository.Referrers() = %v, want %v", got, referrers) 2350 } 2351 return nil 2352 }); err != nil { 2353 t.Errorf("Repository.Referrers() error = %v", err) 2354 } 2355 if index != len(filteredReferrerSet) { 2356 t.Errorf("fn invoked %d time(s), want %d", index, len(referrerSet)) 2357 } 2358 } 2359 2360 func TestRepository_Referrers_TagSchemaFallback_ClientFiltering(t *testing.T) { 2361 manifest := []byte(`{"layers":[]}`) 2362 manifestDesc := ocispec.Descriptor{ 2363 MediaType: ocispec.MediaTypeImageManifest, 2364 Digest: digest.FromBytes(manifest), 2365 Size: int64(len(manifest)), 2366 } 2367 2368 referrers := []ocispec.Descriptor{ 2369 { 2370 MediaType: spec.MediaTypeArtifactManifest, 2371 Size: 1, 2372 Digest: digest.FromString("1"), 2373 ArtifactType: "application/vnd.test", 2374 }, 2375 { 2376 MediaType: spec.MediaTypeArtifactManifest, 2377 Size: 2, 2378 Digest: digest.FromString("2"), 2379 ArtifactType: "application/vnd.foo", 2380 }, 2381 { 2382 MediaType: spec.MediaTypeArtifactManifest, 2383 Size: 3, 2384 Digest: digest.FromString("3"), 2385 ArtifactType: "application/vnd.test", 2386 }, 2387 { 2388 MediaType: spec.MediaTypeArtifactManifest, 2389 Size: 4, 2390 Digest: digest.FromString("4"), 2391 ArtifactType: "application/vnd.bar", 2392 }, 2393 { 2394 MediaType: spec.MediaTypeArtifactManifest, 2395 Size: 5, 2396 Digest: digest.FromString("5"), 2397 ArtifactType: "application/vnd.baz", 2398 }, 2399 } 2400 filteredReferrers := []ocispec.Descriptor{ 2401 { 2402 MediaType: spec.MediaTypeArtifactManifest, 2403 Size: 1, 2404 Digest: digest.FromString("1"), 2405 ArtifactType: "application/vnd.test", 2406 }, 2407 { 2408 MediaType: spec.MediaTypeArtifactManifest, 2409 Size: 3, 2410 Digest: digest.FromString("3"), 2411 ArtifactType: "application/vnd.test", 2412 }, 2413 } 2414 2415 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2416 referrersTag := strings.Replace(manifestDesc.Digest.String(), ":", "-", 1) 2417 path := "/v2/test/manifests/" + referrersTag 2418 if r.Method != http.MethodGet || r.URL.Path != path { 2419 if r.URL.Path != "/v2/test/referrers/"+manifestDesc.Digest.String() { 2420 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 2421 } 2422 w.WriteHeader(http.StatusNotFound) 2423 return 2424 } 2425 2426 result := ocispec.Index{ 2427 Versioned: specs.Versioned{ 2428 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 2429 }, 2430 MediaType: ocispec.MediaTypeImageIndex, 2431 Manifests: referrers, 2432 } 2433 if err := json.NewEncoder(w).Encode(result); err != nil { 2434 t.Errorf("failed to write response: %v", err) 2435 } 2436 })) 2437 defer ts.Close() 2438 uri, err := url.Parse(ts.URL) 2439 if err != nil { 2440 t.Fatalf("invalid test http server: %v", err) 2441 } 2442 2443 repo, err := NewRepository(uri.Host + "/test") 2444 if err != nil { 2445 t.Fatalf("NewRepository() error = %v", err) 2446 } 2447 repo.PlainHTTP = true 2448 2449 ctx := context.Background() 2450 if err := repo.Referrers(ctx, manifestDesc, "application/vnd.test", func(got []ocispec.Descriptor) error { 2451 if !reflect.DeepEqual(got, filteredReferrers) { 2452 t.Errorf("Repository.Referrers() = %v, want %v", got, filteredReferrers) 2453 } 2454 return nil 2455 }); err != nil { 2456 t.Errorf("Repository.Referrers() error = %v", err) 2457 } 2458 } 2459 2460 func Test_BlobStore_Fetch(t *testing.T) { 2461 blob := []byte("hello world") 2462 blobDesc := ocispec.Descriptor{ 2463 MediaType: "test", 2464 Digest: digest.FromBytes(blob), 2465 Size: int64(len(blob)), 2466 } 2467 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2468 if r.Method != http.MethodGet { 2469 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2470 w.WriteHeader(http.StatusMethodNotAllowed) 2471 return 2472 } 2473 switch r.URL.Path { 2474 case "/v2/test/blobs/" + blobDesc.Digest.String(): 2475 w.Header().Set("Content-Type", "application/octet-stream") 2476 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2477 if _, err := w.Write(blob); err != nil { 2478 t.Errorf("failed to write %q: %v", r.URL, err) 2479 } 2480 default: 2481 w.WriteHeader(http.StatusNotFound) 2482 } 2483 })) 2484 defer ts.Close() 2485 uri, err := url.Parse(ts.URL) 2486 if err != nil { 2487 t.Fatalf("invalid test http server: %v", err) 2488 } 2489 2490 repo, err := NewRepository(uri.Host + "/test") 2491 if err != nil { 2492 t.Fatalf("NewRepository() error = %v", err) 2493 } 2494 repo.PlainHTTP = true 2495 store := repo.Blobs() 2496 ctx := context.Background() 2497 2498 rc, err := store.Fetch(ctx, blobDesc) 2499 if err != nil { 2500 t.Fatalf("Blobs.Fetch() error = %v", err) 2501 } 2502 buf := bytes.NewBuffer(nil) 2503 if _, err := buf.ReadFrom(rc); err != nil { 2504 t.Errorf("fail to read: %v", err) 2505 } 2506 if err := rc.Close(); err != nil { 2507 t.Errorf("fail to close: %v", err) 2508 } 2509 if got := buf.Bytes(); !bytes.Equal(got, blob) { 2510 t.Errorf("Blobs.Fetch() = %v, want %v", got, blob) 2511 } 2512 2513 content := []byte("foobar") 2514 contentDesc := ocispec.Descriptor{ 2515 MediaType: "test", 2516 Digest: digest.FromBytes(content), 2517 Size: int64(len(content)), 2518 } 2519 _, err = store.Fetch(ctx, contentDesc) 2520 if !errors.Is(err, errdef.ErrNotFound) { 2521 t.Errorf("Blobs.Fetch() error = %v, wantErr %v", err, errdef.ErrNotFound) 2522 } 2523 } 2524 2525 func Test_BlobStore_Fetch_Seek(t *testing.T) { 2526 blob := []byte("hello world") 2527 blobDesc := ocispec.Descriptor{ 2528 MediaType: "test", 2529 Digest: digest.FromBytes(blob), 2530 Size: int64(len(blob)), 2531 } 2532 seekable := false 2533 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2534 if r.Method != http.MethodGet { 2535 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2536 w.WriteHeader(http.StatusMethodNotAllowed) 2537 return 2538 } 2539 switch r.URL.Path { 2540 case "/v2/test/blobs/" + blobDesc.Digest.String(): 2541 w.Header().Set("Content-Type", "application/octet-stream") 2542 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2543 if seekable { 2544 w.Header().Set("Accept-Ranges", "bytes") 2545 } 2546 rangeHeader := r.Header.Get("Range") 2547 if !seekable || rangeHeader == "" { 2548 w.WriteHeader(http.StatusOK) 2549 if _, err := w.Write(blob); err != nil { 2550 t.Errorf("failed to write %q: %v", r.URL, err) 2551 } 2552 return 2553 } 2554 var start, end int 2555 _, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end) 2556 if err != nil { 2557 t.Errorf("invalid range header: %s", rangeHeader) 2558 w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) 2559 return 2560 } 2561 if start < 0 || start > end || start >= int(blobDesc.Size) { 2562 t.Errorf("invalid range: %s", rangeHeader) 2563 w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) 2564 return 2565 } 2566 end++ 2567 if end > int(blobDesc.Size) { 2568 end = int(blobDesc.Size) 2569 } 2570 w.WriteHeader(http.StatusPartialContent) 2571 if _, err := w.Write(blob[start:end]); err != nil { 2572 t.Errorf("failed to write %q: %v", r.URL, err) 2573 } 2574 default: 2575 w.WriteHeader(http.StatusNotFound) 2576 } 2577 })) 2578 defer ts.Close() 2579 uri, err := url.Parse(ts.URL) 2580 if err != nil { 2581 t.Fatalf("invalid test http server: %v", err) 2582 } 2583 2584 repo, err := NewRepository(uri.Host + "/test") 2585 if err != nil { 2586 t.Fatalf("NewRepository() error = %v", err) 2587 } 2588 repo.PlainHTTP = true 2589 store := repo.Blobs() 2590 ctx := context.Background() 2591 2592 rc, err := store.Fetch(ctx, blobDesc) 2593 if err != nil { 2594 t.Fatalf("Blobs.Fetch() error = %v", err) 2595 } 2596 if _, ok := rc.(io.Seeker); ok { 2597 t.Errorf("Blobs.Fetch() returns io.Seeker on non-seekable content") 2598 } 2599 buf := bytes.NewBuffer(nil) 2600 if _, err := buf.ReadFrom(rc); err != nil { 2601 t.Errorf("fail to read: %v", err) 2602 } 2603 if err := rc.Close(); err != nil { 2604 t.Errorf("fail to close: %v", err) 2605 } 2606 if got := buf.Bytes(); !bytes.Equal(got, blob) { 2607 t.Errorf("Blobs.Fetch() = %v, want %v", got, blob) 2608 } 2609 2610 seekable = true 2611 rc, err = store.Fetch(ctx, blobDesc) 2612 if err != nil { 2613 t.Fatalf("Blobs.Fetch() error = %v", err) 2614 } 2615 s, ok := rc.(io.Seeker) 2616 if !ok { 2617 t.Fatalf("Blobs.Fetch() = %v, want io.Seeker", rc) 2618 } 2619 buf.Reset() 2620 if _, err := buf.ReadFrom(rc); err != nil { 2621 t.Errorf("fail to read: %v", err) 2622 } 2623 if got := buf.Bytes(); !bytes.Equal(got, blob) { 2624 t.Errorf("Blobs.Fetch() = %v, want %v", got, blob) 2625 } 2626 2627 _, err = s.Seek(3, io.SeekStart) 2628 if err != nil { 2629 t.Errorf("fail to seek: %v", err) 2630 } 2631 buf.Reset() 2632 if _, err := buf.ReadFrom(rc); err != nil { 2633 t.Errorf("fail to read: %v", err) 2634 } 2635 if got := buf.Bytes(); !bytes.Equal(got, blob[3:]) { 2636 t.Errorf("Blobs.Fetch() = %v, want %v", got, blob[3:]) 2637 } 2638 2639 if err := rc.Close(); err != nil { 2640 t.Errorf("fail to close: %v", err) 2641 } 2642 } 2643 2644 func Test_BlobStore_Fetch_ZeroSizedBlob(t *testing.T) { 2645 blob := []byte("") 2646 blobDesc := ocispec.Descriptor{ 2647 MediaType: "test", 2648 Digest: digest.FromBytes(blob), 2649 Size: int64(len(blob)), 2650 } 2651 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2652 if r.Method != http.MethodGet { 2653 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2654 w.WriteHeader(http.StatusMethodNotAllowed) 2655 return 2656 } 2657 2658 switch r.URL.Path { 2659 case "/v2/test/blobs/" + blobDesc.Digest.String(): 2660 if rangeHeader := r.Header.Get("Range"); rangeHeader != "" { 2661 t.Errorf("unexpected range header") 2662 w.WriteHeader(http.StatusBadRequest) 2663 return 2664 } 2665 w.Header().Set("Content-Type", "application/octet-stream") 2666 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2667 default: 2668 w.WriteHeader(http.StatusNotFound) 2669 } 2670 })) 2671 defer ts.Close() 2672 uri, err := url.Parse(ts.URL) 2673 if err != nil { 2674 t.Fatalf("invalid test http server: %v", err) 2675 } 2676 2677 repo, err := NewRepository(uri.Host + "/test") 2678 if err != nil { 2679 t.Fatalf("NewRepository() error = %v", err) 2680 } 2681 repo.PlainHTTP = true 2682 store := repo.Blobs() 2683 ctx := context.Background() 2684 2685 rc, err := store.Fetch(ctx, blobDesc) 2686 if err != nil { 2687 t.Fatalf("Blobs.Fetch() error = %v", err) 2688 } 2689 buf := bytes.NewBuffer(nil) 2690 if _, err := buf.ReadFrom(rc); err != nil { 2691 t.Errorf("fail to read: %v", err) 2692 } 2693 if err := rc.Close(); err != nil { 2694 t.Errorf("fail to close: %v", err) 2695 } 2696 if got := buf.Bytes(); !bytes.Equal(got, blob) { 2697 t.Errorf("Blobs.Fetch() = %v, want %v", got, blob) 2698 } 2699 } 2700 2701 func Test_BlobStore_Push(t *testing.T) { 2702 blob := []byte("hello world") 2703 blobDesc := ocispec.Descriptor{ 2704 MediaType: "test", 2705 Digest: digest.FromBytes(blob), 2706 Size: int64(len(blob)), 2707 } 2708 var gotBlob []byte 2709 uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c" 2710 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2711 switch { 2712 case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/": 2713 w.Header().Set("Location", "/v2/test/blobs/uploads/"+uuid) 2714 w.WriteHeader(http.StatusAccepted) 2715 return 2716 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid: 2717 if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" { 2718 w.WriteHeader(http.StatusBadRequest) 2719 break 2720 } 2721 if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() { 2722 w.WriteHeader(http.StatusBadRequest) 2723 break 2724 } 2725 buf := bytes.NewBuffer(nil) 2726 if _, err := buf.ReadFrom(r.Body); err != nil { 2727 t.Errorf("fail to read: %v", err) 2728 } 2729 gotBlob = buf.Bytes() 2730 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2731 w.WriteHeader(http.StatusCreated) 2732 return 2733 default: 2734 w.WriteHeader(http.StatusForbidden) 2735 } 2736 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2737 })) 2738 defer ts.Close() 2739 uri, err := url.Parse(ts.URL) 2740 if err != nil { 2741 t.Fatalf("invalid test http server: %v", err) 2742 } 2743 2744 repo, err := NewRepository(uri.Host + "/test") 2745 if err != nil { 2746 t.Fatalf("NewRepository() error = %v", err) 2747 } 2748 repo.PlainHTTP = true 2749 store := repo.Blobs() 2750 ctx := context.Background() 2751 2752 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 2753 if err != nil { 2754 t.Fatalf("Blobs.Push() error = %v", err) 2755 } 2756 if !bytes.Equal(gotBlob, blob) { 2757 t.Errorf("Blobs.Push() = %v, want %v", gotBlob, blob) 2758 } 2759 } 2760 2761 func Test_BlobStore_Exists(t *testing.T) { 2762 blob := []byte("hello world") 2763 blobDesc := ocispec.Descriptor{ 2764 MediaType: "test", 2765 Digest: digest.FromBytes(blob), 2766 Size: int64(len(blob)), 2767 } 2768 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2769 if r.Method != http.MethodHead { 2770 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2771 w.WriteHeader(http.StatusMethodNotAllowed) 2772 return 2773 } 2774 switch r.URL.Path { 2775 case "/v2/test/blobs/" + blobDesc.Digest.String(): 2776 w.Header().Set("Content-Type", "application/octet-stream") 2777 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2778 w.Header().Set("Content-Length", strconv.Itoa(int(blobDesc.Size))) 2779 default: 2780 w.WriteHeader(http.StatusNotFound) 2781 } 2782 })) 2783 defer ts.Close() 2784 uri, err := url.Parse(ts.URL) 2785 if err != nil { 2786 t.Fatalf("invalid test http server: %v", err) 2787 } 2788 2789 repo, err := NewRepository(uri.Host + "/test") 2790 if err != nil { 2791 t.Fatalf("NewRepository() error = %v", err) 2792 } 2793 repo.PlainHTTP = true 2794 store := repo.Blobs() 2795 ctx := context.Background() 2796 2797 exists, err := store.Exists(ctx, blobDesc) 2798 if err != nil { 2799 t.Fatalf("Blobs.Exists() error = %v", err) 2800 } 2801 if !exists { 2802 t.Errorf("Blobs.Exists() = %v, want %v", exists, true) 2803 } 2804 2805 content := []byte("foobar") 2806 contentDesc := ocispec.Descriptor{ 2807 MediaType: "test", 2808 Digest: digest.FromBytes(content), 2809 Size: int64(len(content)), 2810 } 2811 exists, err = store.Exists(ctx, contentDesc) 2812 if err != nil { 2813 t.Fatalf("Blobs.Exists() error = %v", err) 2814 } 2815 if exists { 2816 t.Errorf("Blobs.Exists() = %v, want %v", exists, false) 2817 } 2818 } 2819 2820 func Test_BlobStore_Delete(t *testing.T) { 2821 blob := []byte("hello world") 2822 blobDesc := ocispec.Descriptor{ 2823 MediaType: "test", 2824 Digest: digest.FromBytes(blob), 2825 Size: int64(len(blob)), 2826 } 2827 blobDeleted := false 2828 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2829 if r.Method != http.MethodDelete { 2830 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2831 w.WriteHeader(http.StatusMethodNotAllowed) 2832 return 2833 } 2834 switch r.URL.Path { 2835 case "/v2/test/blobs/" + blobDesc.Digest.String(): 2836 blobDeleted = true 2837 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2838 w.WriteHeader(http.StatusAccepted) 2839 default: 2840 w.WriteHeader(http.StatusNotFound) 2841 } 2842 })) 2843 defer ts.Close() 2844 uri, err := url.Parse(ts.URL) 2845 if err != nil { 2846 t.Fatalf("invalid test http server: %v", err) 2847 } 2848 2849 repo, err := NewRepository(uri.Host + "/test") 2850 if err != nil { 2851 t.Fatalf("NewRepository() error = %v", err) 2852 } 2853 repo.PlainHTTP = true 2854 store := repo.Blobs() 2855 ctx := context.Background() 2856 2857 err = store.Delete(ctx, blobDesc) 2858 if err != nil { 2859 t.Fatalf("Blobs.Delete() error = %v", err) 2860 } 2861 if !blobDeleted { 2862 t.Errorf("Blobs.Delete() = %v, want %v", blobDeleted, true) 2863 } 2864 2865 content := []byte("foobar") 2866 contentDesc := ocispec.Descriptor{ 2867 MediaType: "test", 2868 Digest: digest.FromBytes(content), 2869 Size: int64(len(content)), 2870 } 2871 err = store.Delete(ctx, contentDesc) 2872 if !errors.Is(err, errdef.ErrNotFound) { 2873 t.Errorf("Blobs.Delete() error = %v, wantErr %v", err, errdef.ErrNotFound) 2874 } 2875 } 2876 2877 func Test_BlobStore_Resolve(t *testing.T) { 2878 blob := []byte("hello world") 2879 blobDesc := ocispec.Descriptor{ 2880 MediaType: "test", 2881 Digest: digest.FromBytes(blob), 2882 Size: int64(len(blob)), 2883 } 2884 ref := "foobar" 2885 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2886 if r.Method != http.MethodHead { 2887 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2888 w.WriteHeader(http.StatusMethodNotAllowed) 2889 return 2890 } 2891 switch r.URL.Path { 2892 case "/v2/test/blobs/" + blobDesc.Digest.String(): 2893 w.Header().Set("Content-Type", "application/octet-stream") 2894 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2895 w.Header().Set("Content-Length", strconv.Itoa(int(blobDesc.Size))) 2896 default: 2897 w.WriteHeader(http.StatusNotFound) 2898 } 2899 })) 2900 defer ts.Close() 2901 uri, err := url.Parse(ts.URL) 2902 if err != nil { 2903 t.Fatalf("invalid test http server: %v", err) 2904 } 2905 2906 repoName := uri.Host + "/test" 2907 repo, err := NewRepository(repoName) 2908 if err != nil { 2909 t.Fatalf("NewRepository() error = %v", err) 2910 } 2911 repo.PlainHTTP = true 2912 store := repo.Blobs() 2913 ctx := context.Background() 2914 2915 got, err := store.Resolve(ctx, blobDesc.Digest.String()) 2916 if err != nil { 2917 t.Fatalf("Blobs.Resolve() error = %v", err) 2918 } 2919 if got.Digest != blobDesc.Digest || got.Size != blobDesc.Size { 2920 t.Errorf("Blobs.Resolve() = %v, want %v", got, blobDesc) 2921 } 2922 2923 _, err = store.Resolve(ctx, ref) 2924 if !errors.Is(err, digest.ErrDigestInvalidFormat) { 2925 t.Errorf("Blobs.Resolve() error = %v, wantErr %v", err, digest.ErrDigestInvalidFormat) 2926 } 2927 2928 fqdnRef := repoName + "@" + blobDesc.Digest.String() 2929 got, err = store.Resolve(ctx, fqdnRef) 2930 if err != nil { 2931 t.Fatalf("Blobs.Resolve() error = %v", err) 2932 } 2933 if got.Digest != blobDesc.Digest || got.Size != blobDesc.Size { 2934 t.Errorf("Blobs.Resolve() = %v, want %v", got, blobDesc) 2935 } 2936 2937 content := []byte("foobar") 2938 contentDesc := ocispec.Descriptor{ 2939 MediaType: "test", 2940 Digest: digest.FromBytes(content), 2941 Size: int64(len(content)), 2942 } 2943 _, err = store.Resolve(ctx, contentDesc.Digest.String()) 2944 if !errors.Is(err, errdef.ErrNotFound) { 2945 t.Errorf("Blobs.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound) 2946 } 2947 } 2948 2949 func Test_BlobStore_FetchReference(t *testing.T) { 2950 blob := []byte("hello world") 2951 blobDesc := ocispec.Descriptor{ 2952 MediaType: "test", 2953 Digest: digest.FromBytes(blob), 2954 Size: int64(len(blob)), 2955 } 2956 ref := "foobar" 2957 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2958 if r.Method != http.MethodGet { 2959 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 2960 w.WriteHeader(http.StatusMethodNotAllowed) 2961 return 2962 } 2963 switch r.URL.Path { 2964 case "/v2/test/blobs/" + blobDesc.Digest.String(): 2965 w.Header().Set("Content-Type", "application/octet-stream") 2966 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 2967 if _, err := w.Write(blob); err != nil { 2968 t.Errorf("failed to write %q: %v", r.URL, err) 2969 } 2970 default: 2971 w.WriteHeader(http.StatusNotFound) 2972 } 2973 })) 2974 defer ts.Close() 2975 uri, err := url.Parse(ts.URL) 2976 if err != nil { 2977 t.Fatalf("invalid test http server: %v", err) 2978 } 2979 2980 repoName := uri.Host + "/test" 2981 repo, err := NewRepository(repoName) 2982 if err != nil { 2983 t.Fatalf("NewRepository() error = %v", err) 2984 } 2985 repo.PlainHTTP = true 2986 store := repo.Blobs() 2987 ctx := context.Background() 2988 2989 // test with digest 2990 gotDesc, rc, err := store.FetchReference(ctx, blobDesc.Digest.String()) 2991 if err != nil { 2992 t.Fatalf("Blobs.FetchReference() error = %v", err) 2993 } 2994 if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size { 2995 t.Errorf("Blobs.FetchReference() = %v, want %v", gotDesc, blobDesc) 2996 } 2997 buf := bytes.NewBuffer(nil) 2998 if _, err := buf.ReadFrom(rc); err != nil { 2999 t.Errorf("fail to read: %v", err) 3000 } 3001 if err := rc.Close(); err != nil { 3002 t.Errorf("fail to close: %v", err) 3003 } 3004 if got := buf.Bytes(); !bytes.Equal(got, blob) { 3005 t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob) 3006 } 3007 3008 // test with non-digest reference 3009 _, _, err = store.FetchReference(ctx, ref) 3010 if !errors.Is(err, digest.ErrDigestInvalidFormat) { 3011 t.Errorf("Blobs.FetchReference() error = %v, wantErr %v", err, digest.ErrDigestInvalidFormat) 3012 } 3013 3014 // test with FQDN reference 3015 fqdnRef := repoName + "@" + blobDesc.Digest.String() 3016 gotDesc, rc, err = store.FetchReference(ctx, fqdnRef) 3017 if err != nil { 3018 t.Fatalf("Blobs.FetchReference() error = %v", err) 3019 } 3020 if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size { 3021 t.Errorf("Blobs.FetchReference() = %v, want %v", gotDesc, blobDesc) 3022 } 3023 buf.Reset() 3024 if _, err := buf.ReadFrom(rc); err != nil { 3025 t.Errorf("fail to read: %v", err) 3026 } 3027 if err := rc.Close(); err != nil { 3028 t.Errorf("fail to close: %v", err) 3029 } 3030 if got := buf.Bytes(); !bytes.Equal(got, blob) { 3031 t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob) 3032 } 3033 3034 content := []byte("foobar") 3035 contentDesc := ocispec.Descriptor{ 3036 MediaType: "test", 3037 Digest: digest.FromBytes(content), 3038 Size: int64(len(content)), 3039 } 3040 3041 // test with other digest 3042 _, _, err = store.FetchReference(ctx, contentDesc.Digest.String()) 3043 if !errors.Is(err, errdef.ErrNotFound) { 3044 t.Errorf("Blobs.FetchReference() error = %v, wantErr %v", err, errdef.ErrNotFound) 3045 } 3046 } 3047 3048 func Test_BlobStore_FetchReference_Seek(t *testing.T) { 3049 blob := []byte("hello world") 3050 blobDesc := ocispec.Descriptor{ 3051 MediaType: "test", 3052 Digest: digest.FromBytes(blob), 3053 Size: int64(len(blob)), 3054 } 3055 seekable := false 3056 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3057 if r.Method != http.MethodGet { 3058 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3059 w.WriteHeader(http.StatusMethodNotAllowed) 3060 return 3061 } 3062 switch r.URL.Path { 3063 case "/v2/test/blobs/" + blobDesc.Digest.String(): 3064 w.Header().Set("Content-Type", "application/octet-stream") 3065 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 3066 if seekable { 3067 w.Header().Set("Accept-Ranges", "bytes") 3068 } 3069 rangeHeader := r.Header.Get("Range") 3070 if !seekable || rangeHeader == "" { 3071 w.WriteHeader(http.StatusOK) 3072 if _, err := w.Write(blob); err != nil { 3073 t.Errorf("failed to write %q: %v", r.URL, err) 3074 } 3075 return 3076 } 3077 var start int 3078 _, err := fmt.Sscanf(rangeHeader, "bytes=%d-", &start) 3079 if err != nil { 3080 t.Errorf("invalid range header: %s", rangeHeader) 3081 w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) 3082 return 3083 } 3084 if start < 0 || start >= int(blobDesc.Size) { 3085 t.Errorf("invalid range: %s", rangeHeader) 3086 w.WriteHeader(http.StatusRequestedRangeNotSatisfiable) 3087 return 3088 } 3089 3090 w.WriteHeader(http.StatusPartialContent) 3091 if _, err := w.Write(blob[start:]); err != nil { 3092 t.Errorf("failed to write %q: %v", r.URL, err) 3093 } 3094 default: 3095 w.WriteHeader(http.StatusNotFound) 3096 } 3097 })) 3098 defer ts.Close() 3099 uri, err := url.Parse(ts.URL) 3100 if err != nil { 3101 t.Fatalf("invalid test http server: %v", err) 3102 } 3103 3104 repo, err := NewRepository(uri.Host + "/test") 3105 if err != nil { 3106 t.Fatalf("NewRepository() error = %v", err) 3107 } 3108 repo.PlainHTTP = true 3109 store := repo.Blobs() 3110 ctx := context.Background() 3111 3112 // test non-seekable content 3113 gotDesc, rc, err := store.FetchReference(ctx, blobDesc.Digest.String()) 3114 if err != nil { 3115 t.Fatalf("Blobs.FetchReference() error = %v", err) 3116 } 3117 if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size { 3118 t.Errorf("Blobs.FetchReference() = %v, want %v", gotDesc, blobDesc) 3119 } 3120 if _, ok := rc.(io.Seeker); ok { 3121 t.Errorf("Blobs.FetchReference() returns io.Seeker on non-seekable content") 3122 } 3123 buf := bytes.NewBuffer(nil) 3124 if _, err := buf.ReadFrom(rc); err != nil { 3125 t.Errorf("fail to read: %v", err) 3126 } 3127 if err := rc.Close(); err != nil { 3128 t.Errorf("fail to close: %v", err) 3129 } 3130 if got := buf.Bytes(); !bytes.Equal(got, blob) { 3131 t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob) 3132 } 3133 3134 // test seekable content 3135 seekable = true 3136 gotDesc, rc, err = store.FetchReference(ctx, blobDesc.Digest.String()) 3137 if err != nil { 3138 t.Fatalf("Blobs.FetchReference() error = %v", err) 3139 } 3140 if gotDesc.Digest != blobDesc.Digest || gotDesc.Size != blobDesc.Size { 3141 t.Errorf("Blobs.FetchReference() = %v, want %v", gotDesc, blobDesc) 3142 } 3143 s, ok := rc.(io.Seeker) 3144 if !ok { 3145 t.Fatalf("Blobs.FetchReference() = %v, want io.Seeker", rc) 3146 } 3147 buf.Reset() 3148 if _, err := buf.ReadFrom(rc); err != nil { 3149 t.Errorf("fail to read: %v", err) 3150 } 3151 if got := buf.Bytes(); !bytes.Equal(got, blob) { 3152 t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob) 3153 } 3154 3155 _, err = s.Seek(3, io.SeekStart) 3156 if err != nil { 3157 t.Errorf("fail to seek: %v", err) 3158 } 3159 buf.Reset() 3160 if _, err := buf.ReadFrom(rc); err != nil { 3161 t.Errorf("fail to read: %v", err) 3162 } 3163 if got := buf.Bytes(); !bytes.Equal(got, blob[3:]) { 3164 t.Errorf("Blobs.FetchReference() = %v, want %v", got, blob[3:]) 3165 } 3166 3167 if err := rc.Close(); err != nil { 3168 t.Errorf("fail to close: %v", err) 3169 } 3170 } 3171 3172 func Test_generateBlobDescriptorWithVariousDockerContentDigestHeaders(t *testing.T) { 3173 reference := registry.Reference{ 3174 Registry: "eastern.haan.com", 3175 Reference: "<calculate>", 3176 Repository: "from25to220ce", 3177 } 3178 tests := getTestIOStructMapForGetDescriptorClass() 3179 for testName, dcdIOStruct := range tests { 3180 if dcdIOStruct.isTag { 3181 continue 3182 } 3183 3184 for i, method := range []string{http.MethodGet, http.MethodHead} { 3185 reference.Reference = dcdIOStruct.clientSuppliedReference 3186 3187 resp := http.Response{ 3188 Header: http.Header{ 3189 "Content-Type": []string{"application/vnd.docker.distribution.manifest.v2+json"}, 3190 headerDockerContentDigest: []string{dcdIOStruct.serverCalculatedDigest.String()}, 3191 }, 3192 } 3193 if method == http.MethodGet { 3194 resp.Body = io.NopCloser(bytes.NewBufferString(theAmazingBanClan)) 3195 } 3196 resp.Request = &http.Request{ 3197 Method: method, 3198 } 3199 3200 var err error 3201 var d digest.Digest 3202 if d, err = reference.Digest(); err != nil { 3203 t.Errorf( 3204 "[Blob.%v] %v; got digest from a tag reference unexpectedly", 3205 method, testName, 3206 ) 3207 } 3208 3209 errExpected := []bool{dcdIOStruct.errExpectedOnGET, dcdIOStruct.errExpectedOnHEAD}[i] 3210 if len(d) == 0 { 3211 // To avoid an otherwise impossible scenario in the tested code 3212 // path, we set d so that verifyContentDigest does not break. 3213 d = dcdIOStruct.serverCalculatedDigest 3214 } 3215 _, err = generateBlobDescriptor(&resp, d) 3216 if !errExpected && err != nil { 3217 t.Errorf( 3218 "[Blob.%v] %v; expected no error for request, but got err: %v", 3219 method, testName, err, 3220 ) 3221 } else if errExpected && err == nil { 3222 t.Errorf( 3223 "[Blob.%v] %v; expected an error for request, but got none", 3224 method, testName, 3225 ) 3226 } 3227 } 3228 } 3229 } 3230 3231 func TestManifestStoreInterface(t *testing.T) { 3232 var ms interface{} = &manifestStore{} 3233 if _, ok := ms.(interfaces.ReferenceParser); !ok { 3234 t.Error("&manifestStore{} does not conform interfaces.ReferenceParser") 3235 } 3236 } 3237 3238 func TestRepositoryMounterInterface(t *testing.T) { 3239 var r interface{} = &Repository{} 3240 if _, ok := r.(registry.Mounter); !ok { 3241 t.Error("&Repository{} does not conform to registry.Mounter") 3242 } 3243 } 3244 3245 func Test_ManifestStore_Fetch(t *testing.T) { 3246 manifest := []byte(`{"layers":[]}`) 3247 manifestDesc := ocispec.Descriptor{ 3248 MediaType: ocispec.MediaTypeImageManifest, 3249 Digest: digest.FromBytes(manifest), 3250 Size: int64(len(manifest)), 3251 } 3252 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3253 if r.Method != http.MethodGet { 3254 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3255 w.WriteHeader(http.StatusMethodNotAllowed) 3256 return 3257 } 3258 switch r.URL.Path { 3259 case "/v2/test/manifests/" + manifestDesc.Digest.String(): 3260 if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { 3261 t.Errorf("manifest not convertable: %s", accept) 3262 w.WriteHeader(http.StatusBadRequest) 3263 return 3264 } 3265 w.Header().Set("Content-Type", manifestDesc.MediaType) 3266 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 3267 if _, err := w.Write(manifest); err != nil { 3268 t.Errorf("failed to write %q: %v", r.URL, err) 3269 } 3270 default: 3271 w.WriteHeader(http.StatusNotFound) 3272 } 3273 })) 3274 defer ts.Close() 3275 uri, err := url.Parse(ts.URL) 3276 if err != nil { 3277 t.Fatalf("invalid test http server: %v", err) 3278 } 3279 3280 repo, err := NewRepository(uri.Host + "/test") 3281 if err != nil { 3282 t.Fatalf("NewRepository() error = %v", err) 3283 } 3284 repo.PlainHTTP = true 3285 store := repo.Manifests() 3286 ctx := context.Background() 3287 3288 rc, err := store.Fetch(ctx, manifestDesc) 3289 if err != nil { 3290 t.Fatalf("Manifests.Fetch() error = %v", err) 3291 } 3292 buf := bytes.NewBuffer(nil) 3293 if _, err := buf.ReadFrom(rc); err != nil { 3294 t.Errorf("fail to read: %v", err) 3295 } 3296 if err := rc.Close(); err != nil { 3297 t.Errorf("fail to close: %v", err) 3298 } 3299 if got := buf.Bytes(); !bytes.Equal(got, manifest) { 3300 t.Errorf("Manifests.Fetch() = %v, want %v", got, manifest) 3301 } 3302 3303 content := []byte(`{"manifests":[]}`) 3304 contentDesc := ocispec.Descriptor{ 3305 MediaType: ocispec.MediaTypeImageIndex, 3306 Digest: digest.FromBytes(content), 3307 Size: int64(len(content)), 3308 } 3309 _, err = store.Fetch(ctx, contentDesc) 3310 if !errors.Is(err, errdef.ErrNotFound) { 3311 t.Errorf("Manifests.Fetch() error = %v, wantErr %v", err, errdef.ErrNotFound) 3312 } 3313 } 3314 3315 func Test_ManifestStore_Push(t *testing.T) { 3316 manifest := []byte(`{"layers":[]}`) 3317 manifestDesc := ocispec.Descriptor{ 3318 MediaType: ocispec.MediaTypeImageManifest, 3319 Digest: digest.FromBytes(manifest), 3320 Size: int64(len(manifest)), 3321 } 3322 var gotManifest []byte 3323 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3324 switch { 3325 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 3326 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 3327 w.WriteHeader(http.StatusBadRequest) 3328 break 3329 } 3330 buf := bytes.NewBuffer(nil) 3331 if _, err := buf.ReadFrom(r.Body); err != nil { 3332 t.Errorf("fail to read: %v", err) 3333 } 3334 gotManifest = buf.Bytes() 3335 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 3336 w.WriteHeader(http.StatusCreated) 3337 return 3338 default: 3339 w.WriteHeader(http.StatusForbidden) 3340 } 3341 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3342 })) 3343 defer ts.Close() 3344 uri, err := url.Parse(ts.URL) 3345 if err != nil { 3346 t.Fatalf("invalid test http server: %v", err) 3347 } 3348 3349 repo, err := NewRepository(uri.Host + "/test") 3350 if err != nil { 3351 t.Fatalf("NewRepository() error = %v", err) 3352 } 3353 repo.PlainHTTP = true 3354 store := repo.Manifests() 3355 ctx := context.Background() 3356 3357 err = store.Push(ctx, manifestDesc, bytes.NewReader(manifest)) 3358 if err != nil { 3359 t.Fatalf("Manifests.Push() error = %v", err) 3360 } 3361 if !bytes.Equal(gotManifest, manifest) { 3362 t.Errorf("Manifests.Push() = %v, want %v", gotManifest, manifest) 3363 } 3364 } 3365 3366 func Test_ManifestStore_Push_ReferrersAPIAvailable(t *testing.T) { 3367 // generate test content 3368 subject := []byte(`{"layers":[]}`) 3369 subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject) 3370 artifact := spec.Artifact{ 3371 MediaType: spec.MediaTypeArtifactManifest, 3372 Subject: &subjectDesc, 3373 } 3374 artifactJSON, err := json.Marshal(artifact) 3375 if err != nil { 3376 t.Fatalf("failed to marshal manifest: %v", err) 3377 } 3378 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 3379 manifest := ocispec.Manifest{ 3380 MediaType: ocispec.MediaTypeImageManifest, 3381 Subject: &subjectDesc, 3382 } 3383 manifestJSON, err := json.Marshal(manifest) 3384 if err != nil { 3385 t.Fatalf("failed to marshal manifest: %v", err) 3386 } 3387 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 3388 index := ocispec.Index{ 3389 MediaType: ocispec.MediaTypeImageIndex, 3390 Subject: &subjectDesc, 3391 } 3392 indexJSON, err := json.Marshal(index) 3393 if err != nil { 3394 t.Fatalf("failed to marshal manifest: %v", err) 3395 } 3396 indexDesc := content.NewDescriptorFromBytes(manifest.MediaType, indexJSON) 3397 3398 var gotManifest []byte 3399 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3400 switch { 3401 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 3402 if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType { 3403 w.WriteHeader(http.StatusBadRequest) 3404 break 3405 } 3406 buf := bytes.NewBuffer(nil) 3407 if _, err := buf.ReadFrom(r.Body); err != nil { 3408 t.Errorf("fail to read: %v", err) 3409 } 3410 gotManifest = buf.Bytes() 3411 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 3412 w.Header().Set("OCI-Subject", subjectDesc.Digest.String()) 3413 w.WriteHeader(http.StatusCreated) 3414 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 3415 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 3416 w.WriteHeader(http.StatusBadRequest) 3417 break 3418 } 3419 buf := bytes.NewBuffer(nil) 3420 if _, err := buf.ReadFrom(r.Body); err != nil { 3421 t.Errorf("fail to read: %v", err) 3422 } 3423 gotManifest = buf.Bytes() 3424 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 3425 w.Header().Set("OCI-Subject", subjectDesc.Digest.String()) 3426 w.WriteHeader(http.StatusCreated) 3427 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 3428 if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { 3429 w.WriteHeader(http.StatusBadRequest) 3430 break 3431 } 3432 buf := bytes.NewBuffer(nil) 3433 if _, err := buf.ReadFrom(r.Body); err != nil { 3434 t.Errorf("fail to read: %v", err) 3435 } 3436 gotManifest = buf.Bytes() 3437 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 3438 w.Header().Set("OCI-Subject", subjectDesc.Digest.String()) 3439 w.WriteHeader(http.StatusCreated) 3440 default: 3441 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3442 w.WriteHeader(http.StatusNotFound) 3443 } 3444 })) 3445 defer ts.Close() 3446 uri, err := url.Parse(ts.URL) 3447 if err != nil { 3448 t.Fatalf("invalid test http server: %v", err) 3449 } 3450 ctx := context.Background() 3451 3452 // test pushing artifact with subject 3453 repo, err := NewRepository(uri.Host + "/test") 3454 if err != nil { 3455 t.Fatalf("NewRepository() error = %v", err) 3456 } 3457 repo.PlainHTTP = true 3458 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3459 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3460 } 3461 err = repo.Push(ctx, artifactDesc, bytes.NewReader(artifactJSON)) 3462 if err != nil { 3463 t.Fatalf("Manifests.Push() error = %v", err) 3464 } 3465 if !bytes.Equal(gotManifest, artifactJSON) { 3466 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON)) 3467 } 3468 if state := repo.loadReferrersState(); state != referrersStateSupported { 3469 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 3470 } 3471 3472 // test pushing image manifest with subject 3473 repo, err = NewRepository(uri.Host + "/test") 3474 if err != nil { 3475 t.Fatalf("NewRepository() error = %v", err) 3476 } 3477 repo.PlainHTTP = true 3478 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3479 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3480 } 3481 err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) 3482 if err != nil { 3483 t.Fatalf("Manifests.Push() error = %v", err) 3484 } 3485 if !bytes.Equal(gotManifest, manifestJSON) { 3486 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) 3487 } 3488 if state := repo.loadReferrersState(); state != referrersStateSupported { 3489 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 3490 } 3491 3492 // test pushing image index with subject 3493 err = repo.Push(ctx, indexDesc, bytes.NewReader(indexJSON)) 3494 if err != nil { 3495 t.Fatalf("Manifests.Push() error = %v", err) 3496 } 3497 if !bytes.Equal(gotManifest, indexJSON) { 3498 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(indexJSON)) 3499 } 3500 if state := repo.loadReferrersState(); state != referrersStateSupported { 3501 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 3502 } 3503 } 3504 3505 func Test_ManifestStore_Push_ReferrersAPIUnavailable(t *testing.T) { 3506 // generate test content 3507 subject := []byte(`{"layers":[]}`) 3508 subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject) 3509 referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) 3510 artifact := spec.Artifact{ 3511 MediaType: spec.MediaTypeArtifactManifest, 3512 Subject: &subjectDesc, 3513 ArtifactType: "application/vnd.test", 3514 Annotations: map[string]string{"foo": "bar"}, 3515 } 3516 artifactJSON, err := json.Marshal(artifact) 3517 if err != nil { 3518 t.Fatalf("failed to marshal manifest: %v", err) 3519 } 3520 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 3521 artifactDesc.ArtifactType = artifact.ArtifactType 3522 artifactDesc.Annotations = artifact.Annotations 3523 3524 // test pushing artifact with subject, a referrer list should be created 3525 index_1 := ocispec.Index{ 3526 Versioned: specs.Versioned{ 3527 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 3528 }, 3529 MediaType: ocispec.MediaTypeImageIndex, 3530 Manifests: []ocispec.Descriptor{ 3531 artifactDesc, 3532 }, 3533 } 3534 indexJSON_1, err := json.Marshal(index_1) 3535 if err != nil { 3536 t.Fatalf("failed to marshal manifest: %v", err) 3537 } 3538 indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1) 3539 var gotManifest []byte 3540 var gotReferrerIndex []byte 3541 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3542 switch { 3543 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 3544 if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType { 3545 w.WriteHeader(http.StatusBadRequest) 3546 break 3547 } 3548 buf := bytes.NewBuffer(nil) 3549 if _, err := buf.ReadFrom(r.Body); err != nil { 3550 t.Errorf("fail to read: %v", err) 3551 } 3552 gotManifest = buf.Bytes() 3553 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 3554 w.WriteHeader(http.StatusCreated) 3555 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3556 w.WriteHeader(http.StatusNotFound) 3557 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3558 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 3559 w.WriteHeader(http.StatusBadRequest) 3560 break 3561 } 3562 buf := bytes.NewBuffer(nil) 3563 if _, err := buf.ReadFrom(r.Body); err != nil { 3564 t.Errorf("fail to read: %v", err) 3565 } 3566 gotReferrerIndex = buf.Bytes() 3567 w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String()) 3568 w.WriteHeader(http.StatusCreated) 3569 default: 3570 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3571 w.WriteHeader(http.StatusNotFound) 3572 } 3573 })) 3574 defer ts.Close() 3575 uri, err := url.Parse(ts.URL) 3576 if err != nil { 3577 t.Fatalf("invalid test http server: %v", err) 3578 } 3579 3580 ctx := context.Background() 3581 repo, err := NewRepository(uri.Host + "/test") 3582 if err != nil { 3583 t.Fatalf("NewRepository() error = %v", err) 3584 } 3585 repo.PlainHTTP = true 3586 3587 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3588 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3589 } 3590 err = repo.Push(ctx, artifactDesc, bytes.NewReader(artifactJSON)) 3591 if err != nil { 3592 t.Fatalf("Manifests.Push() error = %v", err) 3593 } 3594 if !bytes.Equal(gotManifest, artifactJSON) { 3595 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON)) 3596 } 3597 if !bytes.Equal(gotReferrerIndex, indexJSON_1) { 3598 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1)) 3599 } 3600 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 3601 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 3602 } 3603 3604 // test pushing artifact with subject when an old empty referrer list exists, 3605 // the referrer list should be updated 3606 emptyIndex := ocispec.Index{ 3607 Versioned: specs.Versioned{ 3608 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 3609 }, 3610 MediaType: ocispec.MediaTypeImageIndex, 3611 } 3612 emptyIndexJSON, err := json.Marshal(emptyIndex) 3613 if err != nil { 3614 t.Error("failed to marshal index", err) 3615 } 3616 emptyIndexDesc := content.NewDescriptorFromBytes(emptyIndex.MediaType, emptyIndexJSON) 3617 var indexDeleted bool 3618 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3619 switch { 3620 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 3621 if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType { 3622 w.WriteHeader(http.StatusBadRequest) 3623 break 3624 } 3625 buf := bytes.NewBuffer(nil) 3626 if _, err := buf.ReadFrom(r.Body); err != nil { 3627 t.Errorf("fail to read: %v", err) 3628 } 3629 gotManifest = buf.Bytes() 3630 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 3631 w.WriteHeader(http.StatusCreated) 3632 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3633 w.Write(emptyIndexJSON) 3634 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3635 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 3636 w.WriteHeader(http.StatusBadRequest) 3637 break 3638 } 3639 buf := bytes.NewBuffer(nil) 3640 if _, err := buf.ReadFrom(r.Body); err != nil { 3641 t.Errorf("fail to read: %v", err) 3642 } 3643 gotReferrerIndex = buf.Bytes() 3644 w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String()) 3645 w.WriteHeader(http.StatusCreated) 3646 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+emptyIndexDesc.Digest.String(): 3647 indexDeleted = true 3648 // no "Docker-Content-Digest" header for manifest deletion 3649 w.WriteHeader(http.StatusAccepted) 3650 default: 3651 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3652 w.WriteHeader(http.StatusNotFound) 3653 } 3654 })) 3655 defer ts.Close() 3656 uri, err = url.Parse(ts.URL) 3657 if err != nil { 3658 t.Fatalf("invalid test http server: %v", err) 3659 } 3660 3661 ctx = context.Background() 3662 repo, err = NewRepository(uri.Host + "/test") 3663 if err != nil { 3664 t.Fatalf("NewRepository() error = %v", err) 3665 } 3666 repo.PlainHTTP = true 3667 3668 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3669 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3670 } 3671 err = repo.Push(ctx, artifactDesc, bytes.NewReader(artifactJSON)) 3672 if err != nil { 3673 t.Fatalf("Manifests.Push() error = %v", err) 3674 } 3675 if !bytes.Equal(gotManifest, artifactJSON) { 3676 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON)) 3677 } 3678 if !bytes.Equal(gotReferrerIndex, indexJSON_1) { 3679 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1)) 3680 } 3681 if !indexDeleted { 3682 t.Errorf("indexDeleted = %v, want %v", indexDeleted, true) 3683 } 3684 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 3685 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 3686 } 3687 3688 // test pushing image manifest with subject, referrer list should be updated 3689 manifest := ocispec.Manifest{ 3690 MediaType: ocispec.MediaTypeImageManifest, 3691 Config: ocispec.Descriptor{ 3692 MediaType: "testconfig", 3693 }, 3694 Subject: &subjectDesc, 3695 Annotations: map[string]string{"foo": "bar"}, 3696 } 3697 manifestJSON, err := json.Marshal(manifest) 3698 if err != nil { 3699 t.Fatalf("failed to marshal manifest: %v", err) 3700 } 3701 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 3702 manifestDesc.ArtifactType = manifest.Config.MediaType 3703 manifestDesc.Annotations = manifest.Annotations 3704 index_2 := ocispec.Index{ 3705 Versioned: specs.Versioned{ 3706 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 3707 }, 3708 MediaType: ocispec.MediaTypeImageIndex, 3709 Manifests: []ocispec.Descriptor{ 3710 artifactDesc, 3711 manifestDesc, 3712 }, 3713 } 3714 indexJSON_2, err := json.Marshal(index_2) 3715 if err != nil { 3716 t.Fatalf("failed to marshal manifest: %v", err) 3717 } 3718 indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2) 3719 indexDeleted = false 3720 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3721 switch { 3722 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 3723 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 3724 w.WriteHeader(http.StatusBadRequest) 3725 break 3726 } 3727 buf := bytes.NewBuffer(nil) 3728 if _, err := buf.ReadFrom(r.Body); err != nil { 3729 t.Errorf("fail to read: %v", err) 3730 } 3731 gotManifest = buf.Bytes() 3732 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 3733 w.WriteHeader(http.StatusCreated) 3734 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3735 w.Write(indexJSON_1) 3736 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3737 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 3738 w.WriteHeader(http.StatusBadRequest) 3739 break 3740 } 3741 buf := bytes.NewBuffer(nil) 3742 if _, err := buf.ReadFrom(r.Body); err != nil { 3743 t.Errorf("fail to read: %v", err) 3744 } 3745 gotReferrerIndex = buf.Bytes() 3746 w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String()) 3747 w.WriteHeader(http.StatusCreated) 3748 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_1.Digest.String(): 3749 indexDeleted = true 3750 // no "Docker-Content-Digest" header for manifest deletion 3751 w.WriteHeader(http.StatusAccepted) 3752 default: 3753 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3754 w.WriteHeader(http.StatusNotFound) 3755 } 3756 })) 3757 defer ts.Close() 3758 uri, err = url.Parse(ts.URL) 3759 if err != nil { 3760 t.Fatalf("invalid test http server: %v", err) 3761 } 3762 3763 ctx = context.Background() 3764 repo, err = NewRepository(uri.Host + "/test") 3765 if err != nil { 3766 t.Fatalf("NewRepository() error = %v", err) 3767 } 3768 repo.PlainHTTP = true 3769 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3770 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3771 } 3772 err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) 3773 if err != nil { 3774 t.Fatalf("Manifests.Push() error = %v", err) 3775 } 3776 if !bytes.Equal(gotManifest, manifestJSON) { 3777 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) 3778 } 3779 if !bytes.Equal(gotReferrerIndex, indexJSON_2) { 3780 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) 3781 } 3782 if !indexDeleted { 3783 t.Errorf("indexDeleted = %v, want %v", indexDeleted, true) 3784 } 3785 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 3786 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 3787 } 3788 3789 // test pushing image manifest with subject again, referrers list should not be changed 3790 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3791 switch { 3792 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 3793 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 3794 w.WriteHeader(http.StatusBadRequest) 3795 break 3796 } 3797 buf := bytes.NewBuffer(nil) 3798 if _, err := buf.ReadFrom(r.Body); err != nil { 3799 t.Errorf("fail to read: %v", err) 3800 } 3801 gotManifest = buf.Bytes() 3802 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 3803 w.WriteHeader(http.StatusCreated) 3804 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3805 w.Write(indexJSON_2) 3806 default: 3807 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3808 w.WriteHeader(http.StatusNotFound) 3809 } 3810 })) 3811 defer ts.Close() 3812 uri, err = url.Parse(ts.URL) 3813 if err != nil { 3814 t.Fatalf("invalid test http server: %v", err) 3815 } 3816 3817 ctx = context.Background() 3818 repo, err = NewRepository(uri.Host + "/test") 3819 if err != nil { 3820 t.Fatalf("NewRepository() error = %v", err) 3821 } 3822 repo.PlainHTTP = true 3823 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3824 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3825 } 3826 err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) 3827 if err != nil { 3828 t.Fatalf("Manifests.Push() error = %v", err) 3829 } 3830 if !bytes.Equal(gotManifest, manifestJSON) { 3831 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) 3832 } 3833 // referrers list should not be changed 3834 if !bytes.Equal(gotReferrerIndex, indexJSON_2) { 3835 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) 3836 } 3837 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 3838 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 3839 } 3840 3841 // push image index with subject, referrer list should be updated 3842 indexManifest := ocispec.Index{ 3843 MediaType: ocispec.MediaTypeImageIndex, 3844 Subject: &subjectDesc, 3845 ArtifactType: "test/index", 3846 Annotations: map[string]string{"foo": "bar"}, 3847 } 3848 indexManifestJSON, err := json.Marshal(indexManifest) 3849 if err != nil { 3850 t.Fatalf("failed to marshal manifest: %v", err) 3851 } 3852 indexManifestDesc := content.NewDescriptorFromBytes(indexManifest.MediaType, indexManifestJSON) 3853 indexManifestDesc.ArtifactType = indexManifest.ArtifactType 3854 indexManifestDesc.Annotations = indexManifest.Annotations 3855 index_3 := ocispec.Index{ 3856 Versioned: specs.Versioned{ 3857 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 3858 }, 3859 MediaType: ocispec.MediaTypeImageIndex, 3860 Manifests: []ocispec.Descriptor{ 3861 artifactDesc, 3862 manifestDesc, 3863 indexManifestDesc, 3864 }, 3865 } 3866 indexJSON_3, err := json.Marshal(index_3) 3867 if err != nil { 3868 t.Fatalf("failed to marshal manifest: %v", err) 3869 } 3870 indexDesc_3 := content.NewDescriptorFromBytes(index_3.MediaType, indexJSON_3) 3871 indexDeleted = false 3872 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3873 switch { 3874 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexManifestDesc.Digest.String(): 3875 if contentType := r.Header.Get("Content-Type"); contentType != indexManifestDesc.MediaType { 3876 w.WriteHeader(http.StatusBadRequest) 3877 break 3878 } 3879 buf := bytes.NewBuffer(nil) 3880 if _, err := buf.ReadFrom(r.Body); err != nil { 3881 t.Errorf("fail to read: %v", err) 3882 } 3883 gotManifest = buf.Bytes() 3884 w.Header().Set("Docker-Content-Digest", indexManifestDesc.Digest.String()) 3885 w.WriteHeader(http.StatusCreated) 3886 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3887 w.Write(indexJSON_2) 3888 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3889 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 3890 w.WriteHeader(http.StatusBadRequest) 3891 break 3892 } 3893 buf := bytes.NewBuffer(nil) 3894 if _, err := buf.ReadFrom(r.Body); err != nil { 3895 t.Errorf("fail to read: %v", err) 3896 } 3897 gotReferrerIndex = buf.Bytes() 3898 w.Header().Set("Docker-Content-Digest", indexDesc_3.Digest.String()) 3899 w.WriteHeader(http.StatusCreated) 3900 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_2.Digest.String(): 3901 indexDeleted = true 3902 // no "Docker-Content-Digest" header for manifest deletion 3903 w.WriteHeader(http.StatusAccepted) 3904 default: 3905 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 3906 w.WriteHeader(http.StatusNotFound) 3907 } 3908 })) 3909 defer ts.Close() 3910 uri, err = url.Parse(ts.URL) 3911 if err != nil { 3912 t.Fatalf("invalid test http server: %v", err) 3913 } 3914 3915 ctx = context.Background() 3916 repo, err = NewRepository(uri.Host + "/test") 3917 if err != nil { 3918 t.Fatalf("NewRepository() error = %v", err) 3919 } 3920 repo.PlainHTTP = true 3921 if state := repo.loadReferrersState(); state != referrersStateUnknown { 3922 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 3923 } 3924 err = repo.Push(ctx, indexManifestDesc, bytes.NewReader(indexManifestJSON)) 3925 if err != nil { 3926 t.Fatalf("Manifests.Push() error = %v", err) 3927 } 3928 if !bytes.Equal(gotManifest, indexManifestJSON) { 3929 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(indexManifestJSON)) 3930 } 3931 if !bytes.Equal(gotReferrerIndex, indexJSON_3) { 3932 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_3)) 3933 } 3934 if !indexDeleted { 3935 t.Errorf("indexDeleted = %v, want %v", indexDeleted, true) 3936 } 3937 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 3938 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 3939 } 3940 } 3941 3942 func Test_ManifestStore_Push_ReferrersAPIUnavailable_SkipReferrersGC(t *testing.T) { 3943 // generate test content 3944 subject := []byte(`{"layers":[]}`) 3945 subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject) 3946 referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) 3947 manifest := ocispec.Manifest{ 3948 MediaType: ocispec.MediaTypeImageManifest, 3949 Config: ocispec.Descriptor{ 3950 MediaType: "testconfig", 3951 }, 3952 Subject: &subjectDesc, 3953 Annotations: map[string]string{"foo": "bar"}, 3954 } 3955 manifestJSON, err := json.Marshal(manifest) 3956 if err != nil { 3957 t.Fatalf("failed to marshal manifest: %v", err) 3958 } 3959 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 3960 manifestDesc.ArtifactType = manifest.Config.MediaType 3961 manifestDesc.Annotations = manifest.Annotations 3962 index_1 := ocispec.Index{ 3963 Versioned: specs.Versioned{ 3964 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 3965 }, 3966 MediaType: ocispec.MediaTypeImageIndex, 3967 Manifests: []ocispec.Descriptor{ 3968 manifestDesc, 3969 }, 3970 } 3971 3972 // test pushing image manifest with subject, a referrers list should be created 3973 indexJSON_1, err := json.Marshal(index_1) 3974 if err != nil { 3975 t.Fatalf("failed to marshal manifest: %v", err) 3976 } 3977 indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1) 3978 var gotManifest []byte 3979 var gotReferrerIndex []byte 3980 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3981 switch { 3982 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 3983 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 3984 w.WriteHeader(http.StatusBadRequest) 3985 break 3986 } 3987 buf := bytes.NewBuffer(nil) 3988 if _, err := buf.ReadFrom(r.Body); err != nil { 3989 t.Errorf("fail to read: %v", err) 3990 } 3991 gotManifest = buf.Bytes() 3992 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 3993 w.WriteHeader(http.StatusCreated) 3994 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3995 w.WriteHeader(http.StatusNotFound) 3996 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 3997 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 3998 w.WriteHeader(http.StatusBadRequest) 3999 break 4000 } 4001 buf := bytes.NewBuffer(nil) 4002 if _, err := buf.ReadFrom(r.Body); err != nil { 4003 t.Errorf("fail to read: %v", err) 4004 } 4005 gotReferrerIndex = buf.Bytes() 4006 w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String()) 4007 w.WriteHeader(http.StatusCreated) 4008 default: 4009 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4010 w.WriteHeader(http.StatusNotFound) 4011 } 4012 })) 4013 defer ts.Close() 4014 uri, err := url.Parse(ts.URL) 4015 if err != nil { 4016 t.Fatalf("invalid test http server: %v", err) 4017 } 4018 4019 ctx := context.Background() 4020 repo, err := NewRepository(uri.Host + "/test") 4021 if err != nil { 4022 t.Fatalf("NewRepository() error = %v", err) 4023 } 4024 repo.PlainHTTP = true 4025 repo.SkipReferrersGC = true 4026 4027 if state := repo.loadReferrersState(); state != referrersStateUnknown { 4028 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 4029 } 4030 err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) 4031 if err != nil { 4032 t.Fatalf("Manifests.Push() error = %v", err) 4033 } 4034 if !bytes.Equal(gotManifest, manifestJSON) { 4035 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) 4036 } 4037 if !bytes.Equal(gotReferrerIndex, indexJSON_1) { 4038 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1)) 4039 } 4040 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 4041 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 4042 } 4043 4044 // test pushing image manifest with subject when an old empty referrer list exists, 4045 // the referrer list should be updated 4046 emptyIndex := ocispec.Index{ 4047 Versioned: specs.Versioned{ 4048 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 4049 }, 4050 MediaType: ocispec.MediaTypeImageIndex, 4051 } 4052 emptyIndexJSON, err := json.Marshal(emptyIndex) 4053 if err != nil { 4054 t.Error("failed to marshal index", err) 4055 } 4056 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4057 switch { 4058 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 4059 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 4060 w.WriteHeader(http.StatusBadRequest) 4061 break 4062 } 4063 buf := bytes.NewBuffer(nil) 4064 if _, err := buf.ReadFrom(r.Body); err != nil { 4065 t.Errorf("fail to read: %v", err) 4066 } 4067 gotManifest = buf.Bytes() 4068 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 4069 w.WriteHeader(http.StatusCreated) 4070 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4071 w.Write(emptyIndexJSON) 4072 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4073 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 4074 w.WriteHeader(http.StatusBadRequest) 4075 break 4076 } 4077 buf := bytes.NewBuffer(nil) 4078 if _, err := buf.ReadFrom(r.Body); err != nil { 4079 t.Errorf("fail to read: %v", err) 4080 } 4081 gotReferrerIndex = buf.Bytes() 4082 w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String()) 4083 w.WriteHeader(http.StatusCreated) 4084 default: 4085 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4086 w.WriteHeader(http.StatusNotFound) 4087 } 4088 })) 4089 defer ts.Close() 4090 uri, err = url.Parse(ts.URL) 4091 if err != nil { 4092 t.Fatalf("invalid test http server: %v", err) 4093 } 4094 4095 ctx = context.Background() 4096 repo, err = NewRepository(uri.Host + "/test") 4097 if err != nil { 4098 t.Fatalf("NewRepository() error = %v", err) 4099 } 4100 repo.PlainHTTP = true 4101 repo.SkipReferrersGC = true 4102 4103 if state := repo.loadReferrersState(); state != referrersStateUnknown { 4104 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 4105 } 4106 err = repo.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) 4107 if err != nil { 4108 t.Fatalf("Manifests.Push() error = %v", err) 4109 } 4110 if !bytes.Equal(gotManifest, manifestJSON) { 4111 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) 4112 } 4113 if !bytes.Equal(gotReferrerIndex, indexJSON_1) { 4114 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1)) 4115 } 4116 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 4117 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 4118 } 4119 4120 // push image index with subject, referrer list should be updated, the old 4121 // one should not be deleted 4122 indexManifest := ocispec.Index{ 4123 MediaType: ocispec.MediaTypeImageIndex, 4124 Subject: &subjectDesc, 4125 ArtifactType: "test/index", 4126 Annotations: map[string]string{"foo": "bar"}, 4127 } 4128 indexManifestJSON, err := json.Marshal(indexManifest) 4129 if err != nil { 4130 t.Fatalf("failed to marshal manifest: %v", err) 4131 } 4132 indexManifestDesc := content.NewDescriptorFromBytes(indexManifest.MediaType, indexManifestJSON) 4133 indexManifestDesc.ArtifactType = indexManifest.ArtifactType 4134 indexManifestDesc.Annotations = indexManifest.Annotations 4135 index_2 := ocispec.Index{ 4136 Versioned: specs.Versioned{ 4137 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 4138 }, 4139 MediaType: ocispec.MediaTypeImageIndex, 4140 Manifests: []ocispec.Descriptor{ 4141 manifestDesc, 4142 indexManifestDesc, 4143 }, 4144 } 4145 indexJSON_2, err := json.Marshal(index_2) 4146 if err != nil { 4147 t.Fatalf("failed to marshal manifest: %v", err) 4148 } 4149 indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2) 4150 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4151 switch { 4152 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexManifestDesc.Digest.String(): 4153 if contentType := r.Header.Get("Content-Type"); contentType != indexManifestDesc.MediaType { 4154 w.WriteHeader(http.StatusBadRequest) 4155 break 4156 } 4157 buf := bytes.NewBuffer(nil) 4158 if _, err := buf.ReadFrom(r.Body); err != nil { 4159 t.Errorf("fail to read: %v", err) 4160 } 4161 gotManifest = buf.Bytes() 4162 w.Header().Set("Docker-Content-Digest", indexManifestDesc.Digest.String()) 4163 w.WriteHeader(http.StatusCreated) 4164 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4165 w.Write(indexJSON_1) 4166 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4167 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 4168 w.WriteHeader(http.StatusBadRequest) 4169 break 4170 } 4171 buf := bytes.NewBuffer(nil) 4172 if _, err := buf.ReadFrom(r.Body); err != nil { 4173 t.Errorf("fail to read: %v", err) 4174 } 4175 gotReferrerIndex = buf.Bytes() 4176 w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String()) 4177 w.WriteHeader(http.StatusCreated) 4178 default: 4179 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4180 w.WriteHeader(http.StatusNotFound) 4181 } 4182 })) 4183 defer ts.Close() 4184 uri, err = url.Parse(ts.URL) 4185 if err != nil { 4186 t.Fatalf("invalid test http server: %v", err) 4187 } 4188 4189 ctx = context.Background() 4190 repo, err = NewRepository(uri.Host + "/test") 4191 if err != nil { 4192 t.Fatalf("NewRepository() error = %v", err) 4193 } 4194 repo.PlainHTTP = true 4195 repo.SkipReferrersGC = true 4196 4197 if state := repo.loadReferrersState(); state != referrersStateUnknown { 4198 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 4199 } 4200 err = repo.Push(ctx, indexManifestDesc, bytes.NewReader(indexManifestJSON)) 4201 if err != nil { 4202 t.Fatalf("Manifests.Push() error = %v", err) 4203 } 4204 if !bytes.Equal(gotManifest, indexManifestJSON) { 4205 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(indexManifestJSON)) 4206 } 4207 if !bytes.Equal(gotReferrerIndex, indexJSON_2) { 4208 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) 4209 } 4210 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 4211 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 4212 } 4213 } 4214 4215 func Test_ManifestStore_Exists(t *testing.T) { 4216 manifest := []byte(`{"layers":[]}`) 4217 manifestDesc := ocispec.Descriptor{ 4218 MediaType: ocispec.MediaTypeImageManifest, 4219 Digest: digest.FromBytes(manifest), 4220 Size: int64(len(manifest)), 4221 } 4222 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4223 if r.Method != http.MethodHead { 4224 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4225 w.WriteHeader(http.StatusMethodNotAllowed) 4226 return 4227 } 4228 switch r.URL.Path { 4229 case "/v2/test/manifests/" + manifestDesc.Digest.String(): 4230 if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { 4231 t.Errorf("manifest not convertable: %s", accept) 4232 w.WriteHeader(http.StatusBadRequest) 4233 return 4234 } 4235 w.Header().Set("Content-Type", manifestDesc.MediaType) 4236 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 4237 w.Header().Set("Content-Length", strconv.Itoa(int(manifestDesc.Size))) 4238 default: 4239 w.WriteHeader(http.StatusNotFound) 4240 } 4241 })) 4242 defer ts.Close() 4243 uri, err := url.Parse(ts.URL) 4244 if err != nil { 4245 t.Fatalf("invalid test http server: %v", err) 4246 } 4247 4248 repo, err := NewRepository(uri.Host + "/test") 4249 if err != nil { 4250 t.Fatalf("NewRepository() error = %v", err) 4251 } 4252 repo.PlainHTTP = true 4253 store := repo.Manifests() 4254 ctx := context.Background() 4255 4256 exists, err := store.Exists(ctx, manifestDesc) 4257 if err != nil { 4258 t.Fatalf("Manifests.Exists() error = %v", err) 4259 } 4260 if !exists { 4261 t.Errorf("Manifests.Exists() = %v, want %v", exists, true) 4262 } 4263 4264 content := []byte(`{"manifests":[]}`) 4265 contentDesc := ocispec.Descriptor{ 4266 MediaType: ocispec.MediaTypeImageIndex, 4267 Digest: digest.FromBytes(content), 4268 Size: int64(len(content)), 4269 } 4270 exists, err = store.Exists(ctx, contentDesc) 4271 if err != nil { 4272 t.Fatalf("Manifests.Exists() error = %v", err) 4273 } 4274 if exists { 4275 t.Errorf("Manifests.Exists() = %v, want %v", exists, false) 4276 } 4277 } 4278 4279 func Test_ManifestStore_Delete(t *testing.T) { 4280 manifest := []byte(`{"layers":[]}`) 4281 manifestDesc := ocispec.Descriptor{ 4282 MediaType: ocispec.MediaTypeImageManifest, 4283 Digest: digest.FromBytes(manifest), 4284 Size: int64(len(manifest)), 4285 } 4286 manifestDeleted := false 4287 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4288 if r.Method != http.MethodDelete && r.Method != http.MethodGet { 4289 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4290 w.WriteHeader(http.StatusMethodNotAllowed) 4291 } 4292 switch { 4293 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 4294 manifestDeleted = true 4295 // no "Docker-Content-Digest" header for manifest deletion 4296 w.WriteHeader(http.StatusAccepted) 4297 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 4298 if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { 4299 t.Errorf("manifest not convertable: %s", accept) 4300 w.WriteHeader(http.StatusBadRequest) 4301 return 4302 } 4303 w.Header().Set("Content-Type", manifestDesc.MediaType) 4304 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 4305 if _, err := w.Write(manifest); err != nil { 4306 t.Errorf("failed to write %q: %v", r.URL, err) 4307 } 4308 default: 4309 w.WriteHeader(http.StatusNotFound) 4310 } 4311 })) 4312 defer ts.Close() 4313 uri, err := url.Parse(ts.URL) 4314 if err != nil { 4315 t.Fatalf("invalid test http server: %v", err) 4316 } 4317 4318 repo, err := NewRepository(uri.Host + "/test") 4319 if err != nil { 4320 t.Fatalf("NewRepository() error = %v", err) 4321 } 4322 repo.PlainHTTP = true 4323 store := repo.Manifests() 4324 ctx := context.Background() 4325 4326 // test deleting manifest without subject 4327 err = store.Delete(ctx, manifestDesc) 4328 if err != nil { 4329 t.Fatalf("Manifests.Delete() error = %v", err) 4330 } 4331 if !manifestDeleted { 4332 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 4333 } 4334 4335 // test deleting content that does not exist 4336 content := []byte(`{"manifests":[]}`) 4337 contentDesc := ocispec.Descriptor{ 4338 MediaType: ocispec.MediaTypeImageIndex, 4339 Digest: digest.FromBytes(content), 4340 Size: int64(len(content)), 4341 } 4342 err = store.Delete(ctx, contentDesc) 4343 if !errors.Is(err, errdef.ErrNotFound) { 4344 t.Errorf("Manifests.Delete() error = %v, wantErr %v", err, errdef.ErrNotFound) 4345 } 4346 } 4347 4348 func Test_ManifestStore_Delete_ReferrersAPIAvailable(t *testing.T) { 4349 // generate test content 4350 subject := []byte(`{"layers":[]}`) 4351 subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject) 4352 artifact := spec.Artifact{ 4353 MediaType: spec.MediaTypeArtifactManifest, 4354 Subject: &subjectDesc, 4355 } 4356 artifactJSON, err := json.Marshal(artifact) 4357 if err != nil { 4358 t.Fatalf("failed to marshal manifest: %v", err) 4359 } 4360 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 4361 4362 manifest := ocispec.Manifest{ 4363 MediaType: ocispec.MediaTypeImageManifest, 4364 Subject: &subjectDesc, 4365 } 4366 manifestJSON, err := json.Marshal(manifest) 4367 if err != nil { 4368 t.Fatalf("failed to marshal manifest: %v", err) 4369 } 4370 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 4371 4372 index := ocispec.Index{ 4373 MediaType: ocispec.MediaTypeImageIndex, 4374 Subject: &subjectDesc, 4375 } 4376 indexJSON, err := json.Marshal(index) 4377 if err != nil { 4378 t.Fatalf("failed to marshal manifest: %v", err) 4379 } 4380 indexDesc := content.NewDescriptorFromBytes(index.MediaType, indexJSON) 4381 4382 var manifestDeleted bool 4383 var artifactDeleted bool 4384 var indexDeleted bool 4385 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4386 if r.Method != http.MethodDelete && r.Method != http.MethodGet { 4387 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4388 w.WriteHeader(http.StatusMethodNotAllowed) 4389 } 4390 switch { 4391 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 4392 artifactDeleted = true 4393 // no "Docker-Content-Digest" header for manifest deletion 4394 w.WriteHeader(http.StatusAccepted) 4395 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 4396 manifestDeleted = true 4397 // no "Docker-Content-Digest" header for manifest deletion 4398 w.WriteHeader(http.StatusAccepted) 4399 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 4400 indexDeleted = true 4401 // no "Docker-Content-Digest" header for manifest deletion 4402 w.WriteHeader(http.StatusAccepted) 4403 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 4404 if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { 4405 t.Errorf("manifest not convertable: %s", accept) 4406 w.WriteHeader(http.StatusBadRequest) 4407 return 4408 } 4409 w.Header().Set("Content-Type", artifactDesc.MediaType) 4410 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 4411 if _, err := w.Write(artifactJSON); err != nil { 4412 t.Errorf("failed to write %q: %v", r.URL, err) 4413 } 4414 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 4415 result := ocispec.Index{ 4416 Versioned: specs.Versioned{ 4417 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 4418 }, 4419 MediaType: ocispec.MediaTypeImageIndex, 4420 Manifests: []ocispec.Descriptor{}, 4421 } 4422 w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex) 4423 if err := json.NewEncoder(w).Encode(result); err != nil { 4424 t.Errorf("failed to write response: %v", err) 4425 } 4426 default: 4427 w.WriteHeader(http.StatusNotFound) 4428 } 4429 })) 4430 defer ts.Close() 4431 uri, err := url.Parse(ts.URL) 4432 if err != nil { 4433 t.Fatalf("invalid test http server: %v", err) 4434 } 4435 repo, err := NewRepository(uri.Host + "/test") 4436 if err != nil { 4437 t.Fatalf("NewRepository() error = %v", err) 4438 } 4439 repo.PlainHTTP = true 4440 store := repo.Manifests() 4441 ctx := context.Background() 4442 // test deleting artifact with subject 4443 if state := repo.loadReferrersState(); state != referrersStateUnknown { 4444 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 4445 } 4446 err = store.Delete(ctx, artifactDesc) 4447 if err != nil { 4448 t.Fatalf("Manifests.Delete() error = %v", err) 4449 } 4450 if !artifactDeleted { 4451 t.Errorf("Manifests.Delete() = %v, want %v", artifactDeleted, true) 4452 } 4453 4454 // test deleting manifest with subject 4455 if state := repo.loadReferrersState(); state != referrersStateSupported { 4456 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 4457 } 4458 err = store.Delete(ctx, manifestDesc) 4459 if err != nil { 4460 t.Fatalf("Manifests.Delete() error = %v", err) 4461 } 4462 if !manifestDeleted { 4463 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 4464 } 4465 4466 // test deleting index with subject 4467 if state := repo.loadReferrersState(); state != referrersStateSupported { 4468 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 4469 } 4470 err = store.Delete(ctx, indexDesc) 4471 if err != nil { 4472 t.Fatalf("Manifests.Delete() error = %v", err) 4473 } 4474 if !indexDeleted { 4475 t.Errorf("Manifests.Delete() = %v, want %v", indexDeleted, true) 4476 } 4477 4478 // test deleting content that does not exist 4479 content := []byte("whatever") 4480 contentDesc := ocispec.Descriptor{ 4481 MediaType: ocispec.MediaTypeImageManifest, 4482 Digest: digest.FromBytes(content), 4483 Size: int64(len(content)), 4484 } 4485 ctx = context.Background() 4486 err = store.Delete(ctx, contentDesc) 4487 if !errors.Is(err, errdef.ErrNotFound) { 4488 t.Errorf("Manifests.Delete() error = %v, wantErr %v", err, errdef.ErrNotFound) 4489 } 4490 } 4491 4492 func Test_ManifestStore_Delete_ReferrersAPIUnavailable(t *testing.T) { 4493 // generate test content 4494 subject := []byte(`{"layers":[]}`) 4495 subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject) 4496 referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) 4497 4498 artifact := spec.Artifact{ 4499 MediaType: spec.MediaTypeArtifactManifest, 4500 Subject: &subjectDesc, 4501 } 4502 artifactJSON, err := json.Marshal(artifact) 4503 if err != nil { 4504 t.Fatalf("failed to marshal manifest: %v", err) 4505 } 4506 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 4507 4508 manifest := ocispec.Manifest{ 4509 MediaType: ocispec.MediaTypeImageManifest, 4510 Subject: &subjectDesc, 4511 } 4512 manifestJSON, err := json.Marshal(manifest) 4513 if err != nil { 4514 t.Fatalf("failed to marshal manifest: %v", err) 4515 } 4516 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 4517 4518 indexManifest := ocispec.Index{ 4519 MediaType: ocispec.MediaTypeImageIndex, 4520 Subject: &subjectDesc, 4521 } 4522 indexManifestJSON, err := json.Marshal(indexManifest) 4523 if err != nil { 4524 t.Fatalf("failed to marshal manifest: %v", err) 4525 } 4526 indexManifestDesc := content.NewDescriptorFromBytes(indexManifest.MediaType, indexManifestJSON) 4527 4528 index_1 := ocispec.Index{ 4529 Versioned: specs.Versioned{ 4530 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 4531 }, 4532 MediaType: ocispec.MediaTypeImageIndex, 4533 Manifests: []ocispec.Descriptor{ 4534 artifactDesc, 4535 manifestDesc, 4536 indexManifestDesc, 4537 }, 4538 } 4539 indexJSON_1, err := json.Marshal(index_1) 4540 if err != nil { 4541 t.Fatalf("failed to marshal manifest: %v", err) 4542 } 4543 indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1) 4544 index_2 := ocispec.Index{ 4545 Versioned: specs.Versioned{ 4546 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 4547 }, 4548 MediaType: ocispec.MediaTypeImageIndex, 4549 Manifests: []ocispec.Descriptor{ 4550 manifestDesc, 4551 indexManifestDesc, 4552 }, 4553 } 4554 indexJSON_2, err := json.Marshal(index_2) 4555 if err != nil { 4556 t.Fatalf("failed to marshal manifest: %v", err) 4557 } 4558 indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2) 4559 index_3 := ocispec.Index{ 4560 Versioned: specs.Versioned{ 4561 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 4562 }, 4563 MediaType: ocispec.MediaTypeImageIndex, 4564 Manifests: []ocispec.Descriptor{ 4565 indexManifestDesc, 4566 }, 4567 } 4568 indexJSON_3, err := json.Marshal(index_3) 4569 if err != nil { 4570 t.Fatalf("failed to marshal manifest: %v", err) 4571 } 4572 indexDesc_3 := content.NewDescriptorFromBytes(index_3.MediaType, indexJSON_3) 4573 4574 // test deleting artifact with subject, referrers list should be updated 4575 manifestDeleted := false 4576 indexDeleted := false 4577 var gotReferrerIndex []byte 4578 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4579 switch { 4580 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 4581 manifestDeleted = true 4582 // no "Docker-Content-Digest" header for manifest deletion 4583 w.WriteHeader(http.StatusAccepted) 4584 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 4585 if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { 4586 t.Errorf("manifest not convertable: %s", accept) 4587 w.WriteHeader(http.StatusBadRequest) 4588 return 4589 } 4590 w.Header().Set("Content-Type", artifactDesc.MediaType) 4591 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 4592 if _, err := w.Write(artifactJSON); err != nil { 4593 t.Errorf("failed to write %q: %v", r.URL, err) 4594 } 4595 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 4596 w.WriteHeader(http.StatusNotFound) 4597 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4598 w.Write(indexJSON_1) 4599 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4600 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 4601 w.WriteHeader(http.StatusBadRequest) 4602 break 4603 } 4604 buf := bytes.NewBuffer(nil) 4605 if _, err := buf.ReadFrom(r.Body); err != nil { 4606 t.Errorf("fail to read: %v", err) 4607 } 4608 gotReferrerIndex = buf.Bytes() 4609 w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String()) 4610 w.WriteHeader(http.StatusCreated) 4611 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_1.Digest.String(): 4612 indexDeleted = true 4613 // no "Docker-Content-Digest" header for manifest deletion 4614 w.WriteHeader(http.StatusAccepted) 4615 default: 4616 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4617 w.WriteHeader(http.StatusNotFound) 4618 } 4619 })) 4620 defer ts.Close() 4621 uri, err := url.Parse(ts.URL) 4622 if err != nil { 4623 t.Fatalf("invalid test http server: %v", err) 4624 } 4625 repo, err := NewRepository(uri.Host + "/test") 4626 if err != nil { 4627 t.Fatalf("NewRepository() error = %v", err) 4628 } 4629 repo.PlainHTTP = true 4630 store := repo.Manifests() 4631 ctx := context.Background() 4632 4633 if state := repo.loadReferrersState(); state != referrersStateUnknown { 4634 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 4635 } 4636 err = store.Delete(ctx, artifactDesc) 4637 if err != nil { 4638 t.Fatalf("Manifests.Delete() error = %v", err) 4639 } 4640 if !manifestDeleted { 4641 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 4642 } 4643 if !bytes.Equal(gotReferrerIndex, indexJSON_2) { 4644 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) 4645 } 4646 if !indexDeleted { 4647 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 4648 } 4649 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 4650 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 4651 } 4652 4653 // test deleting manifest with subject, referrers list should be updated 4654 manifestDeleted = false 4655 indexDeleted = false 4656 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4657 switch { 4658 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 4659 manifestDeleted = true 4660 // no "Docker-Content-Digest" header for manifest deletion 4661 w.WriteHeader(http.StatusAccepted) 4662 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 4663 if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { 4664 t.Errorf("manifest not convertable: %s", accept) 4665 w.WriteHeader(http.StatusBadRequest) 4666 return 4667 } 4668 w.Header().Set("Content-Type", manifestDesc.MediaType) 4669 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 4670 if _, err := w.Write(manifestJSON); err != nil { 4671 t.Errorf("failed to write %q: %v", r.URL, err) 4672 } 4673 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 4674 w.WriteHeader(http.StatusNotFound) 4675 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4676 w.Write(indexJSON_2) 4677 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4678 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 4679 w.WriteHeader(http.StatusBadRequest) 4680 break 4681 } 4682 buf := bytes.NewBuffer(nil) 4683 if _, err := buf.ReadFrom(r.Body); err != nil { 4684 t.Errorf("fail to read: %v", err) 4685 } 4686 gotReferrerIndex = buf.Bytes() 4687 w.Header().Set("Docker-Content-Digest", indexDesc_3.Digest.String()) 4688 w.WriteHeader(http.StatusCreated) 4689 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_2.Digest.String(): 4690 indexDeleted = true 4691 // no "Docker-Content-Digest" header for manifest deletion 4692 w.WriteHeader(http.StatusAccepted) 4693 default: 4694 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4695 w.WriteHeader(http.StatusNotFound) 4696 } 4697 })) 4698 defer ts.Close() 4699 uri, err = url.Parse(ts.URL) 4700 if err != nil { 4701 t.Fatalf("invalid test http server: %v", err) 4702 } 4703 repo, err = NewRepository(uri.Host + "/test") 4704 if err != nil { 4705 t.Fatalf("NewRepository() error = %v", err) 4706 } 4707 repo.PlainHTTP = true 4708 store = repo.Manifests() 4709 ctx = context.Background() 4710 if state := repo.loadReferrersState(); state != referrersStateUnknown { 4711 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 4712 } 4713 err = store.Delete(ctx, manifestDesc) 4714 if err != nil { 4715 t.Fatalf("Manifests.Delete() error = %v", err) 4716 } 4717 if !manifestDeleted { 4718 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 4719 } 4720 if !indexDeleted { 4721 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 4722 } 4723 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 4724 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 4725 } 4726 4727 // test deleting index with a subject, referrers list should be updated 4728 manifestDeleted = false 4729 indexDeleted = false 4730 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4731 switch { 4732 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexManifestDesc.Digest.String(): 4733 manifestDeleted = true 4734 // no "Docker-Content-Digest" header for manifest deletion 4735 w.WriteHeader(http.StatusAccepted) 4736 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexManifestDesc.Digest.String(): 4737 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexManifestDesc.MediaType) { 4738 t.Errorf("manifest not convertable: %s", accept) 4739 w.WriteHeader(http.StatusBadRequest) 4740 return 4741 } 4742 w.Header().Set("Content-Type", indexManifestDesc.MediaType) 4743 w.Header().Set("Docker-Content-Digest", indexManifestDesc.Digest.String()) 4744 if _, err := w.Write(indexManifestJSON); err != nil { 4745 t.Errorf("failed to write %q: %v", r.URL, err) 4746 } 4747 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 4748 w.WriteHeader(http.StatusNotFound) 4749 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4750 w.Write(indexJSON_3) 4751 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_3.Digest.String(): 4752 indexDeleted = true 4753 // no "Docker-Content-Digest" header for manifest deletion 4754 w.WriteHeader(http.StatusAccepted) 4755 default: 4756 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4757 w.WriteHeader(http.StatusNotFound) 4758 } 4759 })) 4760 defer ts.Close() 4761 uri, err = url.Parse(ts.URL) 4762 if err != nil { 4763 t.Fatalf("invalid test http server: %v", err) 4764 } 4765 repo, err = NewRepository(uri.Host + "/test") 4766 if err != nil { 4767 t.Fatalf("NewRepository() error = %v", err) 4768 } 4769 repo.PlainHTTP = true 4770 store = repo.Manifests() 4771 ctx = context.Background() 4772 if state := repo.loadReferrersState(); state != referrersStateUnknown { 4773 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 4774 } 4775 err = store.Delete(ctx, indexManifestDesc) 4776 if err != nil { 4777 t.Fatalf("Manifests.Delete() error = %v", err) 4778 } 4779 if !manifestDeleted { 4780 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 4781 } 4782 if !indexDeleted { 4783 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 4784 } 4785 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 4786 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 4787 } 4788 } 4789 4790 func Test_ManifestStore_Delete_ReferrersAPIUnavailable_SkipReferrersGC(t *testing.T) { 4791 // generate test content 4792 subject := []byte(`{"layers":[]}`) 4793 subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject) 4794 referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) 4795 4796 manifest := ocispec.Manifest{ 4797 MediaType: ocispec.MediaTypeImageManifest, 4798 Subject: &subjectDesc, 4799 } 4800 manifestJSON, err := json.Marshal(manifest) 4801 if err != nil { 4802 t.Fatalf("failed to marshal manifest: %v", err) 4803 } 4804 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 4805 4806 indexManifest := ocispec.Index{ 4807 MediaType: ocispec.MediaTypeImageIndex, 4808 Subject: &subjectDesc, 4809 } 4810 indexManifestJSON, err := json.Marshal(indexManifest) 4811 if err != nil { 4812 t.Fatalf("failed to marshal manifest: %v", err) 4813 } 4814 indexManifestDesc := content.NewDescriptorFromBytes(indexManifest.MediaType, indexManifestJSON) 4815 4816 index_1 := ocispec.Index{ 4817 Versioned: specs.Versioned{ 4818 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 4819 }, 4820 MediaType: ocispec.MediaTypeImageIndex, 4821 Manifests: []ocispec.Descriptor{ 4822 manifestDesc, 4823 indexManifestDesc, 4824 }, 4825 } 4826 indexJSON_1, err := json.Marshal(index_1) 4827 if err != nil { 4828 t.Fatalf("failed to marshal manifest: %v", err) 4829 } 4830 index_2 := ocispec.Index{ 4831 Versioned: specs.Versioned{ 4832 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 4833 }, 4834 MediaType: ocispec.MediaTypeImageIndex, 4835 Manifests: []ocispec.Descriptor{ 4836 indexManifestDesc, 4837 }, 4838 } 4839 indexJSON_2, err := json.Marshal(index_2) 4840 if err != nil { 4841 t.Fatalf("failed to marshal manifest: %v", err) 4842 } 4843 indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2) 4844 index_3 := ocispec.Index{ 4845 Versioned: specs.Versioned{ 4846 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 4847 }, 4848 MediaType: ocispec.MediaTypeImageIndex, 4849 Manifests: []ocispec.Descriptor{}, 4850 } 4851 indexJSON_3, err := json.Marshal(index_3) 4852 if err != nil { 4853 t.Fatalf("failed to marshal manifest: %v", err) 4854 } 4855 indexDesc_3 := content.NewDescriptorFromBytes(index_3.MediaType, indexJSON_3) 4856 4857 // test deleting image manifest with subject, referrers list should be updated, 4858 // the old one should not be deleted 4859 manifestDeleted := false 4860 var gotReferrerIndex []byte 4861 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4862 switch { 4863 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 4864 manifestDeleted = true 4865 // no "Docker-Content-Digest" header for manifest deletion 4866 w.WriteHeader(http.StatusAccepted) 4867 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+manifestDesc.Digest.String(): 4868 if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { 4869 t.Errorf("manifest not convertable: %s", accept) 4870 w.WriteHeader(http.StatusBadRequest) 4871 return 4872 } 4873 w.Header().Set("Content-Type", manifestDesc.MediaType) 4874 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 4875 if _, err := w.Write(manifestJSON); err != nil { 4876 t.Errorf("failed to write %q: %v", r.URL, err) 4877 } 4878 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 4879 w.WriteHeader(http.StatusNotFound) 4880 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4881 w.Write(indexJSON_1) 4882 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4883 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 4884 w.WriteHeader(http.StatusBadRequest) 4885 break 4886 } 4887 buf := bytes.NewBuffer(nil) 4888 if _, err := buf.ReadFrom(r.Body); err != nil { 4889 t.Errorf("fail to read: %v", err) 4890 } 4891 gotReferrerIndex = buf.Bytes() 4892 w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String()) 4893 w.WriteHeader(http.StatusCreated) 4894 default: 4895 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4896 w.WriteHeader(http.StatusNotFound) 4897 } 4898 })) 4899 defer ts.Close() 4900 uri, err := url.Parse(ts.URL) 4901 if err != nil { 4902 t.Fatalf("invalid test http server: %v", err) 4903 } 4904 repo, err := NewRepository(uri.Host + "/test") 4905 if err != nil { 4906 t.Fatalf("NewRepository() error = %v", err) 4907 } 4908 repo.PlainHTTP = true 4909 repo.SkipReferrersGC = true 4910 store := repo.Manifests() 4911 ctx := context.Background() 4912 4913 if state := repo.loadReferrersState(); state != referrersStateUnknown { 4914 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 4915 } 4916 err = store.Delete(ctx, manifestDesc) 4917 if err != nil { 4918 t.Fatalf("Manifests.Delete() error = %v", err) 4919 } 4920 if !manifestDeleted { 4921 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 4922 } 4923 if !bytes.Equal(gotReferrerIndex, indexJSON_2) { 4924 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) 4925 } 4926 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 4927 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 4928 } 4929 4930 // test deleting index with a subject, referrers list should be updated, 4931 // the old one should not be deleted, an empty one should be pushed 4932 manifestDeleted = false 4933 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4934 switch { 4935 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexManifestDesc.Digest.String(): 4936 manifestDeleted = true 4937 // no "Docker-Content-Digest" header for manifest deletion 4938 w.WriteHeader(http.StatusAccepted) 4939 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexManifestDesc.Digest.String(): 4940 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexManifestDesc.MediaType) { 4941 t.Errorf("manifest not convertable: %s", accept) 4942 w.WriteHeader(http.StatusBadRequest) 4943 return 4944 } 4945 w.Header().Set("Content-Type", indexManifestDesc.MediaType) 4946 w.Header().Set("Docker-Content-Digest", indexManifestDesc.Digest.String()) 4947 if _, err := w.Write(indexManifestJSON); err != nil { 4948 t.Errorf("failed to write %q: %v", r.URL, err) 4949 } 4950 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 4951 w.WriteHeader(http.StatusNotFound) 4952 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4953 w.Write(indexJSON_2) 4954 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 4955 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 4956 w.WriteHeader(http.StatusBadRequest) 4957 break 4958 } 4959 buf := bytes.NewBuffer(nil) 4960 if _, err := buf.ReadFrom(r.Body); err != nil { 4961 t.Errorf("fail to read: %v", err) 4962 } 4963 gotReferrerIndex = buf.Bytes() 4964 w.Header().Set("Docker-Content-Digest", indexDesc_3.Digest.String()) 4965 w.WriteHeader(http.StatusCreated) 4966 default: 4967 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 4968 w.WriteHeader(http.StatusNotFound) 4969 } 4970 })) 4971 defer ts.Close() 4972 uri, err = url.Parse(ts.URL) 4973 if err != nil { 4974 t.Fatalf("invalid test http server: %v", err) 4975 } 4976 repo, err = NewRepository(uri.Host + "/test") 4977 if err != nil { 4978 t.Fatalf("NewRepository() error = %v", err) 4979 } 4980 repo.PlainHTTP = true 4981 repo.SkipReferrersGC = true 4982 store = repo.Manifests() 4983 ctx = context.Background() 4984 4985 if state := repo.loadReferrersState(); state != referrersStateUnknown { 4986 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 4987 } 4988 err = store.Delete(ctx, indexManifestDesc) 4989 if err != nil { 4990 t.Fatalf("Manifests.Delete() error = %v", err) 4991 } 4992 if !manifestDeleted { 4993 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 4994 } 4995 if !bytes.Equal(gotReferrerIndex, indexJSON_3) { 4996 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_3)) 4997 } 4998 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 4999 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 5000 } 5001 } 5002 5003 func Test_ManifestStore_Delete_ReferrersAPIUnavailable_InconsistentIndex(t *testing.T) { 5004 // generate test content 5005 subject := []byte(`{"layers":[]}`) 5006 subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject) 5007 referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) 5008 artifact := spec.Artifact{ 5009 MediaType: spec.MediaTypeArtifactManifest, 5010 Subject: &subjectDesc, 5011 } 5012 artifactJSON, err := json.Marshal(artifact) 5013 if err != nil { 5014 t.Fatalf("failed to marshal manifest: %v", err) 5015 } 5016 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 5017 5018 // test inconsistent state: index not found 5019 manifestDeleted := true 5020 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5021 switch { 5022 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 5023 manifestDeleted = true 5024 // no "Docker-Content-Digest" header for manifest deletion 5025 w.WriteHeader(http.StatusAccepted) 5026 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 5027 if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { 5028 t.Errorf("manifest not convertable: %s", accept) 5029 w.WriteHeader(http.StatusBadRequest) 5030 return 5031 } 5032 w.Header().Set("Content-Type", artifactDesc.MediaType) 5033 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 5034 if _, err := w.Write(artifactJSON); err != nil { 5035 t.Errorf("failed to write %q: %v", r.URL, err) 5036 } 5037 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 5038 w.WriteHeader(http.StatusNotFound) 5039 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 5040 w.WriteHeader(http.StatusNotFound) 5041 default: 5042 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5043 w.WriteHeader(http.StatusNotFound) 5044 } 5045 })) 5046 defer ts.Close() 5047 uri, err := url.Parse(ts.URL) 5048 if err != nil { 5049 t.Fatalf("invalid test http server: %v", err) 5050 } 5051 repo, err := NewRepository(uri.Host + "/test") 5052 if err != nil { 5053 t.Fatalf("NewRepository() error = %v", err) 5054 } 5055 repo.PlainHTTP = true 5056 store := repo.Manifests() 5057 ctx := context.Background() 5058 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5059 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5060 } 5061 err = store.Delete(ctx, artifactDesc) 5062 if err != nil { 5063 t.Fatalf("Manifests.Delete() error = %v", err) 5064 } 5065 if !manifestDeleted { 5066 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 5067 } 5068 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 5069 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 5070 } 5071 5072 // test inconsistent state: empty referrers list 5073 manifestDeleted = true 5074 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5075 switch { 5076 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 5077 manifestDeleted = true 5078 // no "Docker-Content-Digest" header for manifest deletion 5079 w.WriteHeader(http.StatusAccepted) 5080 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 5081 if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { 5082 t.Errorf("manifest not convertable: %s", accept) 5083 w.WriteHeader(http.StatusBadRequest) 5084 return 5085 } 5086 w.Header().Set("Content-Type", artifactDesc.MediaType) 5087 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 5088 if _, err := w.Write(artifactJSON); err != nil { 5089 t.Errorf("failed to write %q: %v", r.URL, err) 5090 } 5091 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 5092 w.WriteHeader(http.StatusNotFound) 5093 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 5094 result := ocispec.Index{ 5095 Versioned: specs.Versioned{ 5096 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 5097 }, 5098 MediaType: ocispec.MediaTypeImageIndex, 5099 Manifests: []ocispec.Descriptor{}, 5100 } 5101 if err := json.NewEncoder(w).Encode(result); err != nil { 5102 t.Errorf("failed to write response: %v", err) 5103 } 5104 default: 5105 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5106 w.WriteHeader(http.StatusNotFound) 5107 } 5108 })) 5109 defer ts.Close() 5110 uri, err = url.Parse(ts.URL) 5111 if err != nil { 5112 t.Fatalf("invalid test http server: %v", err) 5113 } 5114 repo, err = NewRepository(uri.Host + "/test") 5115 if err != nil { 5116 t.Fatalf("NewRepository() error = %v", err) 5117 } 5118 repo.PlainHTTP = true 5119 store = repo.Manifests() 5120 ctx = context.Background() 5121 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5122 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5123 } 5124 err = store.Delete(ctx, artifactDesc) 5125 if err != nil { 5126 t.Fatalf("Manifests.Delete() error = %v", err) 5127 } 5128 if !manifestDeleted { 5129 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 5130 } 5131 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 5132 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 5133 } 5134 5135 // test inconsistent state: current referrer is not in referrers list 5136 manifestDeleted = true 5137 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5138 switch { 5139 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 5140 manifestDeleted = true 5141 // no "Docker-Content-Digest" header for manifest deletion 5142 w.WriteHeader(http.StatusAccepted) 5143 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+artifactDesc.Digest.String(): 5144 if accept := r.Header.Get("Accept"); !strings.Contains(accept, artifactDesc.MediaType) { 5145 t.Errorf("manifest not convertable: %s", accept) 5146 w.WriteHeader(http.StatusBadRequest) 5147 return 5148 } 5149 w.Header().Set("Content-Type", artifactDesc.MediaType) 5150 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 5151 if _, err := w.Write(artifactJSON); err != nil { 5152 t.Errorf("failed to write %q: %v", r.URL, err) 5153 } 5154 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 5155 w.WriteHeader(http.StatusNotFound) 5156 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 5157 result := ocispec.Index{ 5158 Versioned: specs.Versioned{ 5159 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 5160 }, 5161 MediaType: ocispec.MediaTypeImageIndex, 5162 Manifests: []ocispec.Descriptor{ 5163 content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, []byte("whaterver")), 5164 }, 5165 } 5166 if err := json.NewEncoder(w).Encode(result); err != nil { 5167 t.Errorf("failed to write response: %v", err) 5168 } 5169 default: 5170 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5171 w.WriteHeader(http.StatusNotFound) 5172 } 5173 })) 5174 defer ts.Close() 5175 uri, err = url.Parse(ts.URL) 5176 if err != nil { 5177 t.Fatalf("invalid test http server: %v", err) 5178 } 5179 repo, err = NewRepository(uri.Host + "/test") 5180 if err != nil { 5181 t.Fatalf("NewRepository() error = %v", err) 5182 } 5183 repo.PlainHTTP = true 5184 store = repo.Manifests() 5185 ctx = context.Background() 5186 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5187 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5188 } 5189 err = store.Delete(ctx, artifactDesc) 5190 if err != nil { 5191 t.Fatalf("Manifests.Delete() error = %v", err) 5192 } 5193 if !manifestDeleted { 5194 t.Errorf("Manifests.Delete() = %v, want %v", manifestDeleted, true) 5195 } 5196 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 5197 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 5198 } 5199 } 5200 5201 func Test_ManifestStore_Resolve(t *testing.T) { 5202 manifest := []byte(`{"layers":[]}`) 5203 manifestDesc := ocispec.Descriptor{ 5204 MediaType: ocispec.MediaTypeImageIndex, 5205 Digest: digest.FromBytes(manifest), 5206 Size: int64(len(manifest)), 5207 } 5208 ref := "foobar" 5209 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5210 if r.Method != http.MethodHead { 5211 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5212 w.WriteHeader(http.StatusMethodNotAllowed) 5213 return 5214 } 5215 switch r.URL.Path { 5216 case "/v2/test/manifests/" + manifestDesc.Digest.String(), 5217 "/v2/test/manifests/" + ref: 5218 if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { 5219 t.Errorf("manifest not convertable: %s", accept) 5220 w.WriteHeader(http.StatusBadRequest) 5221 return 5222 } 5223 w.Header().Set("Content-Type", manifestDesc.MediaType) 5224 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 5225 w.Header().Set("Content-Length", strconv.Itoa(int(manifestDesc.Size))) 5226 default: 5227 w.WriteHeader(http.StatusNotFound) 5228 } 5229 })) 5230 defer ts.Close() 5231 uri, err := url.Parse(ts.URL) 5232 if err != nil { 5233 t.Fatalf("invalid test http server: %v", err) 5234 } 5235 5236 repoName := uri.Host + "/test" 5237 repo, err := NewRepository(repoName) 5238 if err != nil { 5239 t.Fatalf("NewRepository() error = %v", err) 5240 } 5241 repo.PlainHTTP = true 5242 store := repo.Manifests() 5243 ctx := context.Background() 5244 5245 got, err := store.Resolve(ctx, manifestDesc.Digest.String()) 5246 if err != nil { 5247 t.Fatalf("Manifests.Resolve() error = %v", err) 5248 } 5249 if !reflect.DeepEqual(got, manifestDesc) { 5250 t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) 5251 } 5252 5253 got, err = store.Resolve(ctx, ref) 5254 if err != nil { 5255 t.Fatalf("Manifests.Resolve() error = %v", err) 5256 } 5257 if !reflect.DeepEqual(got, manifestDesc) { 5258 t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) 5259 } 5260 5261 tagDigestRef := "whatever" + "@" + manifestDesc.Digest.String() 5262 got, err = repo.Resolve(ctx, tagDigestRef) 5263 if err != nil { 5264 t.Fatalf("Manifests.Resolve() error = %v", err) 5265 } 5266 if !reflect.DeepEqual(got, manifestDesc) { 5267 t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) 5268 } 5269 5270 fqdnRef := repoName + ":" + tagDigestRef 5271 got, err = repo.Resolve(ctx, fqdnRef) 5272 if err != nil { 5273 t.Fatalf("Manifests.Resolve() error = %v", err) 5274 } 5275 if !reflect.DeepEqual(got, manifestDesc) { 5276 t.Errorf("Manifests.Resolve() = %v, want %v", got, manifestDesc) 5277 } 5278 5279 content := []byte(`{"manifests":[]}`) 5280 contentDesc := ocispec.Descriptor{ 5281 MediaType: ocispec.MediaTypeImageIndex, 5282 Digest: digest.FromBytes(content), 5283 Size: int64(len(content)), 5284 } 5285 _, err = store.Resolve(ctx, contentDesc.Digest.String()) 5286 if !errors.Is(err, errdef.ErrNotFound) { 5287 t.Errorf("Manifests.Resolve() error = %v, wantErr %v", err, errdef.ErrNotFound) 5288 } 5289 } 5290 5291 func Test_ManifestStore_FetchReference(t *testing.T) { 5292 manifest := []byte(`{"layers":[]}`) 5293 manifestDesc := ocispec.Descriptor{ 5294 MediaType: ocispec.MediaTypeImageIndex, 5295 Digest: digest.FromBytes(manifest), 5296 Size: int64(len(manifest)), 5297 } 5298 ref := "foobar" 5299 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5300 if r.Method != http.MethodGet { 5301 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5302 w.WriteHeader(http.StatusMethodNotAllowed) 5303 return 5304 } 5305 switch r.URL.Path { 5306 case "/v2/test/manifests/" + manifestDesc.Digest.String(), 5307 "/v2/test/manifests/" + ref: 5308 if accept := r.Header.Get("Accept"); !strings.Contains(accept, manifestDesc.MediaType) { 5309 t.Errorf("manifest not convertable: %s", accept) 5310 w.WriteHeader(http.StatusBadRequest) 5311 return 5312 } 5313 w.Header().Set("Content-Type", manifestDesc.MediaType) 5314 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 5315 if _, err := w.Write(manifest); err != nil { 5316 t.Errorf("failed to write %q: %v", r.URL, err) 5317 } 5318 default: 5319 w.WriteHeader(http.StatusNotFound) 5320 } 5321 })) 5322 defer ts.Close() 5323 uri, err := url.Parse(ts.URL) 5324 if err != nil { 5325 t.Fatalf("invalid test http server: %v", err) 5326 } 5327 5328 repoName := uri.Host + "/test" 5329 repo, err := NewRepository(repoName) 5330 if err != nil { 5331 t.Fatalf("NewRepository() error = %v", err) 5332 } 5333 repo.PlainHTTP = true 5334 store := repo.Manifests() 5335 ctx := context.Background() 5336 5337 // test with tag 5338 gotDesc, rc, err := store.FetchReference(ctx, ref) 5339 if err != nil { 5340 t.Fatalf("Manifests.FetchReference() error = %v", err) 5341 } 5342 if !reflect.DeepEqual(gotDesc, manifestDesc) { 5343 t.Errorf("Manifests.FetchReference() = %v, want %v", gotDesc, manifestDesc) 5344 } 5345 buf := bytes.NewBuffer(nil) 5346 if _, err := buf.ReadFrom(rc); err != nil { 5347 t.Errorf("fail to read: %v", err) 5348 } 5349 if err := rc.Close(); err != nil { 5350 t.Errorf("fail to close: %v", err) 5351 } 5352 if got := buf.Bytes(); !bytes.Equal(got, manifest) { 5353 t.Errorf("Manifests.FetchReference() = %v, want %v", got, manifest) 5354 } 5355 5356 // test with other tag 5357 randomRef := "whatever" 5358 _, _, err = store.FetchReference(ctx, randomRef) 5359 if !errors.Is(err, errdef.ErrNotFound) { 5360 t.Errorf("Manifests.FetchReference() error = %v, wantErr %v", err, errdef.ErrNotFound) 5361 } 5362 5363 // test with digest 5364 gotDesc, rc, err = store.FetchReference(ctx, manifestDesc.Digest.String()) 5365 if err != nil { 5366 t.Fatalf("Manifests.FetchReference() error = %v", err) 5367 } 5368 if !reflect.DeepEqual(gotDesc, manifestDesc) { 5369 t.Errorf("Manifests.FetchReference() = %v, want %v", gotDesc, manifestDesc) 5370 } 5371 buf.Reset() 5372 if _, err := buf.ReadFrom(rc); err != nil { 5373 t.Errorf("fail to read: %v", err) 5374 } 5375 if err := rc.Close(); err != nil { 5376 t.Errorf("fail to close: %v", err) 5377 } 5378 if got := buf.Bytes(); !bytes.Equal(got, manifest) { 5379 t.Errorf("Manifests.FetchReference() = %v, want %v", got, manifest) 5380 } 5381 5382 // test with other digest 5383 randomContent := []byte("whatever") 5384 randomContentDigest := digest.FromBytes(randomContent) 5385 _, _, err = store.FetchReference(ctx, randomContentDigest.String()) 5386 if !errors.Is(err, errdef.ErrNotFound) { 5387 t.Errorf("Manifests.FetchReference() error = %v, wantErr %v", err, errdef.ErrNotFound) 5388 } 5389 5390 // test with tag@digest 5391 tagDigestRef := randomRef + "@" + manifestDesc.Digest.String() 5392 gotDesc, rc, err = store.FetchReference(ctx, tagDigestRef) 5393 if err != nil { 5394 t.Fatalf("Manifests.FetchReference() error = %v", err) 5395 } 5396 if !reflect.DeepEqual(gotDesc, manifestDesc) { 5397 t.Errorf("Manifests.FetchReference() = %v, want %v", gotDesc, manifestDesc) 5398 } 5399 buf.Reset() 5400 if _, err := buf.ReadFrom(rc); err != nil { 5401 t.Errorf("fail to read: %v", err) 5402 } 5403 if err := rc.Close(); err != nil { 5404 t.Errorf("fail to close: %v", err) 5405 } 5406 if got := buf.Bytes(); !bytes.Equal(got, manifest) { 5407 t.Errorf("Manifests.FetchReference() = %v, want %v", got, manifest) 5408 } 5409 5410 // test with FQDN 5411 fqdnRef := repoName + ":" + tagDigestRef 5412 gotDesc, rc, err = store.FetchReference(ctx, fqdnRef) 5413 if err != nil { 5414 t.Fatalf("Manifests.FetchReference() error = %v", err) 5415 } 5416 if !reflect.DeepEqual(gotDesc, manifestDesc) { 5417 t.Errorf("Manifests.FetchReference() = %v, want %v", gotDesc, manifestDesc) 5418 } 5419 buf.Reset() 5420 if _, err := buf.ReadFrom(rc); err != nil { 5421 t.Errorf("fail to read: %v", err) 5422 } 5423 if err := rc.Close(); err != nil { 5424 t.Errorf("fail to close: %v", err) 5425 } 5426 if got := buf.Bytes(); !bytes.Equal(got, manifest) { 5427 t.Errorf("Manifests.FetchReference() = %v, want %v", got, manifest) 5428 } 5429 } 5430 5431 func Test_ManifestStore_Tag(t *testing.T) { 5432 blob := []byte("hello world") 5433 blobDesc := ocispec.Descriptor{ 5434 MediaType: "test", 5435 Digest: digest.FromBytes(blob), 5436 Size: int64(len(blob)), 5437 } 5438 index := []byte(`{"manifests":[]}`) 5439 indexDesc := ocispec.Descriptor{ 5440 MediaType: ocispec.MediaTypeImageIndex, 5441 Digest: digest.FromBytes(index), 5442 Size: int64(len(index)), 5443 } 5444 var gotIndex []byte 5445 ref := "foobar" 5446 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5447 switch { 5448 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+blobDesc.Digest.String(): 5449 w.WriteHeader(http.StatusNotFound) 5450 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 5451 if accept := r.Header.Get("Accept"); !strings.Contains(accept, indexDesc.MediaType) { 5452 t.Errorf("manifest not convertable: %s", accept) 5453 w.WriteHeader(http.StatusBadRequest) 5454 return 5455 } 5456 w.Header().Set("Content-Type", indexDesc.MediaType) 5457 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 5458 if _, err := w.Write(index); err != nil { 5459 t.Errorf("failed to write %q: %v", r.URL, err) 5460 } 5461 case r.Method == http.MethodPut && 5462 r.URL.Path == "/v2/test/manifests/"+ref || r.URL.Path == "/v2/test/manifests/"+indexDesc.Digest.String(): 5463 if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { 5464 w.WriteHeader(http.StatusBadRequest) 5465 break 5466 } 5467 buf := bytes.NewBuffer(nil) 5468 if _, err := buf.ReadFrom(r.Body); err != nil { 5469 t.Errorf("fail to read: %v", err) 5470 } 5471 gotIndex = buf.Bytes() 5472 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 5473 w.WriteHeader(http.StatusCreated) 5474 return 5475 default: 5476 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5477 w.WriteHeader(http.StatusForbidden) 5478 } 5479 })) 5480 defer ts.Close() 5481 uri, err := url.Parse(ts.URL) 5482 if err != nil { 5483 t.Fatalf("invalid test http server: %v", err) 5484 } 5485 5486 repo, err := NewRepository(uri.Host + "/test") 5487 if err != nil { 5488 t.Fatalf("NewRepository() error = %v", err) 5489 } 5490 store := repo.Manifests() 5491 repo.PlainHTTP = true 5492 ctx := context.Background() 5493 5494 err = store.Tag(ctx, blobDesc, ref) 5495 if err == nil { 5496 t.Errorf("Repository.Tag() error = %v, wantErr %v", err, true) 5497 } 5498 5499 err = store.Tag(ctx, indexDesc, ref) 5500 if err != nil { 5501 t.Fatalf("Repository.Tag() error = %v", err) 5502 } 5503 if !bytes.Equal(gotIndex, index) { 5504 t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index) 5505 } 5506 5507 gotIndex = nil 5508 err = store.Tag(ctx, indexDesc, indexDesc.Digest.String()) 5509 if err != nil { 5510 t.Fatalf("Repository.Tag() error = %v", err) 5511 } 5512 if !bytes.Equal(gotIndex, index) { 5513 t.Errorf("Repository.Tag() = %v, want %v", gotIndex, index) 5514 } 5515 } 5516 5517 func Test_ManifestStore_PushReference(t *testing.T) { 5518 index := []byte(`{"manifests":[]}`) 5519 indexDesc := ocispec.Descriptor{ 5520 MediaType: ocispec.MediaTypeImageIndex, 5521 Digest: digest.FromBytes(index), 5522 Size: int64(len(index)), 5523 } 5524 var gotIndex []byte 5525 ref := "foobar" 5526 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5527 switch { 5528 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+ref: 5529 if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { 5530 w.WriteHeader(http.StatusBadRequest) 5531 break 5532 } 5533 buf := bytes.NewBuffer(nil) 5534 if _, err := buf.ReadFrom(r.Body); err != nil { 5535 t.Errorf("fail to read: %v", err) 5536 } 5537 gotIndex = buf.Bytes() 5538 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 5539 w.WriteHeader(http.StatusCreated) 5540 return 5541 default: 5542 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5543 w.WriteHeader(http.StatusForbidden) 5544 } 5545 })) 5546 defer ts.Close() 5547 uri, err := url.Parse(ts.URL) 5548 if err != nil { 5549 t.Fatalf("invalid test http server: %v", err) 5550 } 5551 5552 repo, err := NewRepository(uri.Host + "/test") 5553 if err != nil { 5554 t.Fatalf("NewRepository() error = %v", err) 5555 } 5556 store := repo.Manifests() 5557 repo.PlainHTTP = true 5558 ctx := context.Background() 5559 err = store.PushReference(ctx, indexDesc, bytes.NewReader(index), ref) 5560 if err != nil { 5561 t.Fatalf("Repository.PushReference() error = %v", err) 5562 } 5563 if !bytes.Equal(gotIndex, index) { 5564 t.Errorf("Repository.PushReference() = %v, want %v", gotIndex, index) 5565 } 5566 } 5567 5568 func Test_ManifestStore_PushReference_ReferrersAPIAvailable(t *testing.T) { 5569 // generate test content 5570 subject := []byte(`{"layers":[]}`) 5571 subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject) 5572 artifact := spec.Artifact{ 5573 MediaType: spec.MediaTypeArtifactManifest, 5574 Subject: &subjectDesc, 5575 } 5576 artifactJSON, err := json.Marshal(artifact) 5577 if err != nil { 5578 t.Fatalf("failed to marshal manifest: %v", err) 5579 } 5580 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 5581 artifactRef := "foo" 5582 5583 manifest := ocispec.Manifest{ 5584 MediaType: ocispec.MediaTypeImageManifest, 5585 Subject: &subjectDesc, 5586 } 5587 manifestJSON, err := json.Marshal(manifest) 5588 if err != nil { 5589 t.Fatalf("failed to marshal manifest: %v", err) 5590 } 5591 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 5592 manifestRef := "bar" 5593 5594 index := ocispec.Index{ 5595 MediaType: ocispec.MediaTypeImageIndex, 5596 Subject: &subjectDesc, 5597 } 5598 indexJSON, err := json.Marshal(index) 5599 if err != nil { 5600 t.Fatalf("failed to marshal manifest: %v", err) 5601 } 5602 indexDesc := content.NewDescriptorFromBytes(index.MediaType, indexJSON) 5603 indexRef := "baz" 5604 5605 var gotManifest []byte 5606 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5607 switch { 5608 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactRef: 5609 if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType { 5610 w.WriteHeader(http.StatusBadRequest) 5611 break 5612 } 5613 buf := bytes.NewBuffer(nil) 5614 if _, err := buf.ReadFrom(r.Body); err != nil { 5615 t.Errorf("fail to read: %v", err) 5616 } 5617 gotManifest = buf.Bytes() 5618 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 5619 w.Header().Set("OCI-Subject", subjectDesc.Digest.String()) 5620 w.WriteHeader(http.StatusCreated) 5621 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestRef: 5622 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 5623 w.WriteHeader(http.StatusBadRequest) 5624 break 5625 } 5626 buf := bytes.NewBuffer(nil) 5627 if _, err := buf.ReadFrom(r.Body); err != nil { 5628 t.Errorf("fail to read: %v", err) 5629 } 5630 gotManifest = buf.Bytes() 5631 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 5632 w.Header().Set("OCI-Subject", subjectDesc.Digest.String()) 5633 w.WriteHeader(http.StatusCreated) 5634 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexRef: 5635 if contentType := r.Header.Get("Content-Type"); contentType != indexDesc.MediaType { 5636 w.WriteHeader(http.StatusBadRequest) 5637 break 5638 } 5639 buf := bytes.NewBuffer(nil) 5640 if _, err := buf.ReadFrom(r.Body); err != nil { 5641 t.Errorf("fail to read: %v", err) 5642 } 5643 gotManifest = buf.Bytes() 5644 w.Header().Set("Docker-Content-Digest", indexDesc.Digest.String()) 5645 w.Header().Set("OCI-Subject", subjectDesc.Digest.String()) 5646 w.WriteHeader(http.StatusCreated) 5647 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 5648 result := ocispec.Index{ 5649 Versioned: specs.Versioned{ 5650 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 5651 }, 5652 MediaType: ocispec.MediaTypeImageIndex, 5653 Manifests: []ocispec.Descriptor{}, 5654 } 5655 if err := json.NewEncoder(w).Encode(result); err != nil { 5656 t.Errorf("failed to write response: %v", err) 5657 } 5658 default: 5659 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5660 w.WriteHeader(http.StatusNotFound) 5661 } 5662 })) 5663 defer ts.Close() 5664 uri, err := url.Parse(ts.URL) 5665 if err != nil { 5666 t.Fatalf("invalid test http server: %v", err) 5667 } 5668 ctx := context.Background() 5669 5670 // test pushing artifact with subject 5671 repo, err := NewRepository(uri.Host + "/test") 5672 if err != nil { 5673 t.Fatalf("NewRepository() error = %v", err) 5674 } 5675 repo.PlainHTTP = true 5676 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5677 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5678 } 5679 err = repo.PushReference(ctx, artifactDesc, bytes.NewReader(artifactJSON), artifactRef) 5680 if err != nil { 5681 t.Fatalf("Manifests.Push() error = %v", err) 5682 } 5683 if !bytes.Equal(gotManifest, artifactJSON) { 5684 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON)) 5685 } 5686 if state := repo.loadReferrersState(); state != referrersStateSupported { 5687 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 5688 } 5689 5690 // test pushing image manifest with subject 5691 repo, err = NewRepository(uri.Host + "/test") 5692 if err != nil { 5693 t.Fatalf("NewRepository() error = %v", err) 5694 } 5695 repo.PlainHTTP = true 5696 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5697 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5698 } 5699 err = repo.PushReference(ctx, manifestDesc, bytes.NewReader(manifestJSON), manifestRef) 5700 if err != nil { 5701 t.Fatalf("Manifests.Push() error = %v", err) 5702 } 5703 if !bytes.Equal(gotManifest, manifestJSON) { 5704 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(manifestJSON)) 5705 } 5706 if state := repo.loadReferrersState(); state != referrersStateSupported { 5707 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 5708 } 5709 5710 // test pushing image index with subject 5711 err = repo.PushReference(ctx, indexDesc, bytes.NewReader(indexJSON), indexRef) 5712 if err != nil { 5713 t.Fatalf("Manifests.Push() error = %v", err) 5714 } 5715 if !bytes.Equal(gotManifest, indexJSON) { 5716 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(indexJSON)) 5717 } 5718 if state := repo.loadReferrersState(); state != referrersStateSupported { 5719 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 5720 } 5721 } 5722 5723 func Test_ManifestStore_PushReference_ReferrersAPIUnavailable(t *testing.T) { 5724 // generate test content 5725 subject := []byte(`{"layers":[]}`) 5726 subjectDesc := content.NewDescriptorFromBytes(spec.MediaTypeArtifactManifest, subject) 5727 referrersTag := strings.Replace(subjectDesc.Digest.String(), ":", "-", 1) 5728 artifact := spec.Artifact{ 5729 MediaType: spec.MediaTypeArtifactManifest, 5730 Subject: &subjectDesc, 5731 ArtifactType: "application/vnd.test", 5732 Annotations: map[string]string{"foo": "bar"}, 5733 } 5734 artifactJSON, err := json.Marshal(artifact) 5735 if err != nil { 5736 t.Fatalf("failed to marshal manifest: %v", err) 5737 } 5738 artifactDesc := content.NewDescriptorFromBytes(artifact.MediaType, artifactJSON) 5739 artifactDesc.ArtifactType = artifact.ArtifactType 5740 artifactDesc.Annotations = artifact.Annotations 5741 artifactRef := "foo" 5742 5743 // test pushing artifact with subject 5744 index_1 := ocispec.Index{ 5745 Versioned: specs.Versioned{ 5746 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 5747 }, 5748 MediaType: ocispec.MediaTypeImageIndex, 5749 Manifests: []ocispec.Descriptor{ 5750 artifactDesc, 5751 }, 5752 } 5753 indexJSON_1, err := json.Marshal(index_1) 5754 if err != nil { 5755 t.Fatalf("failed to marshal manifest: %v", err) 5756 } 5757 indexDesc_1 := content.NewDescriptorFromBytes(index_1.MediaType, indexJSON_1) 5758 var gotManifest []byte 5759 var gotReferrerIndex []byte 5760 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5761 switch { 5762 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+artifactRef: 5763 if contentType := r.Header.Get("Content-Type"); contentType != artifactDesc.MediaType { 5764 w.WriteHeader(http.StatusBadRequest) 5765 break 5766 } 5767 buf := bytes.NewBuffer(nil) 5768 if _, err := buf.ReadFrom(r.Body); err != nil { 5769 t.Errorf("fail to read: %v", err) 5770 } 5771 gotManifest = buf.Bytes() 5772 w.Header().Set("Docker-Content-Digest", artifactDesc.Digest.String()) 5773 w.WriteHeader(http.StatusCreated) 5774 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 5775 w.WriteHeader(http.StatusNotFound) 5776 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 5777 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 5778 w.WriteHeader(http.StatusBadRequest) 5779 break 5780 } 5781 buf := bytes.NewBuffer(nil) 5782 if _, err := buf.ReadFrom(r.Body); err != nil { 5783 t.Errorf("fail to read: %v", err) 5784 } 5785 gotReferrerIndex = buf.Bytes() 5786 w.Header().Set("Docker-Content-Digest", indexDesc_1.Digest.String()) 5787 w.WriteHeader(http.StatusCreated) 5788 default: 5789 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5790 w.WriteHeader(http.StatusNotFound) 5791 } 5792 })) 5793 defer ts.Close() 5794 uri, err := url.Parse(ts.URL) 5795 if err != nil { 5796 t.Fatalf("invalid test http server: %v", err) 5797 } 5798 5799 ctx := context.Background() 5800 repo, err := NewRepository(uri.Host + "/test") 5801 if err != nil { 5802 t.Fatalf("NewRepository() error = %v", err) 5803 } 5804 repo.PlainHTTP = true 5805 5806 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5807 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5808 } 5809 err = repo.PushReference(ctx, artifactDesc, bytes.NewReader(artifactJSON), artifactRef) 5810 if err != nil { 5811 t.Fatalf("Manifests.Push() error = %v", err) 5812 } 5813 if !bytes.Equal(gotManifest, artifactJSON) { 5814 t.Errorf("Manifests.Push() = %v, want %v", string(gotManifest), string(artifactJSON)) 5815 } 5816 if !bytes.Equal(gotReferrerIndex, indexJSON_1) { 5817 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_1)) 5818 } 5819 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 5820 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 5821 } 5822 5823 // test pushing image manifest with subject, referrers list should be updated 5824 manifest := ocispec.Manifest{ 5825 MediaType: ocispec.MediaTypeImageManifest, 5826 Config: ocispec.Descriptor{ 5827 MediaType: "testconfig", 5828 }, 5829 Subject: &subjectDesc, 5830 Annotations: map[string]string{"foo": "bar"}, 5831 } 5832 manifestJSON, err := json.Marshal(manifest) 5833 if err != nil { 5834 t.Fatalf("failed to marshal manifest: %v", err) 5835 } 5836 manifestDesc := content.NewDescriptorFromBytes(manifest.MediaType, manifestJSON) 5837 manifestDesc.ArtifactType = manifest.Config.MediaType 5838 manifestDesc.Annotations = manifest.Annotations 5839 manifestRef := "bar" 5840 5841 index_2 := ocispec.Index{ 5842 Versioned: specs.Versioned{ 5843 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 5844 }, 5845 MediaType: ocispec.MediaTypeImageIndex, 5846 Manifests: []ocispec.Descriptor{ 5847 artifactDesc, 5848 manifestDesc, 5849 }, 5850 } 5851 indexJSON_2, err := json.Marshal(index_2) 5852 if err != nil { 5853 t.Fatalf("failed to marshal manifest: %v", err) 5854 } 5855 indexDesc_2 := content.NewDescriptorFromBytes(index_2.MediaType, indexJSON_2) 5856 var manifestDeleted bool 5857 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5858 switch { 5859 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestRef: 5860 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 5861 w.WriteHeader(http.StatusBadRequest) 5862 break 5863 } 5864 buf := bytes.NewBuffer(nil) 5865 if _, err := buf.ReadFrom(r.Body); err != nil { 5866 t.Errorf("fail to read: %v", err) 5867 } 5868 gotManifest = buf.Bytes() 5869 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 5870 w.WriteHeader(http.StatusCreated) 5871 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 5872 w.Write(indexJSON_1) 5873 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 5874 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 5875 w.WriteHeader(http.StatusBadRequest) 5876 break 5877 } 5878 buf := bytes.NewBuffer(nil) 5879 if _, err := buf.ReadFrom(r.Body); err != nil { 5880 t.Errorf("fail to read: %v", err) 5881 } 5882 gotReferrerIndex = buf.Bytes() 5883 w.Header().Set("Docker-Content-Digest", indexDesc_2.Digest.String()) 5884 w.WriteHeader(http.StatusCreated) 5885 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_1.Digest.String(): 5886 manifestDeleted = true 5887 // no "Docker-Content-Digest" header for manifest deletion 5888 w.WriteHeader(http.StatusAccepted) 5889 default: 5890 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5891 w.WriteHeader(http.StatusNotFound) 5892 } 5893 })) 5894 defer ts.Close() 5895 uri, err = url.Parse(ts.URL) 5896 if err != nil { 5897 t.Fatalf("invalid test http server: %v", err) 5898 } 5899 5900 ctx = context.Background() 5901 repo, err = NewRepository(uri.Host + "/test") 5902 if err != nil { 5903 t.Fatalf("NewRepository() error = %v", err) 5904 } 5905 repo.PlainHTTP = true 5906 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5907 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5908 } 5909 err = repo.PushReference(ctx, manifestDesc, bytes.NewReader(manifestJSON), manifestRef) 5910 if err != nil { 5911 t.Fatalf("Manifests.PushReference() error = %v", err) 5912 } 5913 if !bytes.Equal(gotManifest, manifestJSON) { 5914 t.Errorf("Manifests.PushReference() = %v, want %v", string(gotManifest), string(manifestJSON)) 5915 } 5916 if !bytes.Equal(gotReferrerIndex, indexJSON_2) { 5917 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) 5918 } 5919 if !manifestDeleted { 5920 t.Errorf("manifestDeleted = %v, want %v", manifestDeleted, true) 5921 } 5922 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 5923 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 5924 } 5925 5926 // test pushing image manifest with subject again, referrers list should not be changed 5927 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 5928 switch { 5929 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+manifestRef: 5930 if contentType := r.Header.Get("Content-Type"); contentType != manifestDesc.MediaType { 5931 w.WriteHeader(http.StatusBadRequest) 5932 break 5933 } 5934 buf := bytes.NewBuffer(nil) 5935 if _, err := buf.ReadFrom(r.Body); err != nil { 5936 t.Errorf("fail to read: %v", err) 5937 } 5938 gotManifest = buf.Bytes() 5939 w.Header().Set("Docker-Content-Digest", manifestDesc.Digest.String()) 5940 w.WriteHeader(http.StatusCreated) 5941 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 5942 w.Write(indexJSON_2) 5943 default: 5944 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 5945 w.WriteHeader(http.StatusNotFound) 5946 } 5947 })) 5948 defer ts.Close() 5949 uri, err = url.Parse(ts.URL) 5950 if err != nil { 5951 t.Fatalf("invalid test http server: %v", err) 5952 } 5953 5954 ctx = context.Background() 5955 repo, err = NewRepository(uri.Host + "/test") 5956 if err != nil { 5957 t.Fatalf("NewRepository() error = %v", err) 5958 } 5959 repo.PlainHTTP = true 5960 if state := repo.loadReferrersState(); state != referrersStateUnknown { 5961 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 5962 } 5963 err = repo.PushReference(ctx, manifestDesc, bytes.NewReader(manifestJSON), manifestRef) 5964 if err != nil { 5965 t.Fatalf("Manifests.PushReference() error = %v", err) 5966 } 5967 if !bytes.Equal(gotManifest, manifestJSON) { 5968 t.Errorf("Manifests.PushReference() = %v, want %v", string(gotManifest), string(manifestJSON)) 5969 } 5970 // referrers list should not be changed 5971 if !bytes.Equal(gotReferrerIndex, indexJSON_2) { 5972 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_2)) 5973 } 5974 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 5975 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 5976 } 5977 5978 // push image index with subject, referrer list should be updated 5979 indexManifest := ocispec.Index{ 5980 MediaType: ocispec.MediaTypeImageIndex, 5981 Subject: &subjectDesc, 5982 ArtifactType: "test/index", 5983 Annotations: map[string]string{"foo": "bar"}, 5984 } 5985 indexManifestJSON, err := json.Marshal(indexManifest) 5986 if err != nil { 5987 t.Fatalf("failed to marshal manifest: %v", err) 5988 } 5989 indexManifestDesc := content.NewDescriptorFromBytes(indexManifest.MediaType, indexManifestJSON) 5990 indexManifestDesc.ArtifactType = indexManifest.ArtifactType 5991 indexManifestDesc.Annotations = indexManifest.Annotations 5992 indexManifestRef := "baz" 5993 index_3 := ocispec.Index{ 5994 Versioned: specs.Versioned{ 5995 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 5996 }, 5997 MediaType: ocispec.MediaTypeImageIndex, 5998 Manifests: []ocispec.Descriptor{ 5999 artifactDesc, 6000 manifestDesc, 6001 indexManifestDesc, 6002 }, 6003 } 6004 indexJSON_3, err := json.Marshal(index_3) 6005 if err != nil { 6006 t.Fatalf("failed to marshal manifest: %v", err) 6007 } 6008 indexDesc_3 := content.NewDescriptorFromBytes(index_3.MediaType, indexJSON_3) 6009 manifestDeleted = false 6010 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 6011 switch { 6012 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+indexManifestRef: 6013 if contentType := r.Header.Get("Content-Type"); contentType != indexManifestDesc.MediaType { 6014 w.WriteHeader(http.StatusBadRequest) 6015 break 6016 } 6017 buf := bytes.NewBuffer(nil) 6018 if _, err := buf.ReadFrom(r.Body); err != nil { 6019 t.Errorf("fail to read: %v", err) 6020 } 6021 gotManifest = buf.Bytes() 6022 w.Header().Set("Docker-Content-Digest", indexManifestDesc.Digest.String()) 6023 w.WriteHeader(http.StatusCreated) 6024 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/manifests/"+referrersTag: 6025 w.Write(indexJSON_2) 6026 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/manifests/"+referrersTag: 6027 if contentType := r.Header.Get("Content-Type"); contentType != ocispec.MediaTypeImageIndex { 6028 w.WriteHeader(http.StatusBadRequest) 6029 break 6030 } 6031 buf := bytes.NewBuffer(nil) 6032 if _, err := buf.ReadFrom(r.Body); err != nil { 6033 t.Errorf("fail to read: %v", err) 6034 } 6035 gotReferrerIndex = buf.Bytes() 6036 w.Header().Set("Docker-Content-Digest", indexDesc_3.Digest.String()) 6037 w.WriteHeader(http.StatusCreated) 6038 case r.Method == http.MethodDelete && r.URL.Path == "/v2/test/manifests/"+indexDesc_2.Digest.String(): 6039 manifestDeleted = true 6040 // no "Docker-Content-Digest" header for manifest deletion 6041 w.WriteHeader(http.StatusAccepted) 6042 default: 6043 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 6044 w.WriteHeader(http.StatusNotFound) 6045 } 6046 })) 6047 defer ts.Close() 6048 uri, err = url.Parse(ts.URL) 6049 if err != nil { 6050 t.Fatalf("invalid test http server: %v", err) 6051 } 6052 6053 ctx = context.Background() 6054 repo, err = NewRepository(uri.Host + "/test") 6055 if err != nil { 6056 t.Fatalf("NewRepository() error = %v", err) 6057 } 6058 repo.PlainHTTP = true 6059 if state := repo.loadReferrersState(); state != referrersStateUnknown { 6060 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 6061 } 6062 err = repo.PushReference(ctx, indexManifestDesc, bytes.NewReader(indexManifestJSON), indexManifestRef) 6063 if err != nil { 6064 t.Fatalf("Manifests.PushReference() error = %v", err) 6065 } 6066 if !bytes.Equal(gotManifest, indexManifestJSON) { 6067 t.Errorf("Manifests.PushReference() = %v, want %v", string(gotManifest), string(indexManifestJSON)) 6068 } 6069 if !bytes.Equal(gotReferrerIndex, indexJSON_3) { 6070 t.Errorf("got referrers index = %v, want %v", string(gotReferrerIndex), string(indexJSON_3)) 6071 } 6072 if !manifestDeleted { 6073 t.Errorf("manifestDeleted = %v, want %v", manifestDeleted, true) 6074 } 6075 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 6076 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 6077 } 6078 } 6079 6080 func Test_ManifestStore_generateDescriptorWithVariousDockerContentDigestHeaders(t *testing.T) { 6081 reference := registry.Reference{ 6082 Registry: "eastern.haan.com", 6083 Reference: "<calculate>", 6084 Repository: "from25to220ce", 6085 } 6086 6087 tests := getTestIOStructMapForGetDescriptorClass() 6088 for testName, dcdIOStruct := range tests { 6089 repo, err := NewRepository(fmt.Sprintf("%s/%s", reference.Repository, reference.Repository)) 6090 if err != nil { 6091 t.Fatalf("failed to initialize repository") 6092 } 6093 6094 s := manifestStore{repo: repo} 6095 6096 for i, method := range []string{http.MethodGet, http.MethodHead} { 6097 reference.Reference = dcdIOStruct.clientSuppliedReference 6098 6099 resp := http.Response{ 6100 Header: http.Header{ 6101 "Content-Type": []string{"application/vnd.docker.distribution.manifest.v2+json"}, 6102 headerDockerContentDigest: []string{dcdIOStruct.serverCalculatedDigest.String()}, 6103 }, 6104 } 6105 if method == http.MethodGet { 6106 resp.Body = io.NopCloser(bytes.NewBufferString(theAmazingBanClan)) 6107 } 6108 resp.Request = &http.Request{ 6109 Method: method, 6110 } 6111 6112 errExpected := []bool{dcdIOStruct.errExpectedOnGET, dcdIOStruct.errExpectedOnHEAD}[i] 6113 _, err = s.generateDescriptor(&resp, reference, method) 6114 if !errExpected && err != nil { 6115 t.Errorf( 6116 "[Manifest.%v] %v; expected no error for request, but got err: %v", 6117 method, testName, err, 6118 ) 6119 } else if errExpected && err == nil { 6120 t.Errorf( 6121 "[Manifest.%v] %v; expected an error for request, but got none", 6122 method, testName, 6123 ) 6124 } 6125 } 6126 } 6127 } 6128 6129 type testTransport struct { 6130 proxyHost string 6131 underlyingTransport http.RoundTripper 6132 mockHost string 6133 } 6134 6135 func (t *testTransport) RoundTrip(originalReq *http.Request) (*http.Response, error) { 6136 req := originalReq.Clone(originalReq.Context()) 6137 mockHostName, mockPort, err := net.SplitHostPort(t.mockHost) 6138 // when t.mockHost is as form host:port 6139 if err == nil && (req.URL.Hostname() != mockHostName || req.URL.Port() != mockPort) { 6140 return nil, errors.New("bad request") 6141 } 6142 // when t.mockHost does not have specified port, in this case, 6143 // err is not nil 6144 if err != nil && req.URL.Hostname() != t.mockHost { 6145 return nil, errors.New("bad request") 6146 } 6147 req.Host = t.proxyHost 6148 req.URL.Host = t.proxyHost 6149 resp, err := t.underlyingTransport.RoundTrip(req) 6150 if err != nil { 6151 return nil, err 6152 } 6153 resp.Request.Host = t.mockHost 6154 resp.Request.URL.Host = t.mockHost 6155 return resp, nil 6156 } 6157 6158 // Helper function to create a registry.BlobStore for 6159 // Test_BlobStore_Push_Port443 6160 func blobStore_Push_Port443_create_store(uri *url.URL, testRegistry string) (registry.BlobStore, error) { 6161 repo, err := NewRepository(testRegistry + "/test") 6162 repo.Client = &auth.Client{ 6163 Client: &http.Client{ 6164 Transport: &testTransport{ 6165 proxyHost: uri.Host, 6166 underlyingTransport: http.DefaultTransport, 6167 mockHost: testRegistry, 6168 }, 6169 }, 6170 Cache: auth.NewCache(), 6171 } 6172 repo.PlainHTTP = true 6173 store := repo.Blobs() 6174 return store, err 6175 } 6176 6177 func Test_BlobStore_Push_Port443(t *testing.T) { 6178 blob := []byte("hello world") 6179 blobDesc := ocispec.Descriptor{ 6180 MediaType: "test", 6181 Digest: digest.FromBytes(blob), 6182 Size: int64(len(blob)), 6183 } 6184 uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c" 6185 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 6186 switch { 6187 case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/": 6188 w.Header().Set("Location", "http://registry.wabbit-networks.io/v2/test/blobs/uploads/"+uuid) 6189 w.WriteHeader(http.StatusAccepted) 6190 return 6191 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid: 6192 if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" { 6193 w.WriteHeader(http.StatusBadRequest) 6194 break 6195 } 6196 if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() { 6197 w.WriteHeader(http.StatusBadRequest) 6198 break 6199 } 6200 buf := bytes.NewBuffer(nil) 6201 if _, err := buf.ReadFrom(r.Body); err != nil { 6202 t.Errorf("fail to read: %v", err) 6203 } 6204 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 6205 w.WriteHeader(http.StatusCreated) 6206 return 6207 default: 6208 w.WriteHeader(http.StatusForbidden) 6209 } 6210 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 6211 })) 6212 defer ts.Close() 6213 uri, err := url.Parse(ts.URL) 6214 if err != nil { 6215 t.Fatalf("invalid test http server: %v", err) 6216 } 6217 6218 // Test case with Host: "registry.wabbit-networks.io:443", 6219 // Location: "registry.wabbit-networks.io" 6220 testRegistry := "registry.wabbit-networks.io:443" 6221 store, err := blobStore_Push_Port443_create_store(uri, testRegistry) 6222 if err != nil { 6223 t.Fatalf("blobStore_Push_Port443_create_store() error = %v", err) 6224 } 6225 ctx := context.Background() 6226 6227 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 6228 if err != nil { 6229 t.Fatalf("Blobs.Push() error = %v", err) 6230 } 6231 6232 // Test case with Host: "registry.wabbit-networks.io", 6233 // Location: "registry.wabbit-networks.io" 6234 testRegistry = "registry.wabbit-networks.io" 6235 store, err = blobStore_Push_Port443_create_store(uri, testRegistry) 6236 if err != nil { 6237 t.Fatalf("blobStore_Push_Port443_create_store() error = %v", err) 6238 } 6239 6240 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 6241 if err != nil { 6242 t.Fatalf("Blobs.Push() error = %v", err) 6243 } 6244 } 6245 6246 // Helper function to create a registry.BlobStore for 6247 // Test_BlobStore_Push_Port443_HTTPS 6248 func blobStore_Push_Port443_HTTPS_create_store(uri *url.URL, testRegistry string) (registry.BlobStore, error) { 6249 repo, err := NewRepository(testRegistry + "/test") 6250 tlsConfig := &tls.Config{ 6251 InsecureSkipVerify: true, 6252 } 6253 transport := &http.Transport{ 6254 TLSClientConfig: tlsConfig, 6255 } 6256 repo.Client = &auth.Client{ 6257 Client: &http.Client{ 6258 Transport: &testTransport{ 6259 proxyHost: uri.Host, 6260 underlyingTransport: transport, 6261 mockHost: testRegistry, 6262 }, 6263 }, 6264 Cache: auth.NewCache(), 6265 } 6266 repo.PlainHTTP = false 6267 store := repo.Blobs() 6268 return store, err 6269 } 6270 6271 func Test_BlobStore_Push_Port443_HTTPS(t *testing.T) { 6272 blob := []byte("hello world") 6273 blobDesc := ocispec.Descriptor{ 6274 MediaType: "test", 6275 Digest: digest.FromBytes(blob), 6276 Size: int64(len(blob)), 6277 } 6278 uuid := "4fd53bc9-565d-4527-ab80-3e051ac4880c" 6279 ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 6280 switch { 6281 case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/": 6282 w.Header().Set("Location", "https://registry.wabbit-networks.io/v2/test/blobs/uploads/"+uuid) 6283 w.WriteHeader(http.StatusAccepted) 6284 return 6285 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid: 6286 if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" { 6287 w.WriteHeader(http.StatusBadRequest) 6288 break 6289 } 6290 if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() { 6291 w.WriteHeader(http.StatusBadRequest) 6292 break 6293 } 6294 buf := bytes.NewBuffer(nil) 6295 if _, err := buf.ReadFrom(r.Body); err != nil { 6296 t.Errorf("fail to read: %v", err) 6297 } 6298 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 6299 w.WriteHeader(http.StatusCreated) 6300 return 6301 default: 6302 w.WriteHeader(http.StatusForbidden) 6303 } 6304 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 6305 })) 6306 defer ts.Close() 6307 uri, err := url.Parse(ts.URL) 6308 if err != nil { 6309 t.Fatalf("invalid test https server: %v", err) 6310 } 6311 6312 ctx := context.Background() 6313 // Test case with Host: "registry.wabbit-networks.io:443", 6314 // Location: "registry.wabbit-networks.io" 6315 testRegistry := "registry.wabbit-networks.io:443" 6316 store, err := blobStore_Push_Port443_HTTPS_create_store(uri, testRegistry) 6317 if err != nil { 6318 t.Fatalf("blobStore_Push_Port443_HTTPS_create_store() error = %v", err) 6319 } 6320 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 6321 if err != nil { 6322 t.Fatalf("Blobs.Push() error = %v", err) 6323 } 6324 6325 // Test case with Host: "registry.wabbit-networks.io", 6326 // Location: "registry.wabbit-networks.io" 6327 testRegistry = "registry.wabbit-networks.io" 6328 store, err = blobStore_Push_Port443_HTTPS_create_store(uri, testRegistry) 6329 if err != nil { 6330 t.Fatalf("blobStore_Push_Port443_HTTPS_create_store() error = %v", err) 6331 } 6332 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 6333 if err != nil { 6334 t.Fatalf("Blobs.Push() error = %v", err) 6335 } 6336 6337 ts = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 6338 switch { 6339 case r.Method == http.MethodPost && r.URL.Path == "/v2/test/blobs/uploads/": 6340 w.Header().Set("Location", "https://registry.wabbit-networks.io:443/v2/test/blobs/uploads/"+uuid) 6341 w.WriteHeader(http.StatusAccepted) 6342 return 6343 case r.Method == http.MethodPut && r.URL.Path == "/v2/test/blobs/uploads/"+uuid: 6344 if contentType := r.Header.Get("Content-Type"); contentType != "application/octet-stream" { 6345 w.WriteHeader(http.StatusBadRequest) 6346 break 6347 } 6348 if contentDigest := r.URL.Query().Get("digest"); contentDigest != blobDesc.Digest.String() { 6349 w.WriteHeader(http.StatusBadRequest) 6350 break 6351 } 6352 buf := bytes.NewBuffer(nil) 6353 if _, err := buf.ReadFrom(r.Body); err != nil { 6354 t.Errorf("fail to read: %v", err) 6355 } 6356 w.Header().Set("Docker-Content-Digest", blobDesc.Digest.String()) 6357 w.WriteHeader(http.StatusCreated) 6358 return 6359 default: 6360 w.WriteHeader(http.StatusForbidden) 6361 } 6362 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 6363 })) 6364 defer ts.Close() 6365 uri, err = url.Parse(ts.URL) 6366 if err != nil { 6367 t.Fatalf("invalid test https server: %v", err) 6368 } 6369 6370 // Test case with Host: "registry.wabbit-networks.io:443", 6371 // Location: "registry.wabbit-networks.io:443" 6372 testRegistry = "registry.wabbit-networks.io:443" 6373 store, err = blobStore_Push_Port443_HTTPS_create_store(uri, testRegistry) 6374 if err != nil { 6375 t.Fatalf("blobStore_Push_Port443_HTTPS_create_store() error = %v", err) 6376 } 6377 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 6378 if err != nil { 6379 t.Fatalf("Blobs.Push() error = %v", err) 6380 } 6381 6382 // Test case with Host: "registry.wabbit-networks.io", 6383 // Location: "registry.wabbit-networks.io:443" 6384 testRegistry = "registry.wabbit-networks.io" 6385 store, err = blobStore_Push_Port443_HTTPS_create_store(uri, testRegistry) 6386 if err != nil { 6387 t.Fatalf("blobStore_Push_Port443_HTTPS_create_store() error = %v", err) 6388 } 6389 err = store.Push(ctx, blobDesc, bytes.NewReader(blob)) 6390 if err != nil { 6391 t.Fatalf("Blobs.Push() error = %v", err) 6392 } 6393 } 6394 6395 // Testing `last` parameter for Tags list 6396 func TestRepository_Tags_WithLastParam(t *testing.T) { 6397 tagSet := strings.Split("abcdefghijklmnopqrstuvwxyz", "") 6398 var offset int 6399 var ts *httptest.Server 6400 ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 6401 if r.Method != http.MethodGet || r.URL.Path != "/v2/test/tags/list" { 6402 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 6403 w.WriteHeader(http.StatusNotFound) 6404 return 6405 } 6406 q := r.URL.Query() 6407 n, err := strconv.Atoi(q.Get("n")) 6408 if err != nil || n != 4 { 6409 t.Errorf("bad page size: %s", q.Get("n")) 6410 w.WriteHeader(http.StatusBadRequest) 6411 return 6412 } 6413 last := q.Get("last") 6414 if last != "" { 6415 offset = indexOf(last, tagSet) + 1 6416 } 6417 var tags []string 6418 switch q.Get("test") { 6419 case "foo": 6420 tags = tagSet[offset : offset+n] 6421 offset += n 6422 w.Header().Set("Link", fmt.Sprintf(`<%s/v2/test/tags/list?n=4&last=v&test=bar>; rel="next"`, ts.URL)) 6423 case "bar": 6424 tags = tagSet[offset : offset+n] 6425 default: 6426 tags = tagSet[offset : offset+n] 6427 offset += n 6428 w.Header().Set("Link", fmt.Sprintf(`<%s/v2/test/tags/list?n=4&last=r&test=foo>; rel="next"`, ts.URL)) 6429 } 6430 result := struct { 6431 Tags []string `json:"tags"` 6432 }{ 6433 Tags: tags, 6434 } 6435 if err := json.NewEncoder(w).Encode(result); err != nil { 6436 t.Errorf("failed to write response: %v", err) 6437 } 6438 })) 6439 defer ts.Close() 6440 uri, err := url.Parse(ts.URL) 6441 if err != nil { 6442 t.Fatalf("invalid test http server: %v", err) 6443 } 6444 6445 repo, err := NewRepository(uri.Host + "/test") 6446 if err != nil { 6447 t.Fatalf("NewRepository() error = %v", err) 6448 } 6449 repo.PlainHTTP = true 6450 repo.TagListPageSize = 4 6451 last := "n" 6452 startInd := indexOf(last, tagSet) + 1 6453 6454 ctx := context.Background() 6455 if err := repo.Tags(ctx, last, func(got []string) error { 6456 want := tagSet[startInd : startInd+repo.TagListPageSize] 6457 startInd += repo.TagListPageSize 6458 if !reflect.DeepEqual(got, want) { 6459 t.Errorf("Registry.Repositories() = %v, want %v", got, want) 6460 } 6461 return nil 6462 }); err != nil { 6463 t.Errorf("Repository.Tags() error = %v", err) 6464 } 6465 } 6466 6467 func TestRepository_ParseReference(t *testing.T) { 6468 type args struct { 6469 reference string 6470 } 6471 tests := []struct { 6472 name string 6473 repoRef registry.Reference 6474 args args 6475 want registry.Reference 6476 wantErr error 6477 }{ 6478 { 6479 name: "parse tag", 6480 repoRef: registry.Reference{ 6481 Registry: "registry.example.com", 6482 Repository: "hello-world", 6483 }, 6484 args: args{ 6485 reference: "foobar", 6486 }, 6487 want: registry.Reference{ 6488 Registry: "registry.example.com", 6489 Repository: "hello-world", 6490 Reference: "foobar", 6491 }, 6492 wantErr: nil, 6493 }, 6494 { 6495 name: "parse digest", 6496 repoRef: registry.Reference{ 6497 Registry: "registry.example.com", 6498 Repository: "hello-world", 6499 }, 6500 args: args{ 6501 reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 6502 }, 6503 want: registry.Reference{ 6504 Registry: "registry.example.com", 6505 Repository: "hello-world", 6506 Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 6507 }, 6508 wantErr: nil, 6509 }, 6510 { 6511 name: "parse tag@digest", 6512 repoRef: registry.Reference{ 6513 Registry: "registry.example.com", 6514 Repository: "hello-world", 6515 }, 6516 args: args{ 6517 reference: "foobar@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 6518 }, 6519 want: registry.Reference{ 6520 Registry: "registry.example.com", 6521 Repository: "hello-world", 6522 Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 6523 }, 6524 wantErr: nil, 6525 }, 6526 { 6527 name: "parse FQDN tag", 6528 repoRef: registry.Reference{ 6529 Registry: "registry.example.com", 6530 Repository: "hello-world", 6531 }, 6532 args: args{ 6533 reference: "registry.example.com/hello-world:foobar", 6534 }, 6535 want: registry.Reference{ 6536 Registry: "registry.example.com", 6537 Repository: "hello-world", 6538 Reference: "foobar", 6539 }, 6540 wantErr: nil, 6541 }, 6542 { 6543 name: "parse FQDN digest", 6544 repoRef: registry.Reference{ 6545 Registry: "registry.example.com", 6546 Repository: "hello-world", 6547 }, 6548 args: args{ 6549 reference: "registry.example.com/hello-world@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 6550 }, 6551 want: registry.Reference{ 6552 Registry: "registry.example.com", 6553 Repository: "hello-world", 6554 Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 6555 }, 6556 wantErr: nil, 6557 }, 6558 { 6559 name: "parse FQDN tag@digest", 6560 repoRef: registry.Reference{ 6561 Registry: "registry.example.com", 6562 Repository: "hello-world", 6563 }, 6564 args: args{ 6565 reference: "registry.example.com/hello-world:foobar@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 6566 }, 6567 want: registry.Reference{ 6568 Registry: "registry.example.com", 6569 Repository: "hello-world", 6570 Reference: "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 6571 }, 6572 wantErr: nil, 6573 }, 6574 { 6575 name: "empty reference", 6576 repoRef: registry.Reference{ 6577 Registry: "registry.example.com", 6578 Repository: "hello-world", 6579 }, 6580 args: args{ 6581 reference: "", 6582 }, 6583 want: registry.Reference{}, 6584 wantErr: errdef.ErrInvalidReference, 6585 }, 6586 { 6587 name: "missing repository", 6588 repoRef: registry.Reference{ 6589 Registry: "registry.example.com", 6590 Repository: "hello-world", 6591 }, 6592 args: args{ 6593 reference: "myregistry.example.com:hello-world", 6594 }, 6595 want: registry.Reference{}, 6596 wantErr: errdef.ErrInvalidReference, 6597 }, 6598 { 6599 name: "missing reference", 6600 repoRef: registry.Reference{ 6601 Registry: "registry.example.com", 6602 Repository: "hello-world", 6603 }, 6604 args: args{ 6605 reference: "registry.example.com/hello-world", 6606 }, 6607 want: registry.Reference{}, 6608 wantErr: errdef.ErrInvalidReference, 6609 }, 6610 { 6611 name: "registry mismatch", 6612 repoRef: registry.Reference{ 6613 Registry: "registry.example.com", 6614 Repository: "hello-world", 6615 }, 6616 args: args{ 6617 reference: "myregistry.example.com/hello-world:foobar@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 6618 }, 6619 want: registry.Reference{}, 6620 wantErr: errdef.ErrInvalidReference, 6621 }, 6622 { 6623 name: "repository mismatch", 6624 repoRef: registry.Reference{ 6625 Registry: "registry.example.com", 6626 Repository: "hello-world", 6627 }, 6628 args: args{ 6629 reference: "registry.example.com/goodbye-world:foobar@sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", 6630 }, 6631 want: registry.Reference{}, 6632 wantErr: errdef.ErrInvalidReference, 6633 }, 6634 { 6635 name: "digest posing as a tag", 6636 repoRef: registry.Reference{ 6637 Registry: "registry.example.com", 6638 Repository: "hello-world", 6639 }, 6640 args: args{ 6641 reference: "registry.example.com:5000/hello-world:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 6642 }, 6643 want: registry.Reference{}, 6644 wantErr: errdef.ErrInvalidReference, 6645 }, 6646 { 6647 name: "missing reference after the at sign", 6648 repoRef: registry.Reference{ 6649 Registry: "registry.example.com", 6650 Repository: "hello-world", 6651 }, 6652 args: args{ 6653 reference: "registry.example.com/hello-world@", 6654 }, 6655 want: registry.Reference{}, 6656 wantErr: errdef.ErrInvalidReference, 6657 }, 6658 { 6659 name: "missing reference after the colon", 6660 repoRef: registry.Reference{ 6661 Registry: "localhost", 6662 }, 6663 args: args{ 6664 reference: "localhost:5000/hello:", 6665 }, 6666 want: registry.Reference{}, 6667 wantErr: errdef.ErrInvalidReference, 6668 }, 6669 { 6670 name: "zero-size tag, zero-size digest", 6671 repoRef: registry.Reference{}, 6672 args: args{ 6673 reference: "localhost:5000/hello:@", 6674 }, 6675 want: registry.Reference{}, 6676 wantErr: errdef.ErrInvalidReference, 6677 }, 6678 { 6679 name: "zero-size tag with valid digest", 6680 repoRef: registry.Reference{}, 6681 args: args{ 6682 reference: "localhost:5000/hello:@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", 6683 }, 6684 want: registry.Reference{}, 6685 wantErr: errdef.ErrInvalidReference, 6686 }, 6687 { 6688 name: "valid tag with zero-size digest", 6689 repoRef: registry.Reference{}, 6690 args: args{ 6691 reference: "localhost:5000/hello:foobar@", 6692 }, 6693 want: registry.Reference{}, 6694 wantErr: errdef.ErrInvalidReference, 6695 }, 6696 } 6697 for _, tt := range tests { 6698 t.Run(tt.name, func(t *testing.T) { 6699 r := &Repository{ 6700 Reference: tt.repoRef, 6701 } 6702 got, err := r.ParseReference(tt.args.reference) 6703 if !errors.Is(err, tt.wantErr) { 6704 t.Errorf("Repository.ParseReference() error = %v, wantErr %v", err, tt.wantErr) 6705 return 6706 } 6707 if !reflect.DeepEqual(got, tt.want) { 6708 t.Errorf("Repository.ParseReference() = %v, want %v", got, tt.want) 6709 } 6710 }) 6711 } 6712 } 6713 6714 func TestRepository_SetReferrersCapability(t *testing.T) { 6715 repo, err := NewRepository("registry.example.com/test") 6716 if err != nil { 6717 t.Fatalf("NewRepository() error = %v", err) 6718 } 6719 // initial state 6720 if state := repo.loadReferrersState(); state != referrersStateUnknown { 6721 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 6722 } 6723 6724 // valid first time set 6725 if err := repo.SetReferrersCapability(true); err != nil { 6726 t.Errorf("Repository.SetReferrersCapability() error = %v", err) 6727 } 6728 if state := repo.loadReferrersState(); state != referrersStateSupported { 6729 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 6730 } 6731 6732 // invalid second time set, state should be no changed 6733 if err := repo.SetReferrersCapability(false); !errors.Is(err, ErrReferrersCapabilityAlreadySet) { 6734 t.Errorf("Repository.SetReferrersCapability() error = %v, wantErr %v", err, ErrReferrersCapabilityAlreadySet) 6735 } 6736 if state := repo.loadReferrersState(); state != referrersStateSupported { 6737 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 6738 } 6739 } 6740 6741 func Test_generateIndex(t *testing.T) { 6742 referrer_1 := spec.Artifact{ 6743 MediaType: spec.MediaTypeArtifactManifest, 6744 ArtifactType: "foo", 6745 } 6746 referrerJSON_1, err := json.Marshal(referrer_1) 6747 if err != nil { 6748 t.Fatal("failed to marshal manifest:", err) 6749 } 6750 referrer_2 := spec.Artifact{ 6751 MediaType: spec.MediaTypeArtifactManifest, 6752 ArtifactType: "bar", 6753 } 6754 referrerJSON_2, err := json.Marshal(referrer_2) 6755 if err != nil { 6756 t.Fatal("failed to marshal manifest:", err) 6757 } 6758 referrers := []ocispec.Descriptor{ 6759 content.NewDescriptorFromBytes(referrer_1.MediaType, referrerJSON_1), 6760 content.NewDescriptorFromBytes(referrer_2.MediaType, referrerJSON_2), 6761 } 6762 6763 wantIndex := ocispec.Index{ 6764 Versioned: specs.Versioned{ 6765 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 6766 }, 6767 MediaType: ocispec.MediaTypeImageIndex, 6768 Manifests: referrers, 6769 } 6770 wantIndexJSON, err := json.Marshal(wantIndex) 6771 if err != nil { 6772 t.Fatal("failed to marshal index:", err) 6773 } 6774 wantIndexDesc := content.NewDescriptorFromBytes(wantIndex.MediaType, wantIndexJSON) 6775 6776 wantEmptyIndex := ocispec.Index{ 6777 Versioned: specs.Versioned{ 6778 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 6779 }, 6780 MediaType: ocispec.MediaTypeImageIndex, 6781 Manifests: []ocispec.Descriptor{}, 6782 } 6783 wantEmptyIndexJSON, err := json.Marshal(wantEmptyIndex) 6784 if err != nil { 6785 t.Fatal("failed to marshal index:", err) 6786 } 6787 wantEmptyIndexDesc := content.NewDescriptorFromBytes(wantEmptyIndex.MediaType, wantEmptyIndexJSON) 6788 6789 tests := []struct { 6790 name string 6791 manifests []ocispec.Descriptor 6792 wantDesc ocispec.Descriptor 6793 wantBytes []byte 6794 wantErr bool 6795 }{ 6796 { 6797 name: "non-empty referrers list", 6798 manifests: referrers, 6799 wantDesc: wantIndexDesc, 6800 wantBytes: wantIndexJSON, 6801 wantErr: false, 6802 }, 6803 { 6804 name: "nil referrers list", 6805 manifests: nil, 6806 wantDesc: wantEmptyIndexDesc, 6807 wantBytes: wantEmptyIndexJSON, 6808 wantErr: false, 6809 }, 6810 { 6811 name: "empty referrers list", 6812 manifests: nil, 6813 wantDesc: wantEmptyIndexDesc, 6814 wantBytes: wantEmptyIndexJSON, 6815 wantErr: false, 6816 }, 6817 } 6818 for _, tt := range tests { 6819 t.Run(tt.name, func(t *testing.T) { 6820 got, got1, err := generateIndex(tt.manifests) 6821 if (err != nil) != tt.wantErr { 6822 t.Errorf("generateReferrersIndex() error = %v, wantErr %v", err, tt.wantErr) 6823 return 6824 } 6825 if !reflect.DeepEqual(got, tt.wantDesc) { 6826 t.Errorf("generateReferrersIndex() got = %v, want %v", got, tt.wantDesc) 6827 } 6828 if !reflect.DeepEqual(got1, tt.wantBytes) { 6829 t.Errorf("generateReferrersIndex() got1 = %v, want %v", got1, tt.wantBytes) 6830 } 6831 }) 6832 } 6833 } 6834 6835 func TestRepository_pingReferrers(t *testing.T) { 6836 t.Run("referrers available", func(t *testing.T) { 6837 count := 0 6838 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 6839 switch { 6840 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 6841 count++ 6842 w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex) 6843 w.WriteHeader(http.StatusOK) 6844 default: 6845 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 6846 w.WriteHeader(http.StatusNotFound) 6847 } 6848 6849 })) 6850 defer ts.Close() 6851 uri, err := url.Parse(ts.URL) 6852 if err != nil { 6853 t.Fatalf("invalid test http server: %v", err) 6854 } 6855 6856 ctx := context.Background() 6857 repo, err := NewRepository(uri.Host + "/test") 6858 if err != nil { 6859 t.Fatalf("NewRepository() error = %v", err) 6860 } 6861 repo.PlainHTTP = true 6862 6863 // 1st call 6864 if state := repo.loadReferrersState(); state != referrersStateUnknown { 6865 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 6866 } 6867 got, err := repo.pingReferrers(ctx) 6868 if err != nil { 6869 t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 6870 } 6871 if got != true { 6872 t.Errorf("Repository.pingReferrers() = %v, want %v", got, true) 6873 } 6874 if state := repo.loadReferrersState(); state != referrersStateSupported { 6875 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 6876 } 6877 if count != 1 { 6878 t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) 6879 } 6880 6881 // 2nd call 6882 if state := repo.loadReferrersState(); state != referrersStateSupported { 6883 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 6884 } 6885 got, err = repo.pingReferrers(ctx) 6886 if err != nil { 6887 t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 6888 } 6889 if got != true { 6890 t.Errorf("Repository.pingReferrers() = %v, want %v", got, true) 6891 } 6892 if state := repo.loadReferrersState(); state != referrersStateSupported { 6893 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 6894 } 6895 if count != 1 { 6896 t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) 6897 } 6898 }) 6899 6900 t.Run("referrers unavailable", func(t *testing.T) { 6901 count := 0 6902 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 6903 switch { 6904 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 6905 count++ 6906 w.WriteHeader(http.StatusNotFound) 6907 default: 6908 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 6909 w.WriteHeader(http.StatusNotFound) 6910 } 6911 6912 })) 6913 defer ts.Close() 6914 uri, err := url.Parse(ts.URL) 6915 if err != nil { 6916 t.Fatalf("invalid test http server: %v", err) 6917 } 6918 6919 ctx := context.Background() 6920 repo, err := NewRepository(uri.Host + "/test") 6921 if err != nil { 6922 t.Fatalf("NewRepository() error = %v", err) 6923 } 6924 repo.PlainHTTP = true 6925 6926 // 1st call 6927 if state := repo.loadReferrersState(); state != referrersStateUnknown { 6928 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 6929 } 6930 got, err := repo.pingReferrers(ctx) 6931 if err != nil { 6932 t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 6933 } 6934 if got != false { 6935 t.Errorf("Repository.pingReferrers() = %v, want %v", got, false) 6936 } 6937 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 6938 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 6939 } 6940 if count != 1 { 6941 t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) 6942 } 6943 6944 // 2nd call 6945 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 6946 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 6947 } 6948 got, err = repo.pingReferrers(ctx) 6949 if err != nil { 6950 t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 6951 } 6952 if got != false { 6953 t.Errorf("Repository.pingReferrers() = %v, want %v", got, false) 6954 } 6955 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 6956 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 6957 } 6958 if count != 1 { 6959 t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) 6960 } 6961 }) 6962 6963 t.Run("referrers unavailable incorrect content type", func(t *testing.T) { 6964 count := 0 6965 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 6966 switch { 6967 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 6968 count++ 6969 w.Header().Set("Content-Type", "text/html") // can be anything except an OCI image index 6970 w.WriteHeader(http.StatusOK) 6971 default: 6972 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 6973 w.WriteHeader(http.StatusNotFound) 6974 } 6975 6976 })) 6977 defer ts.Close() 6978 uri, err := url.Parse(ts.URL) 6979 if err != nil { 6980 t.Fatalf("invalid test http server: %v", err) 6981 } 6982 6983 ctx := context.Background() 6984 repo, err := NewRepository(uri.Host + "/test") 6985 if err != nil { 6986 t.Fatalf("NewRepository() error = %v", err) 6987 } 6988 repo.PlainHTTP = true 6989 6990 got, err := repo.pingReferrers(ctx) 6991 if err != nil { 6992 t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 6993 } 6994 if got != false { 6995 t.Errorf("Repository.pingReferrers() = %v, want %v", got, false) 6996 } 6997 if count != 1 { 6998 t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) 6999 } 7000 }) 7001 } 7002 7003 func TestRepository_pingReferrers_RepositoryNotFound(t *testing.T) { 7004 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 7005 if r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest { 7006 w.WriteHeader(http.StatusNotFound) 7007 w.Write([]byte(`{ "errors": [ { "code": "NAME_UNKNOWN", "message": "repository name not known to registry" } ] }`)) 7008 return 7009 } 7010 t.Errorf("unexpected access: %s %q", r.Method, r.URL) 7011 w.WriteHeader(http.StatusNotFound) 7012 })) 7013 defer ts.Close() 7014 uri, err := url.Parse(ts.URL) 7015 if err != nil { 7016 t.Fatalf("invalid test http server: %v", err) 7017 } 7018 ctx := context.Background() 7019 7020 // test referrers state unknown 7021 repo, err := NewRepository(uri.Host + "/test") 7022 if err != nil { 7023 t.Fatalf("NewRepository() error = %v", err) 7024 } 7025 repo.PlainHTTP = true 7026 if state := repo.loadReferrersState(); state != referrersStateUnknown { 7027 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 7028 } 7029 if _, err = repo.pingReferrers(ctx); err == nil { 7030 t.Fatalf("Repository.pingReferrers() error = %v, wantErr %v", err, true) 7031 } 7032 if state := repo.loadReferrersState(); state != referrersStateUnknown { 7033 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 7034 } 7035 7036 // test referrers state supported 7037 repo, err = NewRepository(uri.Host + "/test") 7038 if err != nil { 7039 t.Fatalf("NewRepository() error = %v", err) 7040 } 7041 repo.PlainHTTP = true 7042 repo.SetReferrersCapability(true) 7043 if state := repo.loadReferrersState(); state != referrersStateSupported { 7044 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 7045 } 7046 got, err := repo.pingReferrers(ctx) 7047 if err != nil { 7048 t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 7049 } 7050 if got != true { 7051 t.Errorf("Repository.pingReferrers() = %v, want %v", got, true) 7052 } 7053 if state := repo.loadReferrersState(); state != referrersStateSupported { 7054 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 7055 } 7056 7057 // test referrers state unsupported 7058 repo, err = NewRepository(uri.Host + "/test") 7059 if err != nil { 7060 t.Fatalf("NewRepository() error = %v", err) 7061 } 7062 repo.PlainHTTP = true 7063 repo.SetReferrersCapability(false) 7064 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 7065 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 7066 } 7067 got, err = repo.pingReferrers(ctx) 7068 if err != nil { 7069 t.Errorf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 7070 } 7071 if got != false { 7072 t.Errorf("Repository.pingReferrers() = %v, want %v", got, false) 7073 } 7074 if state := repo.loadReferrersState(); state != referrersStateUnsupported { 7075 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnsupported) 7076 } 7077 } 7078 7079 func TestRepository_pingReferrers_Concurrent(t *testing.T) { 7080 // referrers available 7081 var count int32 7082 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 7083 switch { 7084 case r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest: 7085 atomic.AddInt32(&count, 1) 7086 w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex) 7087 w.WriteHeader(http.StatusOK) 7088 default: 7089 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 7090 w.WriteHeader(http.StatusNotFound) 7091 } 7092 7093 })) 7094 defer ts.Close() 7095 uri, err := url.Parse(ts.URL) 7096 if err != nil { 7097 t.Fatalf("invalid test http server: %v", err) 7098 } 7099 7100 ctx := context.Background() 7101 repo, err := NewRepository(uri.Host + "/test") 7102 if err != nil { 7103 t.Fatalf("NewRepository() error = %v", err) 7104 } 7105 repo.PlainHTTP = true 7106 7107 concurrency := 64 7108 eg, egCtx := errgroup.WithContext(ctx) 7109 7110 if state := repo.loadReferrersState(); state != referrersStateUnknown { 7111 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateUnknown) 7112 } 7113 for i := 0; i < concurrency; i++ { 7114 eg.Go(func() func() error { 7115 return func() error { 7116 got, err := repo.pingReferrers(egCtx) 7117 if err != nil { 7118 t.Fatalf("Repository.pingReferrers() error = %v, wantErr %v", err, nil) 7119 } 7120 if got != true { 7121 t.Errorf("Repository.pingReferrers() = %v, want %v", got, true) 7122 } 7123 return nil 7124 } 7125 }()) 7126 } 7127 if err := eg.Wait(); err != nil { 7128 t.Fatal(err) 7129 } 7130 7131 if got := atomic.LoadInt32(&count); got != 1 { 7132 t.Errorf("count(Repository.pingReferrers()) = %v, want %v", count, 1) 7133 } 7134 if state := repo.loadReferrersState(); state != referrersStateSupported { 7135 t.Errorf("Repository.loadReferrersState() = %v, want %v", state, referrersStateSupported) 7136 } 7137 } 7138 7139 func TestRepository_do(t *testing.T) { 7140 data := []byte(`hello world!`) 7141 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 7142 if r.Method != http.MethodGet || r.URL.Path != "/test" { 7143 t.Errorf("unexpected access: %s %s", r.Method, r.URL) 7144 w.WriteHeader(http.StatusNotFound) 7145 return 7146 } 7147 w.Header().Add("Warning", `299 - "Test 1: Good warning."`) 7148 w.Header().Add("Warning", `199 - "Test 2: Warning with a non-299 code."`) 7149 w.Header().Add("Warning", `299 - "Test 3: Good warning."`) 7150 w.Header().Add("Warning", `299 myregistry.example.com "Test 4: Warning with a non-unknown agent"`) 7151 w.Header().Add("Warning", `299 - "Test 5: Warning with a date." "Sat, 25 Aug 2012 23:34:45 GMT"`) 7152 w.Header().Add("wArnIng", `299 - "Test 6: Good warning."`) 7153 w.Write(data) 7154 })) 7155 defer ts.Close() 7156 uri, err := url.Parse(ts.URL) 7157 if err != nil { 7158 t.Fatalf("invalid test http server: %v", err) 7159 } 7160 testURL := ts.URL + "/test" 7161 7162 // test do() without HandleWarning 7163 repo, err := NewRepository(uri.Host + "/test") 7164 if err != nil { 7165 t.Fatal("NewRepository() error =", err) 7166 } 7167 req, err := http.NewRequest(http.MethodGet, testURL, nil) 7168 if err != nil { 7169 t.Fatal("failed to create test request:", err) 7170 } 7171 resp, err := repo.do(req) 7172 if err != nil { 7173 t.Fatal("Repository.do() error =", err) 7174 } 7175 if resp.StatusCode != http.StatusOK { 7176 t.Errorf("Repository.do() status code = %v, want %v", resp.StatusCode, http.StatusOK) 7177 } 7178 if got := len(resp.Header["Warning"]); got != 6 { 7179 t.Errorf("Repository.do() warning header len = %v, want %v", got, 6) 7180 } 7181 got, err := io.ReadAll(resp.Body) 7182 if err != nil { 7183 t.Fatal("io.ReadAll() error =", err) 7184 } 7185 resp.Body.Close() 7186 if !bytes.Equal(got, data) { 7187 t.Errorf("Repository.do() = %v, want %v", got, data) 7188 } 7189 7190 // test do() with HandleWarning 7191 repo, err = NewRepository(uri.Host + "/test") 7192 if err != nil { 7193 t.Fatal("NewRepository() error =", err) 7194 } 7195 var gotWarnings []Warning 7196 repo.HandleWarning = func(warning Warning) { 7197 gotWarnings = append(gotWarnings, warning) 7198 } 7199 7200 req, err = http.NewRequest(http.MethodGet, testURL, nil) 7201 if err != nil { 7202 t.Fatal("failed to create test request:", err) 7203 } 7204 resp, err = repo.do(req) 7205 if err != nil { 7206 t.Fatal("Repository.do() error =", err) 7207 } 7208 if resp.StatusCode != http.StatusOK { 7209 t.Errorf("Repository.do() status code = %v, want %v", resp.StatusCode, http.StatusOK) 7210 } 7211 if got := len(resp.Header["Warning"]); got != 6 { 7212 t.Errorf("Repository.do() warning header len = %v, want %v", got, 6) 7213 } 7214 got, err = io.ReadAll(resp.Body) 7215 if err != nil { 7216 t.Errorf("Repository.do() = %v, want %v", got, data) 7217 } 7218 resp.Body.Close() 7219 if !bytes.Equal(got, data) { 7220 t.Errorf("Repository.do() = %v, want %v", got, data) 7221 } 7222 7223 wantWarnings := []Warning{ 7224 { 7225 WarningValue: WarningValue{ 7226 Code: 299, 7227 Agent: "-", 7228 Text: "Test 1: Good warning.", 7229 }, 7230 }, 7231 { 7232 WarningValue: WarningValue{ 7233 Code: 299, 7234 Agent: "-", 7235 Text: "Test 3: Good warning.", 7236 }, 7237 }, 7238 { 7239 WarningValue: WarningValue{ 7240 Code: 299, 7241 Agent: "-", 7242 Text: "Test 6: Good warning.", 7243 }, 7244 }, 7245 } 7246 if !reflect.DeepEqual(gotWarnings, wantWarnings) { 7247 t.Errorf("Repository.do() = %v, want %v", gotWarnings, wantWarnings) 7248 } 7249 } 7250 7251 func TestRepository_clone(t *testing.T) { 7252 repo, err := NewRepository("localhost:1234/repo/image") 7253 if err != nil { 7254 t.Fatalf("invalid repository: %v", err) 7255 } 7256 7257 crepo := repo.clone() 7258 7259 if repo.Reference != crepo.Reference { 7260 t.Fatal("references should be the same") 7261 } 7262 7263 if !reflect.DeepEqual(&repo.referrersPingLock, &crepo.referrersPingLock) { 7264 t.Fatal("referrersPingLock should be different") 7265 } 7266 7267 if !reflect.DeepEqual(&repo.referrersMergePool, &crepo.referrersMergePool) { 7268 t.Fatal("referrersMergePool should be different") 7269 } 7270 }