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