github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/registry/remote/repository.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 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "mime" 26 "net/http" 27 "strconv" 28 "strings" 29 "sync" 30 "sync/atomic" 31 32 "github.com/opcr-io/oras-go/v2/content" 33 "github.com/opcr-io/oras-go/v2/errdef" 34 "github.com/opcr-io/oras-go/v2/internal/cas" 35 "github.com/opcr-io/oras-go/v2/internal/httputil" 36 "github.com/opcr-io/oras-go/v2/internal/ioutil" 37 "github.com/opcr-io/oras-go/v2/internal/registryutil" 38 "github.com/opcr-io/oras-go/v2/internal/slices" 39 "github.com/opcr-io/oras-go/v2/internal/syncutil" 40 "github.com/opcr-io/oras-go/v2/registry" 41 "github.com/opcr-io/oras-go/v2/registry/remote/auth" 42 "github.com/opcr-io/oras-go/v2/registry/remote/errcode" 43 "github.com/opcr-io/oras-go/v2/registry/remote/internal/errutil" 44 "github.com/opencontainers/go-digest" 45 specs "github.com/opencontainers/image-spec/specs-go" 46 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 47 ) 48 49 // dockerContentDigestHeader - The Docker-Content-Digest header, if present 50 // on the response, returns the canonical digest of the uploaded blob. 51 // See https://docs.docker.com/registry/spec/api/#digest-header 52 // See https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pull 53 const dockerContentDigestHeader = "Docker-Content-Digest" 54 55 // Client is an interface for a HTTP client. 56 type Client interface { 57 // Do sends an HTTP request and returns an HTTP response. 58 // 59 // Unlike http.RoundTripper, Client can attempt to interpret the response 60 // and handle higher-level protocol details such as redirects and 61 // authentication. 62 // 63 // Like http.RoundTripper, Client should not modify the request, and must 64 // always close the request body. 65 Do(*http.Request) (*http.Response, error) 66 } 67 68 // Repository is an HTTP client to a remote repository. 69 type Repository struct { 70 // Client is the underlying HTTP client used to access the remote registry. 71 // If nil, auth.DefaultClient is used. 72 Client Client 73 74 // Reference references the remote repository. 75 Reference registry.Reference 76 77 // PlainHTTP signals the transport to access the remote repository via HTTP 78 // instead of HTTPS. 79 PlainHTTP bool 80 81 // ManifestMediaTypes is used in `Accept` header for resolving manifests 82 // from references. It is also used in identifying manifests and blobs from 83 // descriptors. If an empty list is present, default manifest media types 84 // are used. 85 ManifestMediaTypes []string 86 87 // TagListPageSize specifies the page size when invoking the tag list API. 88 // If zero, the page size is determined by the remote registry. 89 // Reference: https://docs.docker.com/registry/spec/api/#tags 90 TagListPageSize int 91 92 // ReferrerListPageSize specifies the page size when invoking the Referrers 93 // API. 94 // If zero, the page size is determined by the remote registry. 95 // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers 96 ReferrerListPageSize int 97 98 // MaxMetadataBytes specifies a limit on how many response bytes are allowed 99 // in the server's response to the metadata APIs, such as catalog list, tag 100 // list, and referrers list. 101 // If less than or equal to zero, a default (currently 4MiB) is used. 102 MaxMetadataBytes int64 103 104 // NOTE: Must keep fields in sync with newRepositoryWithOptions function. 105 106 // referrersState represents that if the repository supports Referrers API. 107 // default: referrersStateUnknown 108 referrersState referrersState 109 110 // referrersPingLock locks the pingReferrers() method and allows only 111 // one go-routine to send the request. 112 referrersPingLock sync.Mutex 113 114 // referrersMergePool provides a way to manage concurrent updates to a 115 // referrers index tagged by referrers tag schema. 116 referrersMergePool syncutil.Pool[syncutil.Merge[referrerChange]] 117 } 118 119 // NewRepository creates a client to the remote repository identified by a 120 // reference. 121 // Example: localhost:5000/hello-world 122 func NewRepository(reference string) (*Repository, error) { 123 ref, err := registry.ParseReference(reference) 124 if err != nil { 125 return nil, err 126 } 127 return &Repository{ 128 Reference: ref, 129 }, nil 130 } 131 132 // newRepositoryWithOptions returns a Repository with the given Reference and 133 // RepositoryOptions. 134 // 135 // RepositoryOptions are part of the Registry struct and set its defaults. 136 // RepositoryOptions shares the same struct definition as Repository, which 137 // contains unexported state that must not be copied to multiple Repositories. 138 // To handle this we explicitly copy only the fields that we want to reproduce. 139 func newRepositoryWithOptions(ref registry.Reference, opts *RepositoryOptions) (*Repository, error) { 140 if err := ref.ValidateRepository(); err != nil { 141 return nil, err 142 } 143 return &Repository{ 144 Client: opts.Client, 145 Reference: ref, 146 PlainHTTP: opts.PlainHTTP, 147 ManifestMediaTypes: slices.Clone(opts.ManifestMediaTypes), 148 TagListPageSize: opts.TagListPageSize, 149 ReferrerListPageSize: opts.ReferrerListPageSize, 150 MaxMetadataBytes: opts.MaxMetadataBytes, 151 }, nil 152 } 153 154 // SetReferrersCapability indicates the Referrers API capability of the remote 155 // repository. true: capable; false: not capable. 156 // 157 // SetReferrersCapability is valid only when it is called for the first time. 158 // SetReferrersCapability returns ErrReferrersCapabilityAlreadySet if the 159 // Referrers API capability has been already set. 160 // - When the capability is set to true, the Referrers() function will always 161 // request the Referrers API. Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers 162 // - When the capability is set to false, the Referrers() function will always 163 // request the Referrers Tag. Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#referrers-tag-schema 164 // - When the capability is not set, the Referrers() function will automatically 165 // determine which API to use. 166 func (r *Repository) SetReferrersCapability(capable bool) error { 167 var state referrersState 168 if capable { 169 state = referrersStateSupported 170 } else { 171 state = referrersStateUnsupported 172 } 173 if swapped := atomic.CompareAndSwapInt32(&r.referrersState, referrersStateUnknown, state); !swapped { 174 if fact := r.loadReferrersState(); fact != state { 175 return fmt.Errorf("%w: current capability = %v, new capability = %v", 176 ErrReferrersCapabilityAlreadySet, 177 fact == referrersStateSupported, 178 capable) 179 } 180 } 181 return nil 182 } 183 184 // setReferrersState atomically loads r.referrersState. 185 func (r *Repository) loadReferrersState() referrersState { 186 return atomic.LoadInt32(&r.referrersState) 187 } 188 189 // client returns an HTTP client used to access the remote repository. 190 // A default HTTP client is return if the client is not configured. 191 func (r *Repository) client() Client { 192 if r.Client == nil { 193 return auth.DefaultClient 194 } 195 return r.Client 196 } 197 198 // blobStore detects the blob store for the given descriptor. 199 func (r *Repository) blobStore(desc ocispec.Descriptor) registry.BlobStore { 200 if isManifest(r.ManifestMediaTypes, desc) { 201 return r.Manifests() 202 } 203 return r.Blobs() 204 } 205 206 // Fetch fetches the content identified by the descriptor. 207 func (r *Repository) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { 208 return r.blobStore(target).Fetch(ctx, target) 209 } 210 211 // Push pushes the content, matching the expected descriptor. 212 func (r *Repository) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { 213 return r.blobStore(expected).Push(ctx, expected, content) 214 } 215 216 // Exists returns true if the described content exists. 217 func (r *Repository) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { 218 return r.blobStore(target).Exists(ctx, target) 219 } 220 221 // Delete removes the content identified by the descriptor. 222 func (r *Repository) Delete(ctx context.Context, target ocispec.Descriptor) error { 223 return r.blobStore(target).Delete(ctx, target) 224 } 225 226 // Blobs provides access to the blob CAS only, which contains config blobs, 227 // layers, and other generic blobs. 228 func (r *Repository) Blobs() registry.BlobStore { 229 return &blobStore{repo: r} 230 } 231 232 // Manifests provides access to the manifest CAS only. 233 func (r *Repository) Manifests() registry.ManifestStore { 234 return &manifestStore{repo: r} 235 } 236 237 // Resolve resolves a reference to a manifest descriptor. 238 // See also `ManifestMediaTypes`. 239 func (r *Repository) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { 240 return r.Manifests().Resolve(ctx, reference) 241 } 242 243 // Tag tags a manifest descriptor with a reference string. 244 func (r *Repository) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error { 245 return r.Manifests().Tag(ctx, desc, reference) 246 } 247 248 // PushReference pushes the manifest with a reference tag. 249 func (r *Repository) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { 250 return r.Manifests().PushReference(ctx, expected, content, reference) 251 } 252 253 // FetchReference fetches the manifest identified by the reference. 254 // The reference can be a tag or digest. 255 func (r *Repository) FetchReference(ctx context.Context, reference string) (ocispec.Descriptor, io.ReadCloser, error) { 256 return r.Manifests().FetchReference(ctx, reference) 257 } 258 259 // ParseReference resolves a tag or a digest reference to a fully qualified 260 // reference from a base reference r.Reference. 261 // Tag, digest, or fully qualified references are accepted as input. 262 // 263 // If reference is a fully qualified reference, then ParseReference parses it 264 // and returns the parsed reference. If the parsed reference does not share 265 // the same base reference with the Repository r, ParseReference returns a 266 // wrapped error ErrInvalidReference. 267 func (r *Repository) ParseReference(reference string) (registry.Reference, error) { 268 ref, err := registry.ParseReference(reference) 269 if err != nil { 270 ref = registry.Reference{ 271 Registry: r.Reference.Registry, 272 Repository: r.Reference.Repository, 273 Reference: reference, 274 } 275 276 // reference is not a FQDN 277 if index := strings.IndexByte(reference, '@'); index != -1 { 278 // `@` implies *digest*, so drop the *tag* (irrespective of what it is). 279 ref.Reference = reference[index+1:] 280 err = ref.ValidateReferenceAsDigest() 281 } else { 282 err = ref.ValidateReference() 283 } 284 285 if err != nil { 286 return registry.Reference{}, err 287 } 288 } else if ref.Registry != r.Reference.Registry || ref.Repository != r.Reference.Repository { 289 return registry.Reference{}, fmt.Errorf( 290 "%w: mismatch between received %q and expected %q", 291 errdef.ErrInvalidReference, ref, r.Reference, 292 ) 293 } 294 295 if len(ref.Reference) == 0 { 296 return registry.Reference{}, errdef.ErrInvalidReference 297 } 298 299 return ref, nil 300 } 301 302 // Tags lists the tags available in the repository. 303 // See also `TagListPageSize`. 304 // If `last` is NOT empty, the entries in the response start after the 305 // tag specified by `last`. Otherwise, the response starts from the top 306 // of the Tags list. 307 // 308 // References: 309 // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#content-discovery 310 // - https://docs.docker.com/registry/spec/api/#tags 311 func (r *Repository) Tags(ctx context.Context, last string, fn func(tags []string) error) error { 312 ctx = registryutil.WithScopeHint(ctx, r.Reference, auth.ActionPull) 313 url := buildRepositoryTagListURL(r.PlainHTTP, r.Reference) 314 var err error 315 for err == nil { 316 url, err = r.tags(ctx, last, fn, url) 317 // clear `last` for subsequent pages 318 last = "" 319 } 320 if err != errNoLink { 321 return err 322 } 323 return nil 324 } 325 326 // tags returns a single page of tag list with the next link. 327 func (r *Repository) tags(ctx context.Context, last string, fn func(tags []string) error, url string) (string, error) { 328 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 329 if err != nil { 330 return "", err 331 } 332 if r.TagListPageSize > 0 || last != "" { 333 q := req.URL.Query() 334 if r.TagListPageSize > 0 { 335 q.Set("n", strconv.Itoa(r.TagListPageSize)) 336 } 337 if last != "" { 338 q.Set("last", last) 339 } 340 req.URL.RawQuery = q.Encode() 341 } 342 resp, err := r.client().Do(req) 343 if err != nil { 344 return "", err 345 } 346 defer resp.Body.Close() 347 348 if resp.StatusCode != http.StatusOK { 349 return "", errutil.ParseErrorResponse(resp) 350 } 351 var page struct { 352 Tags []string `json:"tags"` 353 } 354 lr := limitReader(resp.Body, r.MaxMetadataBytes) 355 if err := json.NewDecoder(lr).Decode(&page); err != nil { 356 return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err) 357 } 358 if err := fn(page.Tags); err != nil { 359 return "", err 360 } 361 362 return parseLink(resp) 363 } 364 365 // Predecessors returns the descriptors of image or artifact manifests directly 366 // referencing the given manifest descriptor. 367 // Predecessors internally leverages Referrers. 368 // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers 369 func (r *Repository) Predecessors(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { 370 var res []ocispec.Descriptor 371 if err := r.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error { 372 res = append(res, referrers...) 373 return nil 374 }); err != nil { 375 return nil, err 376 } 377 return res, nil 378 } 379 380 // Referrers lists the descriptors of image or artifact manifests directly 381 // referencing the given manifest descriptor. 382 // 383 // fn is called for each page of the referrers result. 384 // If artifactType is not empty, only referrers of the same artifact type are 385 // fed to fn. 386 // 387 // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers 388 func (r *Repository) Referrers(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error { 389 state := r.loadReferrersState() 390 if state == referrersStateUnsupported { 391 // The repository is known to not support Referrers API, fallback to 392 // referrers tag schema. 393 return r.referrersByTagSchema(ctx, desc, artifactType, fn) 394 } 395 396 err := r.referrersByAPI(ctx, desc, artifactType, fn) 397 if state == referrersStateSupported { 398 // The repository is known to support Referrers API, no fallback. 399 return err 400 } 401 402 // The referrers state is unknown. 403 if err != nil { 404 var errResp *errcode.ErrorResponse 405 if !errors.As(err, &errResp) || errResp.StatusCode != http.StatusNotFound { 406 return err 407 } 408 if errutil.IsErrorCode(errResp, errcode.ErrorCodeNameUnknown) { 409 // The repository is not found, no fallback. 410 return err 411 } 412 // A 404 returned by Referrers API indicates that Referrers API is 413 // not supported. Fallback to referrers tag schema. 414 r.SetReferrersCapability(false) 415 return r.referrersByTagSchema(ctx, desc, artifactType, fn) 416 } 417 418 r.SetReferrersCapability(true) 419 return nil 420 } 421 422 // referrersByAPI lists the descriptors of manifests directly referencing 423 // the given manifest descriptor by requesting Referrers API. 424 // fn is called for the referrers result. If artifactType is not empty, 425 // only referrers of the same artifact type are fed to fn. 426 func (r *Repository) referrersByAPI(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error { 427 ref := r.Reference 428 ref.Reference = desc.Digest.String() 429 ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull) 430 431 url := buildReferrersURL(r.PlainHTTP, ref, artifactType) 432 var err error 433 for err == nil { 434 url, err = r.referrersPageByAPI(ctx, artifactType, fn, url) 435 } 436 if err == errNoLink { 437 return nil 438 } 439 return err 440 } 441 442 // referrersPageByAPI lists a single page of the descriptors of manifests 443 // directly referencing the given manifest descriptor. fn is called for 444 // a page of referrersPageByAPI result. 445 // If artifactType is not empty, only referrersPageByAPI of the same 446 // artifact type are fed to fn. 447 // referrersPageByAPI returns the link url for the next page. 448 func (r *Repository) referrersPageByAPI(ctx context.Context, artifactType string, fn func(referrers []ocispec.Descriptor) error, url string) (string, error) { 449 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 450 if err != nil { 451 return "", err 452 } 453 if r.ReferrerListPageSize > 0 { 454 q := req.URL.Query() 455 q.Set("n", strconv.Itoa(r.ReferrerListPageSize)) 456 req.URL.RawQuery = q.Encode() 457 } 458 459 resp, err := r.client().Do(req) 460 if err != nil { 461 return "", err 462 } 463 defer resp.Body.Close() 464 465 if resp.StatusCode != http.StatusOK { 466 return "", errutil.ParseErrorResponse(resp) 467 } 468 469 var index ocispec.Index 470 lr := limitReader(resp.Body, r.MaxMetadataBytes) 471 if err := json.NewDecoder(lr).Decode(&index); err != nil { 472 return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err) 473 } 474 referrers := index.Manifests 475 if artifactType != "" && !isReferrersFilterApplied(index.Annotations, "artifactType") { 476 // perform client side filtering if the filter is not applied on the server side 477 referrers = filterReferrers(referrers, artifactType) 478 } 479 if len(referrers) > 0 { 480 if err := fn(referrers); err != nil { 481 return "", err 482 } 483 } 484 return parseLink(resp) 485 } 486 487 // referrersByTagSchema lists the descriptors of manifests directly 488 // referencing the given manifest descriptor by requesting referrers tag. 489 // fn is called for the referrers result. If artifactType is not empty, 490 // only referrers of the same artifact type are fed to fn. 491 // reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#backwards-compatibility 492 func (r *Repository) referrersByTagSchema(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error { 493 referrersTag := buildReferrersTag(desc) 494 _, referrers, err := r.referrersFromIndex(ctx, referrersTag) 495 if err != nil { 496 if errors.Is(err, errdef.ErrNotFound) { 497 // no referrers to the manifest 498 return nil 499 } 500 return err 501 } 502 503 filtered := filterReferrers(referrers, artifactType) 504 if len(filtered) == 0 { 505 return nil 506 } 507 return fn(filtered) 508 } 509 510 // referrersFromIndex queries the referrers index using the the given referrers 511 // tag. If Succeeded, returns the descriptor of referrers index and the 512 // referrers list. 513 func (r *Repository) referrersFromIndex(ctx context.Context, referrersTag string) (ocispec.Descriptor, []ocispec.Descriptor, error) { 514 desc, rc, err := r.FetchReference(ctx, referrersTag) 515 if err != nil { 516 return ocispec.Descriptor{}, nil, err 517 } 518 defer rc.Close() 519 520 if err := limitSize(desc, r.MaxMetadataBytes); err != nil { 521 return ocispec.Descriptor{}, nil, fmt.Errorf("failed to read referrers index from referrers tag %s: %w", referrersTag, err) 522 } 523 var index ocispec.Index 524 if err := decodeJSON(rc, desc, &index); err != nil { 525 return ocispec.Descriptor{}, nil, fmt.Errorf("failed to decode referrers index from referrers tag %s: %w", referrersTag, err) 526 } 527 528 return desc, index.Manifests, nil 529 } 530 531 // pingReferrers returns true if the Referrers API is available for r. 532 func (r *Repository) pingReferrers(ctx context.Context) (bool, error) { 533 switch r.loadReferrersState() { 534 case referrersStateSupported: 535 return true, nil 536 case referrersStateUnsupported: 537 return false, nil 538 } 539 540 // referrers state is unknown 541 // limit the rate of pinging referrers API 542 r.referrersPingLock.Lock() 543 defer r.referrersPingLock.Unlock() 544 545 switch r.loadReferrersState() { 546 case referrersStateSupported: 547 return true, nil 548 case referrersStateUnsupported: 549 return false, nil 550 } 551 552 ref := r.Reference 553 ref.Reference = zeroDigest 554 ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull) 555 556 url := buildReferrersURL(r.PlainHTTP, ref, "") 557 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 558 if err != nil { 559 return false, err 560 } 561 resp, err := r.client().Do(req) 562 if err != nil { 563 return false, err 564 } 565 defer resp.Body.Close() 566 567 switch resp.StatusCode { 568 case http.StatusOK: 569 r.SetReferrersCapability(true) 570 return true, nil 571 case http.StatusNotFound: 572 if err := errutil.ParseErrorResponse(resp); errutil.IsErrorCode(err, errcode.ErrorCodeNameUnknown) { 573 // repository not found 574 return false, err 575 } 576 r.SetReferrersCapability(false) 577 return false, nil 578 default: 579 return false, errutil.ParseErrorResponse(resp) 580 } 581 } 582 583 // delete removes the content identified by the descriptor in the entity "blobs" 584 // or "manifests". 585 func (r *Repository) delete(ctx context.Context, target ocispec.Descriptor, isManifest bool) error { 586 ref := r.Reference 587 ref.Reference = target.Digest.String() 588 ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionDelete) 589 buildURL := buildRepositoryBlobURL 590 if isManifest { 591 buildURL = buildRepositoryManifestURL 592 } 593 url := buildURL(r.PlainHTTP, ref) 594 req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) 595 if err != nil { 596 return err 597 } 598 599 resp, err := r.client().Do(req) 600 if err != nil { 601 return err 602 } 603 defer resp.Body.Close() 604 605 switch resp.StatusCode { 606 case http.StatusAccepted: 607 return verifyContentDigest(resp, target.Digest) 608 case http.StatusNotFound: 609 return fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound) 610 default: 611 return errutil.ParseErrorResponse(resp) 612 } 613 } 614 615 // blobStore accesses the blob part of the repository. 616 type blobStore struct { 617 repo *Repository 618 } 619 620 // Fetch fetches the content identified by the descriptor. 621 func (s *blobStore) Fetch(ctx context.Context, target ocispec.Descriptor) (rc io.ReadCloser, err error) { 622 ref := s.repo.Reference 623 ref.Reference = target.Digest.String() 624 ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull) 625 url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref) 626 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 627 if err != nil { 628 return nil, err 629 } 630 631 resp, err := s.repo.client().Do(req) 632 if err != nil { 633 return nil, err 634 } 635 defer func() { 636 if err != nil { 637 resp.Body.Close() 638 } 639 }() 640 641 switch resp.StatusCode { 642 case http.StatusOK: // server does not support seek as `Range` was ignored. 643 if size := resp.ContentLength; size != -1 && size != target.Size { 644 return nil, fmt.Errorf("%s %q: mismatch Content-Length", resp.Request.Method, resp.Request.URL) 645 } 646 647 // check server range request capability. 648 // Docker spec allows range header form of "Range: bytes=<start>-<end>". 649 // However, the remote server may still not RFC 7233 compliant. 650 // Reference: https://docs.docker.com/registry/spec/api/#blob 651 if rangeUnit := resp.Header.Get("Accept-Ranges"); rangeUnit == "bytes" { 652 return httputil.NewReadSeekCloser(s.repo.client(), req, resp.Body, target.Size), nil 653 } 654 return resp.Body, nil 655 case http.StatusNotFound: 656 return nil, fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound) 657 default: 658 return nil, errutil.ParseErrorResponse(resp) 659 } 660 } 661 662 // Push pushes the content, matching the expected descriptor. 663 // Existing content is not checked by Push() to minimize the number of out-going 664 // requests. 665 // Push is done by conventional 2-step monolithic upload instead of a single 666 // `POST` request for better overall performance. It also allows early fail on 667 // authentication errors. 668 // References: 669 // - https://docs.docker.com/registry/spec/api/#pushing-an-image 670 // - https://docs.docker.com/registry/spec/api/#initiate-blob-upload 671 // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pushing-a-blob-monolithically 672 func (s *blobStore) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { 673 // start an upload 674 // pushing usually requires both pull and push actions. 675 // Reference: https://github.com/distribution/distribution/blob/v2.7.1/registry/handlers/app.go#L921-L930 676 ctx = registryutil.WithScopeHint(ctx, s.repo.Reference, auth.ActionPull, auth.ActionPush) 677 url := buildRepositoryBlobUploadURL(s.repo.PlainHTTP, s.repo.Reference) 678 req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil) 679 if err != nil { 680 return err 681 } 682 reqHostname := req.URL.Hostname() 683 reqPort := req.URL.Port() 684 685 client := s.repo.client() 686 resp, err := client.Do(req) 687 if err != nil { 688 return err 689 } 690 691 if resp.StatusCode != http.StatusAccepted { 692 defer resp.Body.Close() 693 return errutil.ParseErrorResponse(resp) 694 } 695 resp.Body.Close() 696 697 // monolithic upload 698 location, err := resp.Location() 699 if err != nil { 700 return err 701 } 702 // work-around solution for https://github.com/oras-project/oras-go/issues/177 703 // For some registries, if the port 443 is explicitly set to the hostname 704 // like registry.wabbit-networks.io:443/myrepo, blob push will fail since 705 // the hostname of the Location header in the response is set to 706 // registry.wabbit-networks.io instead of registry.wabbit-networks.io:443. 707 locationHostname := location.Hostname() 708 locationPort := location.Port() 709 // if location port 443 is missing, add it back 710 if reqPort == "443" && locationHostname == reqHostname && locationPort == "" { 711 location.Host = locationHostname + ":" + reqPort 712 } 713 url = location.String() 714 req, err = http.NewRequestWithContext(ctx, http.MethodPut, url, content) 715 if err != nil { 716 return err 717 } 718 if req.GetBody != nil && req.ContentLength != expected.Size { 719 // short circuit a size mismatch for built-in types. 720 return fmt.Errorf("mismatch content length %d: expect %d", req.ContentLength, expected.Size) 721 } 722 req.ContentLength = expected.Size 723 // the expected media type is ignored as in the API doc. 724 req.Header.Set("Content-Type", "application/octet-stream") 725 q := req.URL.Query() 726 q.Set("digest", expected.Digest.String()) 727 req.URL.RawQuery = q.Encode() 728 729 // reuse credential from previous POST request 730 if auth := resp.Request.Header.Get("Authorization"); auth != "" { 731 req.Header.Set("Authorization", auth) 732 } 733 resp, err = client.Do(req) 734 if err != nil { 735 return err 736 } 737 defer resp.Body.Close() 738 739 if resp.StatusCode != http.StatusCreated { 740 return errutil.ParseErrorResponse(resp) 741 } 742 return nil 743 } 744 745 // Exists returns true if the described content exists. 746 func (s *blobStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { 747 _, err := s.Resolve(ctx, target.Digest.String()) 748 if err == nil { 749 return true, nil 750 } 751 if errors.Is(err, errdef.ErrNotFound) { 752 return false, nil 753 } 754 return false, err 755 } 756 757 // Delete removes the content identified by the descriptor. 758 func (s *blobStore) Delete(ctx context.Context, target ocispec.Descriptor) error { 759 return s.repo.delete(ctx, target, false) 760 } 761 762 // Resolve resolves a reference to a descriptor. 763 func (s *blobStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { 764 ref, err := s.repo.ParseReference(reference) 765 if err != nil { 766 return ocispec.Descriptor{}, err 767 } 768 refDigest, err := ref.Digest() 769 if err != nil { 770 return ocispec.Descriptor{}, err 771 } 772 ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull) 773 url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref) 774 req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) 775 if err != nil { 776 return ocispec.Descriptor{}, err 777 } 778 779 resp, err := s.repo.client().Do(req) 780 if err != nil { 781 return ocispec.Descriptor{}, err 782 } 783 defer resp.Body.Close() 784 785 switch resp.StatusCode { 786 case http.StatusOK: 787 return generateBlobDescriptor(resp, refDigest) 788 case http.StatusNotFound: 789 return ocispec.Descriptor{}, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound) 790 default: 791 return ocispec.Descriptor{}, errutil.ParseErrorResponse(resp) 792 } 793 } 794 795 // FetchReference fetches the blob identified by the reference. 796 // The reference must be a digest. 797 func (s *blobStore) FetchReference(ctx context.Context, reference string) (desc ocispec.Descriptor, rc io.ReadCloser, err error) { 798 ref, err := s.repo.ParseReference(reference) 799 if err != nil { 800 return ocispec.Descriptor{}, nil, err 801 } 802 refDigest, err := ref.Digest() 803 if err != nil { 804 return ocispec.Descriptor{}, nil, err 805 } 806 807 ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull) 808 url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref) 809 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 810 if err != nil { 811 return ocispec.Descriptor{}, nil, err 812 } 813 814 resp, err := s.repo.client().Do(req) 815 if err != nil { 816 return ocispec.Descriptor{}, nil, err 817 } 818 defer func() { 819 if err != nil { 820 resp.Body.Close() 821 } 822 }() 823 824 switch resp.StatusCode { 825 case http.StatusOK: // server does not support seek as `Range` was ignored. 826 if resp.ContentLength == -1 { 827 desc, err = s.Resolve(ctx, reference) 828 } else { 829 desc, err = generateBlobDescriptor(resp, refDigest) 830 } 831 if err != nil { 832 return ocispec.Descriptor{}, nil, err 833 } 834 835 // check server range request capability. 836 // Docker spec allows range header form of "Range: bytes=<start>-<end>". 837 // However, the remote server may still not RFC 7233 compliant. 838 // Reference: https://docs.docker.com/registry/spec/api/#blob 839 if rangeUnit := resp.Header.Get("Accept-Ranges"); rangeUnit == "bytes" { 840 return desc, httputil.NewReadSeekCloser(s.repo.client(), req, resp.Body, desc.Size), nil 841 } 842 return desc, resp.Body, nil 843 case http.StatusNotFound: 844 return ocispec.Descriptor{}, nil, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound) 845 default: 846 return ocispec.Descriptor{}, nil, errutil.ParseErrorResponse(resp) 847 } 848 } 849 850 // generateBlobDescriptor returns a descriptor generated from the response. 851 func generateBlobDescriptor(resp *http.Response, refDigest digest.Digest) (ocispec.Descriptor, error) { 852 mediaType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) 853 if mediaType == "" { 854 mediaType = "application/octet-stream" 855 } 856 857 size := resp.ContentLength 858 if size == -1 { 859 return ocispec.Descriptor{}, fmt.Errorf("%s %q: unknown response Content-Length", resp.Request.Method, resp.Request.URL) 860 } 861 862 if err := verifyContentDigest(resp, refDigest); err != nil { 863 return ocispec.Descriptor{}, err 864 } 865 866 return ocispec.Descriptor{ 867 MediaType: mediaType, 868 Digest: refDigest, 869 Size: size, 870 }, nil 871 } 872 873 // manifestStore accesses the manifest part of the repository. 874 type manifestStore struct { 875 repo *Repository 876 } 877 878 // Fetch fetches the content identified by the descriptor. 879 func (s *manifestStore) Fetch(ctx context.Context, target ocispec.Descriptor) (rc io.ReadCloser, err error) { 880 ref := s.repo.Reference 881 ref.Reference = target.Digest.String() 882 ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull) 883 url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref) 884 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 885 if err != nil { 886 return nil, err 887 } 888 req.Header.Set("Accept", target.MediaType) 889 890 resp, err := s.repo.client().Do(req) 891 if err != nil { 892 return nil, err 893 } 894 defer func() { 895 if err != nil { 896 resp.Body.Close() 897 } 898 }() 899 900 switch resp.StatusCode { 901 case http.StatusOK: 902 // no-op 903 case http.StatusNotFound: 904 return nil, fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound) 905 default: 906 return nil, errutil.ParseErrorResponse(resp) 907 } 908 mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 909 if err != nil { 910 return nil, fmt.Errorf("%s %q: invalid response Content-Type: %w", resp.Request.Method, resp.Request.URL, err) 911 } 912 if mediaType != target.MediaType { 913 return nil, fmt.Errorf("%s %q: mismatch response Content-Type %q: expect %q", resp.Request.Method, resp.Request.URL, mediaType, target.MediaType) 914 } 915 if size := resp.ContentLength; size != -1 && size != target.Size { 916 return nil, fmt.Errorf("%s %q: mismatch Content-Length", resp.Request.Method, resp.Request.URL) 917 } 918 if err := verifyContentDigest(resp, target.Digest); err != nil { 919 return nil, err 920 } 921 return resp.Body, nil 922 } 923 924 // Push pushes the content, matching the expected descriptor. 925 func (s *manifestStore) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { 926 return s.pushWithIndexing(ctx, expected, content, expected.Digest.String()) 927 } 928 929 // Exists returns true if the described content exists. 930 func (s *manifestStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { 931 _, err := s.Resolve(ctx, target.Digest.String()) 932 if err == nil { 933 return true, nil 934 } 935 if errors.Is(err, errdef.ErrNotFound) { 936 return false, nil 937 } 938 return false, err 939 } 940 941 // Delete removes the manifest content identified by the descriptor. 942 func (s *manifestStore) Delete(ctx context.Context, target ocispec.Descriptor) error { 943 return s.deleteWithIndexing(ctx, target) 944 } 945 946 // deleteWithIndexing removes the manifest content identified by the descriptor, 947 // and indexes referrers for the manifest when needed. 948 func (s *manifestStore) deleteWithIndexing(ctx context.Context, target ocispec.Descriptor) error { 949 if target.MediaType == ocispec.MediaTypeImageManifest { 950 if state := s.repo.loadReferrersState(); state == referrersStateSupported { 951 // referrers API is available, no client-side indexing needed 952 return s.repo.delete(ctx, target, true) 953 } 954 955 if err := limitSize(target, s.repo.MaxMetadataBytes); err != nil { 956 return err 957 } 958 manifestJSON, err := content.FetchAll(ctx, s, target) 959 if err != nil { 960 return err 961 } 962 if err := s.indexReferrersForDelete(ctx, target, manifestJSON); err != nil { 963 return err 964 } 965 } 966 967 return s.repo.delete(ctx, target, true) 968 } 969 970 // indexReferrersForDelete indexes referrers for image or artifact manifest with 971 // the subject field on manifest delete. 972 // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#deleting-manifests 973 func (s *manifestStore) indexReferrersForDelete(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error { 974 var manifest struct { 975 Subject *ocispec.Descriptor `json:"subject"` 976 } 977 if err := json.Unmarshal(manifestJSON, &manifest); err != nil { 978 return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err) 979 } 980 if manifest.Subject == nil { 981 // no subject, no indexing needed 982 return nil 983 } 984 985 subject := *manifest.Subject 986 ok, err := s.repo.pingReferrers(ctx) 987 if err != nil { 988 return err 989 } 990 if ok { 991 // referrers API is available, no client-side indexing needed 992 return nil 993 } 994 return s.updateReferrersIndex(ctx, subject, referrerChange{desc, referrerOperationRemove}) 995 } 996 997 // Resolve resolves a reference to a descriptor. 998 // See also `ManifestMediaTypes`. 999 func (s *manifestStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { 1000 ref, err := s.repo.ParseReference(reference) 1001 if err != nil { 1002 return ocispec.Descriptor{}, err 1003 } 1004 ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull) 1005 url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref) 1006 req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil) 1007 if err != nil { 1008 return ocispec.Descriptor{}, err 1009 } 1010 req.Header.Set("Accept", manifestAcceptHeader(s.repo.ManifestMediaTypes)) 1011 1012 resp, err := s.repo.client().Do(req) 1013 if err != nil { 1014 return ocispec.Descriptor{}, err 1015 } 1016 defer resp.Body.Close() 1017 1018 switch resp.StatusCode { 1019 case http.StatusOK: 1020 return s.generateDescriptor(resp, ref, req.Method) 1021 case http.StatusNotFound: 1022 return ocispec.Descriptor{}, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound) 1023 default: 1024 return ocispec.Descriptor{}, errutil.ParseErrorResponse(resp) 1025 } 1026 } 1027 1028 // FetchReference fetches the manifest identified by the reference. 1029 // The reference can be a tag or digest. 1030 func (s *manifestStore) FetchReference(ctx context.Context, reference string) (desc ocispec.Descriptor, rc io.ReadCloser, err error) { 1031 ref, err := s.repo.ParseReference(reference) 1032 if err != nil { 1033 return ocispec.Descriptor{}, nil, err 1034 } 1035 1036 ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull) 1037 url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref) 1038 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 1039 if err != nil { 1040 return ocispec.Descriptor{}, nil, err 1041 } 1042 req.Header.Set("Accept", manifestAcceptHeader(s.repo.ManifestMediaTypes)) 1043 1044 resp, err := s.repo.client().Do(req) 1045 if err != nil { 1046 return ocispec.Descriptor{}, nil, err 1047 } 1048 defer func() { 1049 if err != nil { 1050 resp.Body.Close() 1051 } 1052 }() 1053 1054 switch resp.StatusCode { 1055 case http.StatusOK: 1056 if resp.ContentLength == -1 { 1057 desc, err = s.Resolve(ctx, reference) 1058 } else { 1059 desc, err = s.generateDescriptor(resp, ref, req.Method) 1060 } 1061 if err != nil { 1062 return ocispec.Descriptor{}, nil, err 1063 } 1064 return desc, resp.Body, nil 1065 case http.StatusNotFound: 1066 return ocispec.Descriptor{}, nil, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound) 1067 default: 1068 return ocispec.Descriptor{}, nil, errutil.ParseErrorResponse(resp) 1069 } 1070 } 1071 1072 // Tag tags a manifest descriptor with a reference string. 1073 func (s *manifestStore) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error { 1074 ref, err := s.repo.ParseReference(reference) 1075 if err != nil { 1076 return err 1077 } 1078 1079 ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull, auth.ActionPush) 1080 rc, err := s.Fetch(ctx, desc) 1081 if err != nil { 1082 return err 1083 } 1084 defer rc.Close() 1085 1086 return s.push(ctx, desc, rc, ref.Reference) 1087 } 1088 1089 // PushReference pushes the manifest with a reference tag. 1090 func (s *manifestStore) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { 1091 ref, err := s.repo.ParseReference(reference) 1092 if err != nil { 1093 return err 1094 } 1095 return s.pushWithIndexing(ctx, expected, content, ref.Reference) 1096 } 1097 1098 // push pushes the manifest content, matching the expected descriptor. 1099 func (s *manifestStore) push(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error { 1100 ref := s.repo.Reference 1101 ref.Reference = reference 1102 // pushing usually requires both pull and push actions. 1103 // Reference: https://github.com/distribution/distribution/blob/v2.7.1/registry/handlers/app.go#L921-L930 1104 ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull, auth.ActionPush) 1105 url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref) 1106 // unwrap the content for optimizations of built-in types. 1107 body := ioutil.UnwrapNopCloser(content) 1108 if _, ok := body.(io.ReadCloser); ok { 1109 // undo unwrap if the nopCloser is intended. 1110 body = content 1111 } 1112 req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, body) 1113 if err != nil { 1114 return err 1115 } 1116 if req.GetBody != nil && req.ContentLength != expected.Size { 1117 // short circuit a size mismatch for built-in types. 1118 return fmt.Errorf("mismatch content length %d: expect %d", req.ContentLength, expected.Size) 1119 } 1120 req.ContentLength = expected.Size 1121 req.Header.Set("Content-Type", expected.MediaType) 1122 1123 // if the underlying client is an auth client, the content might be read 1124 // more than once for obtaining the auth challenge and the actual request. 1125 // To prevent double reading, the manifest is read and stored in the memory, 1126 // and serve from the memory. 1127 client := s.repo.client() 1128 if _, ok := client.(*auth.Client); ok && req.GetBody == nil { 1129 store := cas.NewMemory() 1130 err := store.Push(ctx, expected, content) 1131 if err != nil { 1132 return err 1133 } 1134 req.GetBody = func() (io.ReadCloser, error) { 1135 return store.Fetch(ctx, expected) 1136 } 1137 req.Body, err = req.GetBody() 1138 if err != nil { 1139 return err 1140 } 1141 } 1142 resp, err := client.Do(req) 1143 if err != nil { 1144 return err 1145 } 1146 defer resp.Body.Close() 1147 1148 if resp.StatusCode != http.StatusCreated { 1149 return errutil.ParseErrorResponse(resp) 1150 } 1151 return verifyContentDigest(resp, expected.Digest) 1152 } 1153 1154 // pushWithIndexing pushes the manifest content matching the expected descriptor, 1155 // and indexes referrers for the manifest when needed. 1156 func (s *manifestStore) pushWithIndexing(ctx context.Context, expected ocispec.Descriptor, r io.Reader, reference string) error { 1157 switch expected.MediaType { 1158 case ocispec.MediaTypeImageManifest: 1159 if state := s.repo.loadReferrersState(); state == referrersStateSupported { 1160 // referrers API is available, no client-side indexing needed 1161 return s.push(ctx, expected, r, reference) 1162 } 1163 1164 if err := limitSize(expected, s.repo.MaxMetadataBytes); err != nil { 1165 return err 1166 } 1167 manifestJSON, err := content.ReadAll(r, expected) 1168 if err != nil { 1169 return err 1170 } 1171 if err := s.push(ctx, expected, bytes.NewReader(manifestJSON), reference); err != nil { 1172 return err 1173 } 1174 return s.indexReferrersForPush(ctx, expected, manifestJSON) 1175 default: 1176 return s.push(ctx, expected, r, reference) 1177 } 1178 } 1179 1180 // indexReferrersForPush indexes referrers for image or artifact manifest with 1181 // the subject field on manifest push. 1182 // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pushing-manifests-with-subject 1183 func (s *manifestStore) indexReferrersForPush(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error { 1184 var subject ocispec.Descriptor 1185 switch desc.MediaType { 1186 case ocispec.MediaTypeImageManifest: 1187 var manifest ocispec.Manifest 1188 if err := json.Unmarshal(manifestJSON, &manifest); err != nil { 1189 return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err) 1190 } 1191 if manifest.Subject == nil { 1192 // no subject, no indexing needed 1193 return nil 1194 } 1195 subject = *manifest.Subject 1196 desc.ArtifactType = manifest.Config.MediaType 1197 desc.Annotations = manifest.Annotations 1198 default: 1199 return nil 1200 } 1201 1202 ok, err := s.repo.pingReferrers(ctx) 1203 if err != nil { 1204 return err 1205 } 1206 if ok { 1207 // referrers API is available, no client-side indexing needed 1208 return nil 1209 } 1210 return s.updateReferrersIndex(ctx, subject, referrerChange{desc, referrerOperationAdd}) 1211 } 1212 1213 // updateReferrersIndex updates the referrers index for desc referencing subject 1214 // on manifest push and manifest delete. 1215 // References: 1216 // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pushing-manifests-with-subject 1217 // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#deleting-manifests 1218 func (s *manifestStore) updateReferrersIndex(ctx context.Context, subject ocispec.Descriptor, change referrerChange) (err error) { 1219 referrersTag := buildReferrersTag(subject) 1220 1221 var skipDelete bool 1222 var oldIndexDesc ocispec.Descriptor 1223 var referrers []ocispec.Descriptor 1224 prepare := func() error { 1225 // 1. pull the original referrers list using the referrers tag schema 1226 var err error 1227 oldIndexDesc, referrers, err = s.repo.referrersFromIndex(ctx, referrersTag) 1228 if err != nil { 1229 if errors.Is(err, errdef.ErrNotFound) { 1230 // no old index found, skip delete 1231 skipDelete = true 1232 return nil 1233 } 1234 return err 1235 } 1236 return nil 1237 } 1238 update := func(referrerChanges []referrerChange) error { 1239 // 2. apply the referrer changes on the referrers list 1240 updatedReferrers, err := applyReferrerChanges(referrers, referrerChanges) 1241 if err != nil { 1242 if err == errNoReferrerUpdate { 1243 return nil 1244 } 1245 return err 1246 } 1247 1248 // 3. push the updated referrers list using referrers tag schema 1249 if len(updatedReferrers) > 0 { 1250 newIndexDesc, newIndex, err := generateIndex(updatedReferrers) 1251 if err != nil { 1252 return fmt.Errorf("failed to generate referrers index for referrers tag %s: %w", referrersTag, err) 1253 } 1254 if err := s.push(ctx, newIndexDesc, bytes.NewReader(newIndex), referrersTag); err != nil { 1255 return fmt.Errorf("failed to push referrers index tagged by %s: %w", referrersTag, err) 1256 } 1257 } 1258 1259 // 4. delete the dangling original referrers index 1260 if !skipDelete { 1261 if err := s.repo.delete(ctx, oldIndexDesc, true); err != nil { 1262 return fmt.Errorf("failed to delete dangling referrers index %s for referrers tag %s: %w", oldIndexDesc.Digest.String(), referrersTag, err) 1263 } 1264 } 1265 return nil 1266 } 1267 1268 merge, done := s.repo.referrersMergePool.Get(referrersTag) 1269 defer done() 1270 return merge.Do(change, prepare, update) 1271 } 1272 1273 // ParseReference parses a reference to a fully qualified reference. 1274 func (s *manifestStore) ParseReference(reference string) (registry.Reference, error) { 1275 return s.repo.ParseReference(reference) 1276 } 1277 1278 // generateDescriptor returns a descriptor generated from the response. 1279 // See the truth table at the top of `repository_test.go` 1280 func (s *manifestStore) generateDescriptor(resp *http.Response, ref registry.Reference, httpMethod string) (ocispec.Descriptor, error) { 1281 // 1. Validate Content-Type 1282 mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 1283 if err != nil { 1284 return ocispec.Descriptor{}, fmt.Errorf( 1285 "%s %q: invalid response `Content-Type` header; %w", 1286 resp.Request.Method, 1287 resp.Request.URL, 1288 err, 1289 ) 1290 } 1291 1292 // 2. Validate Size 1293 if resp.ContentLength == -1 { 1294 return ocispec.Descriptor{}, fmt.Errorf( 1295 "%s %q: unknown response Content-Length", 1296 resp.Request.Method, 1297 resp.Request.URL, 1298 ) 1299 } 1300 1301 // 3. Validate Client Reference 1302 var refDigest digest.Digest 1303 if d, err := ref.Digest(); err == nil { 1304 refDigest = d 1305 } 1306 1307 // 4. Validate Server Digest (if present) 1308 var serverHeaderDigest digest.Digest 1309 if serverHeaderDigestStr := resp.Header.Get(dockerContentDigestHeader); serverHeaderDigestStr != "" { 1310 if serverHeaderDigest, err = digest.Parse(serverHeaderDigestStr); err != nil { 1311 return ocispec.Descriptor{}, fmt.Errorf( 1312 "%s %q: invalid response header value: `%s: %s`; %w", 1313 resp.Request.Method, 1314 resp.Request.URL, 1315 dockerContentDigestHeader, 1316 serverHeaderDigestStr, 1317 err, 1318 ) 1319 } 1320 } 1321 1322 /* 5. Now, look for specific error conditions; see truth table in method docstring */ 1323 var contentDigest digest.Digest 1324 1325 if len(serverHeaderDigest) == 0 { 1326 if httpMethod == http.MethodHead { 1327 if len(refDigest) == 0 { 1328 // HEAD without server `Docker-Content-Digest` header is an 1329 // immediate fail 1330 return ocispec.Descriptor{}, fmt.Errorf( 1331 "HTTP %s request missing required header %q", 1332 httpMethod, dockerContentDigestHeader, 1333 ) 1334 } 1335 // Otherwise, just trust the client-supplied digest 1336 contentDigest = refDigest 1337 } else { 1338 // GET without server `Docker-Content-Digest` header forces the 1339 // expensive calculation 1340 var calculatedDigest digest.Digest 1341 if calculatedDigest, err = calculateDigestFromResponse(resp, s.repo.MaxMetadataBytes); err != nil { 1342 return ocispec.Descriptor{}, fmt.Errorf("failed to calculate digest on response body; %w", err) 1343 } 1344 contentDigest = calculatedDigest 1345 } 1346 } else { 1347 contentDigest = serverHeaderDigest 1348 } 1349 1350 if len(refDigest) > 0 && refDigest != contentDigest { 1351 return ocispec.Descriptor{}, fmt.Errorf( 1352 "%s %q: invalid response; digest mismatch in %s: received %q when expecting %q", 1353 resp.Request.Method, resp.Request.URL, 1354 dockerContentDigestHeader, contentDigest, 1355 refDigest, 1356 ) 1357 } 1358 1359 // 6. Finally, if we made it this far, then all is good; return. 1360 return ocispec.Descriptor{ 1361 MediaType: mediaType, 1362 Digest: contentDigest, 1363 Size: resp.ContentLength, 1364 }, nil 1365 } 1366 1367 // calculateDigestFromResponse calculates the actual digest of the response body 1368 // taking care not to destroy it in the process. 1369 func calculateDigestFromResponse(resp *http.Response, maxMetadataBytes int64) (digest.Digest, error) { 1370 defer resp.Body.Close() 1371 1372 body := limitReader(resp.Body, maxMetadataBytes) 1373 content, err := io.ReadAll(body) 1374 if err != nil { 1375 return "", fmt.Errorf("%s %q: failed to read response body: %w", resp.Request.Method, resp.Request.URL, err) 1376 } 1377 resp.Body = io.NopCloser(bytes.NewReader(content)) 1378 1379 return digest.FromBytes(content), nil 1380 } 1381 1382 // verifyContentDigest verifies "Docker-Content-Digest" header if present. 1383 // OCI distribution-spec states the Docker-Content-Digest header is optional. 1384 // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#legacy-docker-support-http-headers 1385 func verifyContentDigest(resp *http.Response, expected digest.Digest) error { 1386 digestStr := resp.Header.Get(dockerContentDigestHeader) 1387 1388 if len(digestStr) == 0 { 1389 return nil 1390 } 1391 1392 contentDigest, err := digest.Parse(digestStr) 1393 if err != nil { 1394 return fmt.Errorf( 1395 "%s %q: invalid response header: `%s: %s`", 1396 resp.Request.Method, resp.Request.URL, 1397 dockerContentDigestHeader, digestStr, 1398 ) 1399 } 1400 1401 if contentDigest != expected { 1402 return fmt.Errorf( 1403 "%s %q: invalid response; digest mismatch in %s: received %q when expecting %q", 1404 resp.Request.Method, resp.Request.URL, 1405 dockerContentDigestHeader, contentDigest, 1406 expected, 1407 ) 1408 } 1409 1410 return nil 1411 } 1412 1413 // generateIndex generates an image index containing the given manifests list. 1414 func generateIndex(manifests []ocispec.Descriptor) (ocispec.Descriptor, []byte, error) { 1415 if manifests == nil { 1416 manifests = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs 1417 } 1418 index := ocispec.Index{ 1419 Versioned: specs.Versioned{ 1420 SchemaVersion: 2, // historical value. does not pertain to OCI or docker version 1421 }, 1422 MediaType: ocispec.MediaTypeImageIndex, 1423 Manifests: manifests, 1424 } 1425 indexJSON, err := json.Marshal(index) 1426 if err != nil { 1427 return ocispec.Descriptor{}, nil, err 1428 } 1429 indexDesc := content.NewDescriptorFromBytes(index.MediaType, indexJSON) 1430 return indexDesc, indexJSON, nil 1431 }