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