diff --git a/go.mod b/go.mod index 04a48699..c644e0af 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/SovereignCloudStack/csctl -go 1.22.0 - -toolchain go1.22.12 +go 1.25.0 require ( github.com/SovereignCloudStack/cluster-stack-operator v0.1.0-alpha.9 @@ -15,7 +13,7 @@ require ( gopkg.in/src-d/go-git.v4 v4.13.1 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.16.4 - oras.land/oras-go/v2 v2.5.0 + oras.land/oras-go/v2 v2.6.1 ) require ( @@ -134,7 +132,7 @@ require ( go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/net v0.33.0 // indirect - golang.org/x/sync v0.10.0 // indirect + golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect diff --git a/go.sum b/go.sum index 21b8ae23..4b099f8b 100644 --- a/go.sum +++ b/go.sum @@ -468,8 +468,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -593,8 +593,8 @@ k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= -oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c= -oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg= +oras.land/oras-go/v2 v2.6.1 h1:bonOEkjLfp8tt6qXWRRWP6p1F+9octchOf2EqnWB4Zs= +oras.land/oras-go/v2 v2.6.1/go.mod h1:dhtFrFOuZuDtAVeZ9FUnaa5zfzplG3ZnFX9/uH1J/Yk= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.17.2 h1:E7/Fjk7V5fboiuijoZHgs4aHuexi5Y2loXlVOAVAG5g= diff --git a/vendor/golang.org/x/sync/errgroup/errgroup.go b/vendor/golang.org/x/sync/errgroup/errgroup.go index 948a3ee6..f69fd754 100644 --- a/vendor/golang.org/x/sync/errgroup/errgroup.go +++ b/vendor/golang.org/x/sync/errgroup/errgroup.go @@ -3,7 +3,7 @@ // license that can be found in the LICENSE file. // Package errgroup provides synchronization, error propagation, and Context -// cancelation for groups of goroutines working on subtasks of a common task. +// cancellation for groups of goroutines working on subtasks of a common task. // // [errgroup.Group] is related to [sync.WaitGroup] but adds handling of tasks // returning errors. @@ -18,7 +18,7 @@ import ( type token struct{} // A Group is a collection of goroutines working on subtasks that are part of -// the same overall task. +// the same overall task. A Group should not be reused for different tasks. // // A zero Group is valid, has no limit on the number of active goroutines, // and does not cancel on error. @@ -46,7 +46,7 @@ func (g *Group) done() { // returns a non-nil error or the first time Wait returns, whichever occurs // first. func WithContext(ctx context.Context) (*Group, context.Context) { - ctx, cancel := withCancelCause(ctx) + ctx, cancel := context.WithCancelCause(ctx) return &Group{cancel: cancel}, ctx } @@ -61,11 +61,14 @@ func (g *Group) Wait() error { } // Go calls the given function in a new goroutine. +// +// The first call to Go must happen before a Wait. // It blocks until the new goroutine can be added without the number of -// active goroutines in the group exceeding the configured limit. +// goroutines in the group exceeding the configured limit. // -// The first call to return a non-nil error cancels the group's context, if the -// group was created by calling WithContext. The error will be returned by Wait. +// The first goroutine in the group that returns a non-nil error will +// cancel the associated Context, if any. The error will be returned +// by Wait. func (g *Group) Go(f func() error) { if g.sem != nil { g.sem <- token{} @@ -75,6 +78,18 @@ func (g *Group) Go(f func() error) { go func() { defer g.done() + // It is tempting to propagate panics from f() + // up to the goroutine that calls Wait, but + // it creates more problems than it solves: + // - it delays panics arbitrarily, + // making bugs harder to detect; + // - it turns f's panic stack into a mere value, + // hiding it from crash-monitoring tools; + // - it risks deadlocks that hide the panic entirely, + // if f's panic leaves the program in a state + // that prevents the Wait call from being reached. + // See #53757, #74275, #74304, #74306. + if err := f(); err != nil { g.errOnce.Do(func() { g.err = err @@ -118,6 +133,7 @@ func (g *Group) TryGo(f func() error) bool { // SetLimit limits the number of active goroutines in this group to at most n. // A negative value indicates no limit. +// A limit of zero will prevent any new goroutines from being added. // // Any subsequent call to the Go method will block until it can add an active // goroutine without exceeding the configured limit. @@ -128,8 +144,8 @@ func (g *Group) SetLimit(n int) { g.sem = nil return } - if len(g.sem) != 0 { - panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", len(g.sem))) + if active := len(g.sem); active != 0 { + panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", active)) } g.sem = make(chan token, n) } diff --git a/vendor/golang.org/x/sync/errgroup/go120.go b/vendor/golang.org/x/sync/errgroup/go120.go deleted file mode 100644 index f93c740b..00000000 --- a/vendor/golang.org/x/sync/errgroup/go120.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.20 - -package errgroup - -import "context" - -func withCancelCause(parent context.Context) (context.Context, func(error)) { - return context.WithCancelCause(parent) -} diff --git a/vendor/golang.org/x/sync/errgroup/pre_go120.go b/vendor/golang.org/x/sync/errgroup/pre_go120.go deleted file mode 100644 index 88ce3343..00000000 --- a/vendor/golang.org/x/sync/errgroup/pre_go120.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !go1.20 - -package errgroup - -import "context" - -func withCancelCause(parent context.Context) (context.Context, func(error)) { - ctx, cancel := context.WithCancel(parent) - return ctx, func(error) { cancel() } -} diff --git a/vendor/modules.txt b/vendor/modules.txt index f1275f02..42ab5277 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -509,8 +509,8 @@ golang.org/x/net/websocket ## explicit; go 1.18 golang.org/x/oauth2 golang.org/x/oauth2/internal -# golang.org/x/sync v0.10.0 -## explicit; go 1.18 +# golang.org/x/sync v0.20.0 +## explicit; go 1.25.0 golang.org/x/sync/errgroup golang.org/x/sync/semaphore # golang.org/x/sys v0.28.0 @@ -1049,8 +1049,8 @@ oras.land/oras-go/pkg/registry/remote/auth oras.land/oras-go/pkg/registry/remote/internal/errutil oras.land/oras-go/pkg/registry/remote/internal/syncutil oras.land/oras-go/pkg/target -# oras.land/oras-go/v2 v2.5.0 -## explicit; go 1.21 +# oras.land/oras-go/v2 v2.6.1 +## explicit; go 1.25.0 oras.land/oras-go/v2 oras.land/oras-go/v2/content oras.land/oras-go/v2/content/file diff --git a/vendor/oras.land/oras-go/v2/.goreleaser.yaml b/vendor/oras.land/oras-go/v2/.goreleaser.yaml new file mode 100644 index 00000000..b4ad1873 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/.goreleaser.yaml @@ -0,0 +1,26 @@ +# Copyright The ORAS Authors. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +version: 2 + +# oras-go is a library — no binary builds or archives needed. +builds: + - skip: true + +checksum: + disable: true + +release: + # Tags containing -alpha, -beta, or -rc are automatically marked pre-release. + prerelease: auto + draft: false diff --git a/vendor/oras.land/oras-go/v2/CODEOWNERS b/vendor/oras.land/oras-go/v2/CODEOWNERS index 45a68a31..f3858e76 100644 --- a/vendor/oras.land/oras-go/v2/CODEOWNERS +++ b/vendor/oras.land/oras-go/v2/CODEOWNERS @@ -1,2 +1,2 @@ # Derived from OWNERS.md -* @sajayantony @shizhMSFT @stevelasker @Wwwsylvia +* @sabre1041 @shizhMSFT @TerryHowe @Wwwsylvia diff --git a/vendor/oras.land/oras-go/v2/MIGRATION_GUIDE.md b/vendor/oras.land/oras-go/v2/MIGRATION_GUIDE.md index b9292f14..9a922f5a 100644 --- a/vendor/oras.land/oras-go/v2/MIGRATION_GUIDE.md +++ b/vendor/oras.land/oras-go/v2/MIGRATION_GUIDE.md @@ -7,19 +7,21 @@ In version `v2`, ORAS Go library has been completely refreshed with: - Higher test coverage - Better documentation -**Besides, ORAS Go `v2` is now a registry client.** +**Additionally, ORAS Go `v2` is now a registry client.** ## Major Changes in `v2` -- Moves `content.FileStore` to [file.Store](https://pkg.go.dev/oras.land/oras-go/v2/content/file#Store) -- Moves `content.OCIStore` to [oci.Store](https://pkg.go.dev/oras.land/oras-go/v2/content/oci#Store) -- Moves `content.MemoryStore` to [memory.Store](https://pkg.go.dev/oras.land/oras-go/v2/content/memory#Store) -- Provides [SDK](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote) to interact with OCI-compliant and Docker-compliant registries -- Supports [Copy](https://pkg.go.dev/oras.land/oras-go/v2#Copy) with more flexible options -- Supports [Extended Copy](https://pkg.go.dev/oras.land/oras-go/v2#ExtendedCopy) with options *(experimental)* -- No longer supports `docker.Login` and `docker.Logout` (removes the dependency on `docker`); instead, provides authentication through [auth.Client](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/auth#Client) - -Documentation and examples are available at [pkg.go.dev](https://pkg.go.dev/oras.land/oras-go/v2). +- Content store + - [`content.File`](https://pkg.go.dev/oras.land/oras-go/pkg/content#File) is now [`file.Store`](https://pkg.go.dev/oras.land/oras-go/v2/content/file#Store) + - [`content.OCI`](https://pkg.go.dev/oras.land/oras-go/pkg/content#OCI) is now [`oci.Store`](https://pkg.go.dev/oras.land/oras-go/v2/content/oci#Store) + - [`content.Memory`](https://pkg.go.dev/oras.land/oras-go/pkg/content#Memory) is now [`memory.Store`](https://pkg.go.dev/oras.land/oras-go/v2/content/memory#Store) +- Registry interaction + - Introduces an [SDK](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote) to interact with OCI-compliant and Docker-compliant registries +- Authentication + - Implements authentication through [`auth.Client`](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/auth#Client) and supports credential management via [`credentials`](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials) +- Copy operations + - Enhances artifact [copying](https://pkg.go.dev/oras.land/oras-go/v2#Copy) capabilities between various [`Target`](https://pkg.go.dev/oras.land/oras-go/v2#Target) with flexible options + - Enables [extended-copying](https://pkg.go.dev/oras.land/oras-go/v2#ExtendedCopy) of artifacts along with their predecessors (e.g., referrers) ## Migrating from `v1` to `v2` @@ -43,3 +45,17 @@ Documentation and examples are available at [pkg.go.dev](https://pkg.go.dev/oras Since breaking changes are introduced in `v2`, code refactoring is required for migrating from `v1` to `v2`. The migration can be done in an iterative fashion, as `v1` and `v2` can be imported and used at the same time. + +For comprehensive documentation and examples, please refer to [pkg.go.dev](https://pkg.go.dev/oras.land/oras-go/v2). + +## FAQs + +### Is there a 1:1 mapping of APIs between `v1` and `v2`? + +No, `v2` does not have a direct 1:1 mapping of APIs with `v1`, as the structure of the APIs has been significantly redesigned. Instead of looking for a direct replacement, see this as a chance to upgrade your application with `v2`'s new features. + +You can explore the [end-to-end examples](https://pkg.go.dev/oras.land/oras-go/v2#pkg-overview) that demonstrate the usage of v2 in practical scenarios. + +## Community Support + +If you encounter challenges during migration, seek assistance from the community by [submitting GitHub issues](https://github.com/oras-project/oras-go/issues/new) or asking in the [#oras](https://cloud-native.slack.com/archives/CJ1KHJM5Z) Slack channel. diff --git a/vendor/oras.land/oras-go/v2/Makefile b/vendor/oras.land/oras-go/v2/Makefile index bc671e44..d0af0a73 100644 --- a/vendor/oras.land/oras-go/v2/Makefile +++ b/vendor/oras.land/oras-go/v2/Makefile @@ -26,12 +26,10 @@ clean: .PHONY: check-encoding check-encoding: ! find . -not -path "./vendor/*" -name "*.go" -type f -exec file "{}" ";" | grep CRLF - ! find scripts -name "*.sh" -type f -exec file "{}" ";" | grep CRLF .PHONY: fix-encoding fix-encoding: find . -not -path "./vendor/*" -name "*.go" -type f -exec sed -i -e "s/\r//g" {} + - find scripts -name "*.sh" -type f -exec sed -i -e "s/\r//g" {} + .PHONY: vendor vendor: diff --git a/vendor/oras.land/oras-go/v2/OWNERS.md b/vendor/oras.land/oras-go/v2/OWNERS.md index 402c4a97..4d29e5eb 100644 --- a/vendor/oras.land/oras-go/v2/OWNERS.md +++ b/vendor/oras.land/oras-go/v2/OWNERS.md @@ -1,11 +1,13 @@ # Owners Owners: - - Sajay Antony (@sajayantony) + - Andrew Block (@sabre1041) - Shiwei Zhang (@shizhMSFT) - - Steve Lasker (@stevelasker) - Sylvia Lei (@Wwwsylvia) + - Terry Howe (@TerryHowe) Emeritus: - Avi Deitcher (@deitch) - Josh Dolitsky (@jdolitsky) + - Sajay Antony (@sajayantony) + - Steve Lasker (@stevelasker) diff --git a/vendor/oras.land/oras-go/v2/README.md b/vendor/oras.land/oras-go/v2/README.md index 7c3013c7..31bdb47c 100644 --- a/vendor/oras.land/oras-go/v2/README.md +++ b/vendor/oras.land/oras-go/v2/README.md @@ -1,58 +1,66 @@ # ORAS Go library +[](https://github.com/oras-project/oras-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Amain) +[](https://codecov.io/gh/oras-project/oras-go) +[](https://goreportcard.com/report/oras.land/oras-go/v2) +[](https://pkg.go.dev/oras.land/oras-go/v2) +
-## Project status +`oras-go` is a Go library for managing OCI artifacts, compliant with the [OCI Image Format Specification](https://github.com/opencontainers/image-spec) and the [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec). It provides unified APIs for pushing, pulling, and managing artifacts across OCI-compliant registries, local file systems, and in-memory stores. -### Versioning +> [!Note] +> The `main` branch follows [Go's Security Policy](https://github.com/golang/go/security/policy) and supports the two latest versions of Go (currently `1.24` and `1.25`). -The ORAS Go library follows [Semantic Versioning](https://semver.org/), where breaking changes are reserved for MAJOR releases, and MINOR and PATCH releases must be 100% backwards compatible. +## Getting Started -### v2: stable +### Concepts -[](https://github.com/oras-project/oras-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Amain) -[](https://codecov.io/gh/oras-project/oras-go) -[](https://goreportcard.com/report/oras.land/oras-go/v2) -[](https://pkg.go.dev/oras.land/oras-go/v2) +Gain insights into the fundamental concepts: -The version `2` is actively developed in the [`main`](https://github.com/oras-project/oras-go/tree/main) branch with all new features. +- [Modeling Artifacts](docs/Modeling-Artifacts.md) +- [Targets and Content Stores](docs/Targets.md) -> [!Note] -> The `main` branch follows [Go's Security Policy](https://github.com/golang/go/security/policy) and supports the two latest versions of Go (currently `1.21` and `1.22`). +### Quickstart + +Follow the step-by-step tutorial to use `oras-go` v2: + +- [Quickstart: Managing OCI Artifacts with `oras-go` v2](docs/tutorial/quickstart.md) + +### Examples + +Check out sample code for common use cases: -Examples for common use cases can be found below: +- [Artifact copying](https://pkg.go.dev/oras.land/oras-go/v2#pkg-examples) +- [Registry operations](https://pkg.go.dev/oras.land/oras-go/v2/registry#pkg-examples) +- [Repository operations](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote#pkg-examples) +- [Authentication](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/auth#pkg-examples) +- [Credentials management](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/credentials#pkg-examples) -- [Copy examples](https://pkg.go.dev/oras.land/oras-go/v2#pkg-examples) -- [Registry interaction examples](https://pkg.go.dev/oras.land/oras-go/v2/registry#pkg-examples) -- [Repository interaction examples](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote#pkg-examples) -- [Authentication examples](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote/auth#pkg-examples) +Find more API examples at [pkg.go.dev](https://pkg.go.dev/oras.land/oras-go/v2). -If you are seeking latest changes, you should use the [`main`](https://github.com/oras-project/oras-go/tree/main) branch (or a specific commit hash) over a tagged version when including the ORAS Go library in your project's `go.mod`. -The Go Reference for the `main` branch is available [here](https://pkg.go.dev/oras.land/oras-go/v2@main). -To migrate from `v1` to `v2`, see [MIGRATION_GUIDE.md](./MIGRATION_GUIDE.md). +## Versioning -### v1: stable +This project follows [Semantic Versioning](https://semver.org/) (`MAJOR`.`MINOR`.`PATCH`), with `MAJOR` for breaking changes, `MINOR` for backward-compatible features, and `PATCH` for backward-compatible fixes. + +## Previous Major Versions + +### v1 (maintenance) [](https://github.com/oras-project/oras-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Av1) [](https://goreportcard.com/report/oras.land/oras-go) [](https://pkg.go.dev/oras.land/oras-go) -As there are various stable projects depending on the ORAS Go library `v1`, the -[`v1`](https://github.com/oras-project/oras-go/tree/v1) branch -is maintained for API stability, dependency updates, and security patches. -All `v1.*` releases are based upon this branch. - -Since `v1` is in a maintenance state, you are highly encouraged -to use releases with major version `2` for new features. - -## Docs +The [`v1`](https://github.com/oras-project/oras-go/tree/v1) branch is maintained for dependency updates and security fixes only. All feature development happens in the [`main`](https://github.com/oras-project/oras-go/tree/main) branch. -- [oras.land/client_libraries/go](https://oras.land/docs/Client_Libraries/go): Documentation for the ORAS Go library -- [Reviewing guide](https://github.com/oras-project/community/blob/main/REVIEWING.md): All reviewers must read the reviewing guide and agree to follow the project review guidelines. +To migrate from `v1` to `v2`, see [MIGRATION_GUIDE.md](MIGRATION_GUIDE.md). -## Code of Conduct +## Community -This project has adopted the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for further details. +- Code of Conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) +- Security Policy: [SECURITY.md](SECURITY.md) +- Reviewing Guide: [Reviewing Guide](https://github.com/oras-project/community/blob/main/REVIEWING.md) +- Slack: [`#oras`](https://cloud-native.slack.com/archives/CJ1KHJM5Z) channel on CNCF Slack diff --git a/vendor/oras.land/oras-go/v2/RELEASES.md b/vendor/oras.land/oras-go/v2/RELEASES.md new file mode 100644 index 00000000..c802c22d --- /dev/null +++ b/vendor/oras.land/oras-go/v2/RELEASES.md @@ -0,0 +1,108 @@ +# Releasing oras-go + +Releases are created via a GitOps workflow. Merging a `release/vX.Y.Z` branch +into `v2` automatically tags the commit and publishes the GitHub Release. + +## Steps + +### 1. Create a release branch + +The release branch needs at least one commit so GitHub will allow a PR to be +opened. Use an empty commit as a lightweight marker: + +```bash +git fetch upstream +git checkout -b release/v2.7.0 upstream/v2 +git commit --allow-empty -s -m "chore: prepare release v2.7.0" +git push origin release/v2.7.0 +``` + +The release does not need to contain the changes being released — those are +already on `v2`. The PR is a trigger: when it merges, the workflow tags the +PR's `merge_commit_sha` (the exact commit that landed on `v2`), which includes +all prior work on the branch. + +### 2. Open a pull request + +Open a PR from `release/v2.7.0` targeting the `v2` branch. Write the release +notes directly in the PR description using the format from prior releases: + +```markdown +## New Features +... + +## Bug Fixes +... + +## Documentation +... + +## Other Changes +... +``` + +The PR description becomes the GitHub Release body verbatim, so write it in +its final form. + +### 3. Get approvals + +Branch protection on `v2` requires approval from at least 3 of the 4 owners +listed in [OWNERS.md](OWNERS.md). Reviewers should verify: + +- The target commit is correct +- The release notes are accurate and complete +- All CI checks pass + +### 4. Merge + +Merge the PR. The [release workflow](.github/workflows/release.yml) +automatically: + +1. Extracts the version from the branch name (`release/v2.7.0` → `v2.7.0`) +2. Creates and pushes the git tag +3. Publishes the GitHub Release with the PR body as release notes + +## Pre-releases + +Tags containing `-alpha`, `-beta`, or `-rc` (e.g., `v2.7.0-rc.1`) are +automatically marked as pre-release on GitHub. Use the same branch naming +convention: `release/v2.7.0-rc.1`. + +## Testing the workflow locally + +Three levels of local validation are available without triggering a real release: + +**1. Validate the goreleaser config:** +```bash +goreleaser check +``` + +**2. Validate workflow structure and job matching (dry run):** +```bash +act pull_request \ + -e .github/act/release-event.json \ + -W .github/workflows/release.yml \ + -n +``` + +**3. Run the workflow end-to-end with a fake token (Colima + cached actions required):** +```bash +act pull_request \ + -e .github/act/release-event.json \ + -W .github/workflows/release.yml \ + -s GITHUB_TOKEN=fake \ + --pull=false \ + --action-offline-mode \ + --container-daemon-socket - +``` + +This runs all steps up to and including version extraction (`version=vX.Y.Z` will +appear in the output). The `git push` step then fails with a permission error — +that is expected and confirms no tag was pushed. The mock event payload is at +`.github/act/release-event.json`. + +## Updating the documentation site + +After a release, update [oras-www](https://github.com/oras-project/oras-www) +to reflect the new version. See the `CLAUDE.md` in that repository for the +exact steps. diff --git a/vendor/oras.land/oras-go/v2/content/file/file.go b/vendor/oras.land/oras-go/v2/content/file/file.go index 3f1e8c08..6cdba2e4 100644 --- a/vendor/oras.land/oras-go/v2/content/file/file.go +++ b/vendor/oras.land/oras-go/v2/content/file/file.go @@ -39,7 +39,7 @@ import ( // bufPool is a pool of byte buffers that can be reused for copying content // between files. var bufPool = sync.Pool{ - New: func() interface{} { + New: func() any { // the buffer size should be larger than or equal to 128 KiB // for performance considerations. // we choose 1 MiB here so there will be less disk I/O. @@ -108,6 +108,9 @@ type Store struct { // value overrides the [AnnotationUnpack]. // Default value: false. SkipUnpack bool + // PreservePermissions controls whether to preserve file permissions when unpacking, + // disregarding the active umask, similar to tar's `--preserve-permissions` + PreservePermissions bool workingDir string // the working directory of the file store closed int32 // if the store is closed - 0: false, 1: true. @@ -171,7 +174,7 @@ func (s *Store) Close() error { s.setClosed() var errs []string - s.tmpFiles.Range(func(name, _ interface{}) bool { + s.tmpFiles.Range(func(name, _ any) bool { if err := os.Remove(name.(string)); err != nil { errs = append(errs, err.Error()) } @@ -394,8 +397,9 @@ func (s *Store) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]oc return s.graph.Predecessors(ctx, node) } -// Add adds a file into the file store. -func (s *Store) Add(_ context.Context, name, mediaType, path string) (ocispec.Descriptor, error) { +// Add adds a file or a directory into the file store. +// Hard links within the directory are treated as regular files. +func (s *Store) Add(ctx context.Context, name, mediaType, path string) (ocispec.Descriptor, error) { if s.isClosedSet() { return ocispec.Descriptor{}, ErrStoreClosed } @@ -426,7 +430,7 @@ func (s *Store) Add(_ context.Context, name, mediaType, path string) (ocispec.De // generate descriptor var desc ocispec.Descriptor if fi.IsDir() { - desc, err = s.descriptorFromDir(name, mediaType, path) + desc, err = s.descriptorFromDir(ctx, name, mediaType, path) } else { desc, err = s.descriptorFromFile(fi, mediaType, path) } @@ -498,14 +502,14 @@ func (s *Store) pushDir(name, target string, expected ocispec.Descriptor, conten checksum := expected.Annotations[AnnotationDigest] buf := bufPool.Get().(*[]byte) defer bufPool.Put(buf) - if err := extractTarGzip(target, name, gzPath, checksum, *buf); err != nil { + if err := extractTarGzip(target, name, gzPath, checksum, *buf, s.PreservePermissions); err != nil { return fmt.Errorf("failed to extract tar to %s: %w", target, err) } return nil } // descriptorFromDir generates descriptor from the given directory. -func (s *Store) descriptorFromDir(name, mediaType, dir string) (desc ocispec.Descriptor, err error) { +func (s *Store) descriptorFromDir(ctx context.Context, name, mediaType, dir string) (desc ocispec.Descriptor, err error) { // make a temp file to store the gzip gz, err := s.tempFile() if err != nil { @@ -532,7 +536,7 @@ func (s *Store) descriptorFromDir(name, mediaType, dir string) (desc ocispec.Des tw := io.MultiWriter(gzw, tarDigester.Hash()) buf := bufPool.Get().(*[]byte) defer bufPool.Put(buf) - if err := tarDirectory(dir, name, tw, s.TarReproducible, *buf); err != nil { + if err := tarDirectory(ctx, dir, name, tw, s.TarReproducible, *buf); err != nil { return ocispec.Descriptor{}, fmt.Errorf("failed to tar %s: %w", dir, err) } @@ -621,6 +625,13 @@ func (s *Store) resolveWritePath(name string) (string, error) { if strings.HasPrefix(rel, "../") || rel == ".." { return "", ErrPathTraversalDisallowed } + // The lexical check above prevents "../" escapes but does not resolve + // symlinks. A symlink component under workingDir (e.g. "out" -> "/outside") + // passes the lexical check yet directs writes outside workingDir. + // Re-check after resolving symlinks in the parent path to close that gap. + if err := checkSymlinkEscape(base, target); err != nil { + return "", err + } } if s.DisableOverwrite { if _, err := os.Stat(path); err == nil { @@ -682,3 +693,52 @@ func (s *Store) setClosed() { func ensureDir(path string) error { return os.MkdirAll(path, 0777) } + +// checkSymlinkEscape returns ErrPathTraversalDisallowed if resolving symlinks +// in target's ancestor directories causes it to escape base. target may not +// yet exist, so symlinks are resolved on its deepest existing ancestor. +func checkSymlinkEscape(base, target string) error { + realBase, err := filepath.EvalSymlinks(base) + if err != nil { + if os.IsNotExist(err) { + return nil // base doesn't exist yet; no symlinks to follow + } + return err + } + realTarget, err := realPathForWrite(target) + if err != nil { + return err + } + rel, err := filepath.Rel(realBase, realTarget) + if err != nil { + return ErrPathTraversalDisallowed + } + rel = filepath.ToSlash(rel) + if strings.HasPrefix(rel, "../") || rel == ".." { + return ErrPathTraversalDisallowed + } + return nil +} + +// realPathForWrite resolves symlinks in the deepest existing ancestor of path +// and returns the resulting absolute path. Non-existent path components are +// appended verbatim, matching the semantics of a file about to be created. +func realPathForWrite(path string) (string, error) { + dir := filepath.Dir(path) + suffix := filepath.Base(path) + for { + real, err := filepath.EvalSymlinks(dir) + if err == nil { + return filepath.Join(real, suffix), nil + } + if !os.IsNotExist(err) { + return "", err + } + parent := filepath.Dir(dir) + if parent == dir { + return path, nil // reached filesystem root + } + suffix = filepath.Join(filepath.Base(dir), suffix) + dir = parent + } +} diff --git a/vendor/oras.land/oras-go/v2/content/file/utils.go b/vendor/oras.land/oras-go/v2/content/file/utils.go index c42013d8..bd1f4af1 100644 --- a/vendor/oras.land/oras-go/v2/content/file/utils.go +++ b/vendor/oras.land/oras-go/v2/content/file/utils.go @@ -18,9 +18,11 @@ package file import ( "archive/tar" "compress/gzip" + "context" "errors" "fmt" "io" + "io/fs" "os" "path/filepath" "strings" @@ -31,7 +33,7 @@ import ( // tarDirectory walks the directory specified by path, and tar those files with a new // path prefix. -func tarDirectory(root, prefix string, w io.Writer, removeTimes bool, buf []byte) (err error) { +func tarDirectory(ctx context.Context, root, prefix string, w io.Writer, removeTimes bool, buf []byte) (err error) { tw := tar.NewWriter(w) defer func() { closeErr := tw.Close() @@ -45,6 +47,12 @@ func tarDirectory(root, prefix string, w io.Writer, removeTimes bool, buf []byte return err } + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + // Rename path name, err := filepath.Rel(root, path) if err != nil { @@ -54,6 +62,7 @@ func tarDirectory(root, prefix string, w io.Writer, removeTimes bool, buf []byte name = filepath.ToSlash(name) // Generate header + // NOTE: We don't support hard links and treat it as regular files var link string mode := info.Mode() if mode&os.ModeSymlink != 0 { @@ -104,8 +113,8 @@ func tarDirectory(root, prefix string, w io.Writer, removeTimes bool, buf []byte // extractTarGzip decompresses the gzip // and extracts tar file to a directory specified by the `dir` parameter. -func extractTarGzip(dir, prefix, filename, checksum string, buf []byte) (err error) { - fp, err := os.Open(filename) +func extractTarGzip(dirPath, dirName, gzPath, checksum string, buf []byte, preservePermissions bool) (err error) { + fp, err := os.Open(gzPath) if err != nil { return err } @@ -135,7 +144,7 @@ func extractTarGzip(dir, prefix, filename, checksum string, buf []byte) (err err r = io.TeeReader(r, verifier) } } - if err := extractTarDirectory(dir, prefix, r, buf); err != nil { + if err := extractTarDirectory(dirPath, dirName, r, buf, preservePermissions); err != nil { return err } if verifier != nil && !verifier.Verified() { @@ -147,7 +156,7 @@ func extractTarGzip(dir, prefix, filename, checksum string, buf []byte) (err err // extractTarDirectory extracts tar file to a directory specified by the `dir` // parameter. The file name prefix is ensured to be the string specified by the // `prefix` parameter and is trimmed. -func extractTarDirectory(dir, prefix string, r io.Reader, buf []byte) error { +func extractTarDirectory(dirPath, dirName string, r io.Reader, buf []byte, preservePermissions bool) error { tr := tar.NewReader(r) for { header, err := tr.Next() @@ -159,28 +168,43 @@ func extractTarDirectory(dir, prefix string, r io.Reader, buf []byte) error { } // Name check - name := header.Name - path, err := ensureBasePath(dir, prefix, name) + filename := header.Name + filePathRel, err := resolveRelToBase(dirPath, dirName, filename) if err != nil { return err } - path = filepath.Join(dir, path) + filePath := filepath.Join(dirPath, filePathRel) // Create content switch header.Typeflag { case tar.TypeReg: - err = writeFile(path, tr, header.FileInfo().Mode(), buf) + err = writeFile(filePath, tr, header.FileInfo().Mode(), buf) case tar.TypeDir: - err = os.MkdirAll(path, header.FileInfo().Mode()) + err = os.MkdirAll(filePath, header.FileInfo().Mode()) case tar.TypeLink: + // NOTE: ORAS does not generate hard links when creating tarballs. + // If a hard link is found in the tarball, it will be extracted. + // If the target link already exists, os.Link will throw an error. + // This is a known limitation and will not be addressed. var target string - if target, err = ensureLinkPath(dir, prefix, path, header.Linkname); err == nil { - err = os.Link(target, path) + if target, err = ensureLinkPath(dirPath, dirName, filePath, header.Linkname); err == nil { + err = os.Link(target, filePath) } case tar.TypeSymlink: var target string - if target, err = ensureLinkPath(dir, prefix, path, header.Linkname); err == nil { - err = os.Symlink(target, path) + target, err = ensureLinkPath(dirPath, dirName, filePath, header.Linkname) + if err != nil { + return err + } + if err = os.Symlink(target, filePath); err != nil { + if !errors.Is(err, fs.ErrExist) { + return err + } + // link already exists, remove the old one and try again + if err := os.Remove(filePath); err != nil { + return err + } + err = os.Symlink(target, filePath) } default: continue // Non-regular files are skipped @@ -190,14 +214,21 @@ func extractTarDirectory(dir, prefix string, r io.Reader, buf []byte) error { } // Change access time and modification time if possible (error ignored) - os.Chtimes(path, header.AccessTime, header.ModTime) + _ = os.Chtimes(filePath, header.AccessTime, header.ModTime) + + // Restore full mode bits + if preservePermissions && (header.Typeflag == tar.TypeReg || header.Typeflag == tar.TypeDir) { + if err := os.Chmod(filePath, os.FileMode(header.Mode)); err != nil { + return err + } + } } } -// ensureBasePath ensures the target path is in the base path, +// resolveRelToBase ensures the target path is in the base path, // returning its relative path to the base path. // target can be either an absolute path or a relative path. -func ensureBasePath(baseAbs, baseRel, target string) (string, error) { +func resolveRelToBase(baseAbs, baseRel, target string) (string, error) { base := baseRel if filepath.IsAbs(target) { // ensure base and target are consistent @@ -237,7 +268,7 @@ func ensureLinkPath(baseAbs, baseRel, link, target string) (string, error) { path = filepath.Join(filepath.Dir(link), target) } // ensure path is under baseAbs or baseRel - if _, err := ensureBasePath(baseAbs, baseRel, path); err != nil { + if _, err := resolveRelToBase(baseAbs, baseRel, path); err != nil { return "", err } return target, nil diff --git a/vendor/oras.land/oras-go/v2/content/reader.go b/vendor/oras.land/oras-go/v2/content/reader.go index e575378e..def9ebde 100644 --- a/vendor/oras.land/oras-go/v2/content/reader.go +++ b/vendor/oras.land/oras-go/v2/content/reader.go @@ -24,6 +24,12 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) +// maxDescriptorSize is the upper-bound for descriptor sizes accepted by +// ReadAll. Descriptors sourced from attacker-supplied OCI layouts can carry +// arbitrarily large Size values; without this cap, make([]byte, desc.Size) +// triggers a runtime panic before any allocation occurs. +const maxDescriptorSize = 32 * 1024 * 1024 // 32 MiB + var ( // ErrInvalidDescriptorSize is returned by ReadAll() when // the descriptor has an invalid size. @@ -99,6 +105,11 @@ func (vr *VerifyReader) Verify() error { // NewVerifyReader wraps r for reading content with verification against desc. func NewVerifyReader(r io.Reader, desc ocispec.Descriptor) *VerifyReader { + if err := desc.Digest.Validate(); err != nil { + return &VerifyReader{ + err: fmt.Errorf("failed to validate %s: %w", desc.Digest, err), + } + } verifier := desc.Digest.Verifier() lr := &io.LimitedReader{ R: io.TeeReader(r, verifier), @@ -114,7 +125,7 @@ func NewVerifyReader(r io.Reader, desc ocispec.Descriptor) *VerifyReader { // The read content is verified against the size and the digest // using a VerifyReader. func ReadAll(r io.Reader, desc ocispec.Descriptor) ([]byte, error) { - if desc.Size < 0 { + if desc.Size < 0 || desc.Size > maxDescriptorSize { return nil, ErrInvalidDescriptorSize } buf := make([]byte, desc.Size) diff --git a/vendor/oras.land/oras-go/v2/copy.go b/vendor/oras.land/oras-go/v2/copy.go index 2f131a8c..2c614694 100644 --- a/vendor/oras.land/oras-go/v2/copy.go +++ b/vendor/oras.land/oras-go/v2/copy.go @@ -120,18 +120,21 @@ type CopyGraphOptions struct { FindSuccessors func(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) } -// Copy copies a rooted directed acyclic graph (DAG) with the tagged root node -// in the source Target to the destination Target. +// Copy copies a rooted directed acyclic graph (DAG), such as an artifact, +// from the source Target to the destination Target. +// +// The root node (e.g. a tagged manifest of the artifact) is identified by the +// source reference. // The destination reference will be the same as the source reference if the // destination reference is left blank. // // Returns the descriptor of the root node on successful copy. func Copy(ctx context.Context, src ReadOnlyTarget, srcRef string, dst Target, dstRef string, opts CopyOptions) (ocispec.Descriptor, error) { if src == nil { - return ocispec.Descriptor{}, errors.New("nil source target") + return ocispec.Descriptor{}, newCopyError("Copy", CopyErrorOriginSource, errors.New("nil source target")) } if dst == nil { - return ocispec.Descriptor{}, errors.New("nil destination target") + return ocispec.Descriptor{}, newCopyError("Copy", CopyErrorOriginDestination, errors.New("nil destination target")) } if dstRef == "" { dstRef = srcRef @@ -144,14 +147,14 @@ func Copy(ctx context.Context, src ReadOnlyTarget, srcRef string, dst Target, ds proxy := cas.NewProxyWithLimit(src, cas.NewMemory(), opts.MaxMetadataBytes) root, err := resolveRoot(ctx, src, srcRef, proxy) if err != nil { - return ocispec.Descriptor{}, fmt.Errorf("failed to resolve %s: %w", srcRef, err) + return ocispec.Descriptor{}, err } if opts.MapRoot != nil { proxy.StopCaching = true root, err = opts.MapRoot(ctx, proxy, root) if err != nil { - return ocispec.Descriptor{}, err + return ocispec.Descriptor{}, newCopyError("MapRoot", CopyErrorOriginSource, err) } proxy.StopCaching = false } @@ -167,9 +170,16 @@ func Copy(ctx context.Context, src ReadOnlyTarget, srcRef string, dst Target, ds return root, nil } -// CopyGraph copies a rooted directed acyclic graph (DAG) from the source CAS to -// the destination CAS. +// CopyGraph copies a rooted directed acyclic graph (DAG), such as an artifact, +// from the source CAS to the destination CAS. +// The root node (e.g. a manifest of the artifact) is identified by a descriptor. func CopyGraph(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, root ocispec.Descriptor, opts CopyGraphOptions) error { + if src == nil { + return newCopyError("CopyGraph", CopyErrorOriginSource, errors.New("nil source target")) + } + if dst == nil { + return newCopyError("CopyGraph", CopyErrorOriginDestination, errors.New("nil destination target")) + } return copyGraph(ctx, src, dst, root, nil, nil, nil, opts) } @@ -218,7 +228,7 @@ func copyGraph(ctx context.Context, src content.ReadOnlyStorage, dst content.Sto // skip if a rooted sub-DAG exists exists, err := dst.Exists(ctx, desc) if err != nil { - return err + return newCopyError("Exists", CopyErrorOriginDestination, err) } if exists { if opts.OnCopySkipped != nil { @@ -232,7 +242,7 @@ func copyGraph(ctx context.Context, src content.ReadOnlyStorage, dst content.Sto // find successors while non-leaf nodes will be fetched and cached successors, err := opts.FindSuccessors(ctx, proxy, desc) if err != nil { - return err + return newCopyError("FindSuccessors", CopyErrorOriginSource, err) } successors = removeForeignLayers(successors) @@ -260,7 +270,7 @@ func copyGraph(ctx context.Context, src content.ReadOnlyStorage, dst content.Sto exists, err = proxy.Cache.Exists(ctx, desc) if err != nil { - return err + return fmt.Errorf("failed to check cache existence: %s: %w", desc.Digest, err) } if exists { return copyNode(ctx, proxy.Cache, dst, desc, opts) @@ -320,7 +330,7 @@ func mountOrCopyNode(ctx context.Context, src content.ReadOnlyStorage, dst conte // Mount or copy if err := mounter.Mount(ctx, desc, sourceRepository, getContent); err != nil && !errors.Is(err, skipSource) { - return err + return newCopyError("Mount", CopyErrorOriginDestination, err) } if !mountFailed { @@ -348,12 +358,12 @@ func mountOrCopyNode(ctx context.Context, src content.ReadOnlyStorage, dst conte func doCopyNode(ctx context.Context, src content.ReadOnlyStorage, dst content.Storage, desc ocispec.Descriptor) error { rc, err := src.Fetch(ctx, desc) if err != nil { - return err + return newCopyError("Fetch", CopyErrorOriginSource, err) } defer rc.Close() err = dst.Push(ctx, desc, rc) if err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { - return err + return newCopyError("Push", CopyErrorOriginDestination, err) } return nil } @@ -385,13 +395,13 @@ func copyNode(ctx context.Context, src content.ReadOnlyStorage, dst content.Stor func copyCachedNodeWithReference(ctx context.Context, src *cas.Proxy, dst registry.ReferencePusher, desc ocispec.Descriptor, dstRef string) error { rc, err := src.FetchCached(ctx, desc) if err != nil { - return err + return newCopyError("Fetch", CopyErrorOriginSource, err) } defer rc.Close() err = dst.PushReference(ctx, desc, rc, dstRef) if err != nil && !errors.Is(err, errdef.ErrAlreadyExists) { - return err + return newCopyError("PushReference", CopyErrorOriginDestination, err) } return nil } @@ -400,7 +410,11 @@ func copyCachedNodeWithReference(ctx context.Context, src *cas.Proxy, dst regist func resolveRoot(ctx context.Context, src ReadOnlyTarget, srcRef string, proxy *cas.Proxy) (ocispec.Descriptor, error) { refFetcher, ok := src.(registry.ReferenceFetcher) if !ok { - return src.Resolve(ctx, srcRef) + desc, err := src.Resolve(ctx, srcRef) + if err != nil { + return ocispec.Descriptor{}, newCopyError("Resolve", CopyErrorOriginSource, err) + } + return desc, nil } // optimize performance for ReferenceFetcher targets @@ -410,7 +424,7 @@ func resolveRoot(ctx context.Context, src ReadOnlyTarget, srcRef string, proxy * } root, rc, err := refProxy.FetchReference(ctx, srcRef) if err != nil { - return ocispec.Descriptor{}, err + return ocispec.Descriptor{}, newCopyError("FetchReference", CopyErrorOriginSource, err) } defer rc.Close() // cache root if it is a non-leaf node @@ -421,7 +435,7 @@ func resolveRoot(ctx context.Context, src ReadOnlyTarget, srcRef string, proxy * return nil, errors.New("fetching only root node expected") }) if _, err = content.Successors(ctx, fetcher, root); err != nil { - return ocispec.Descriptor{}, err + return ocispec.Descriptor{}, newCopyError("Successors", CopyErrorOriginSource, err) } // TODO: optimize special case where root is a leaf node (i.e. a blob) @@ -430,7 +444,7 @@ func resolveRoot(ctx context.Context, src ReadOnlyTarget, srcRef string, proxy * } // prepareCopy prepares the hooks for copy. -func prepareCopy(ctx context.Context, dst Target, dstRef string, proxy *cas.Proxy, root ocispec.Descriptor, opts *CopyOptions) error { +func prepareCopy(_ context.Context, dst Target, dstRef string, proxy *cas.Proxy, root ocispec.Descriptor, opts *CopyOptions) error { if refPusher, ok := dst.(registry.ReferencePusher); ok { // optimize performance for ReferencePusher targets preCopy := opts.PreCopy @@ -463,7 +477,7 @@ func prepareCopy(ctx context.Context, dst Target, dstRef string, proxy *cas.Prox if content.Equal(desc, root) { // for root node, tag it after copying it if err := dst.Tag(ctx, root, dstRef); err != nil { - return err + return newCopyError("Tag", CopyErrorOriginDestination, err) } } if postCopy != nil { @@ -495,7 +509,10 @@ func prepareCopy(ctx context.Context, dst Target, dstRef string, proxy *cas.Prox return err } } - return dst.Tag(ctx, root, dstRef) + if err := dst.Tag(ctx, root, dstRef); err != nil { + return newCopyError("Tag", CopyErrorOriginDestination, err) + } + return nil } return nil diff --git a/vendor/oras.land/oras-go/v2/copyerror.go b/vendor/oras.land/oras-go/v2/copyerror.go new file mode 100644 index 00000000..03ed8e24 --- /dev/null +++ b/vendor/oras.land/oras-go/v2/copyerror.go @@ -0,0 +1,78 @@ +/* +Copyright The ORAS Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package oras + +import "fmt" + +// CopyErrorOrigin defines the source of a copy error. +type CopyErrorOrigin int + +const ( + // CopyErrorOriginSource indicates the error occurred at the source side. + CopyErrorOriginSource CopyErrorOrigin = 1 + + // CopyErrorOriginDestination indicates the error occurred at the destination side. + CopyErrorOriginDestination CopyErrorOrigin = 2 +) + +// String returns the string representation of the CopyErrorOrigin. +func (o CopyErrorOrigin) String() string { + switch o { + case CopyErrorOriginSource: + return "source" + case CopyErrorOriginDestination: + return "destination" + default: + return "unknown" + } +} + +// CopyError represents an error encountered during a copy operation. +type CopyError struct { + // Op is the operation that caused the error. + Op string + // Origin indicates the source of the error. + Origin CopyErrorOrigin + // Err is the underlying error. + Err error +} + +// newCopyError creates a new CopyError. +func newCopyError(op string, origin CopyErrorOrigin, err error) error { + if err == nil { + return nil + } + return &CopyError{ + Op: op, + Origin: origin, + Err: err, + } +} + +// Error implements the error interface for CopyError. +func (e *CopyError) Error() string { + switch e.Origin { + case CopyErrorOriginSource, CopyErrorOriginDestination: + return fmt.Sprintf("failed to perform %q on %s: %v", e.Op, e.Origin, e.Err) + default: + return fmt.Sprintf("failed to perform %q: %v", e.Op, e.Err) + } +} + +// Unwrap implements the errors.Unwrap interface for CopyError. +func (e *CopyError) Unwrap() error { + return e.Err +} diff --git a/vendor/oras.land/oras-go/v2/extendedcopy.go b/vendor/oras.land/oras-go/v2/extendedcopy.go index 49b6264e..40f57832 100644 --- a/vendor/oras.land/oras-go/v2/extendedcopy.go +++ b/vendor/oras.land/oras-go/v2/extendedcopy.go @@ -65,16 +65,21 @@ type ExtendedCopyGraphOptions struct { // ExtendedCopy copies the directed acyclic graph (DAG) that are reachable from // the given tagged node from the source GraphTarget to the destination Target. +// In other words, it copies a tagged artifact along with its referrers or +// other predecessor manifests referencing it. +// +// The tagged node (e.g. a tagged manifest of the artifact) is identified by the +// source reference. // The destination reference will be the same as the source reference if the // destination reference is left blank. // // Returns the descriptor of the tagged node on successful copy. func ExtendedCopy(ctx context.Context, src ReadOnlyGraphTarget, srcRef string, dst Target, dstRef string, opts ExtendedCopyOptions) (ocispec.Descriptor, error) { if src == nil { - return ocispec.Descriptor{}, errors.New("nil source graph target") + return ocispec.Descriptor{}, newCopyError("ExtendedCopy", CopyErrorOriginSource, errors.New("nil source target")) } if dst == nil { - return ocispec.Descriptor{}, errors.New("nil destination target") + return ocispec.Descriptor{}, newCopyError("ExtendedCopy", CopyErrorOriginDestination, errors.New("nil destination target")) } if dstRef == "" { dstRef = srcRef @@ -82,7 +87,7 @@ func ExtendedCopy(ctx context.Context, src ReadOnlyGraphTarget, srcRef string, d node, err := src.Resolve(ctx, srcRef) if err != nil { - return ocispec.Descriptor{}, err + return ocispec.Descriptor{}, newCopyError("Resolve", CopyErrorOriginSource, err) } if err := ExtendedCopyGraph(ctx, src, dst, node, opts.ExtendedCopyGraphOptions); err != nil { @@ -90,7 +95,7 @@ func ExtendedCopy(ctx context.Context, src ReadOnlyGraphTarget, srcRef string, d } if err := dst.Tag(ctx, node, dstRef); err != nil { - return ocispec.Descriptor{}, err + return ocispec.Descriptor{}, newCopyError("Tag", CopyErrorOriginDestination, err) } return node, nil @@ -98,7 +103,17 @@ func ExtendedCopy(ctx context.Context, src ReadOnlyGraphTarget, srcRef string, d // ExtendedCopyGraph copies the directed acyclic graph (DAG) that are reachable // from the given node from the source GraphStorage to the destination Storage. +// In other words, it copies an artifact along with its referrers or other +// predecessor manifests referencing it. +// The node (e.g. a manifest of the artifact) is identified by a descriptor. func ExtendedCopyGraph(ctx context.Context, src content.ReadOnlyGraphStorage, dst content.Storage, node ocispec.Descriptor, opts ExtendedCopyGraphOptions) error { + if src == nil { + return newCopyError("ExtendedCopyGraph", CopyErrorOriginSource, errors.New("nil source target")) + } + if dst == nil { + return newCopyError("ExtendedCopyGraph", CopyErrorOriginDestination, errors.New("nil destination target")) + } + roots, err := findRoots(ctx, src, node, opts) if err != nil { return err @@ -174,7 +189,7 @@ func findRoots(ctx context.Context, storage content.ReadOnlyGraphStorage, node o predecessors, err := opts.FindPredecessors(ctx, storage, currentNode) if err != nil { - return nil, err + return nil, newCopyError("FindPredecessors", CopyErrorOriginSource, err) } // The current node has no predecessor node, diff --git a/vendor/oras.land/oras-go/v2/internal/cas/memory.go b/vendor/oras.land/oras-go/v2/internal/cas/memory.go index 7e358e13..2d97e2a6 100644 --- a/vendor/oras.land/oras-go/v2/internal/cas/memory.go +++ b/vendor/oras.land/oras-go/v2/internal/cas/memory.go @@ -80,7 +80,7 @@ func (m *Memory) Exists(_ context.Context, target ocispec.Descriptor) (bool, err // necessarily correspond to any consistent snapshot of the storage contents. func (m *Memory) Map() map[descriptor.Descriptor][]byte { res := make(map[descriptor.Descriptor][]byte) - m.content.Range(func(key, value interface{}) bool { + m.content.Range(func(key, value any) bool { res[key.(descriptor.Descriptor)] = value.([]byte) return true }) diff --git a/vendor/oras.land/oras-go/v2/internal/graph/memory.go b/vendor/oras.land/oras-go/v2/internal/graph/memory.go index 016e5f96..ffd06545 100644 --- a/vendor/oras.land/oras-go/v2/internal/graph/memory.go +++ b/vendor/oras.land/oras-go/v2/internal/graph/memory.go @@ -25,7 +25,6 @@ import ( "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/internal/container/set" - "oras.land/oras-go/v2/internal/descriptor" "oras.land/oras-go/v2/internal/status" "oras.land/oras-go/v2/internal/syncutil" ) @@ -34,9 +33,9 @@ import ( type Memory struct { // nodes has the following properties and behaviors: // 1. a node exists in Memory.nodes if and only if it exists in the memory - // 2. Memory.nodes saves the ocispec.Descriptor map keys, which are used by + // 2. Memory.nodes saves the ocispec.Descriptor indexed by digest, which are used by // the other fields. - nodes map[descriptor.Descriptor]ocispec.Descriptor + nodes map[digest.Digest]ocispec.Descriptor // predecessors has the following properties and behaviors: // 1. a node exists in Memory.predecessors if it has at least one predecessor @@ -44,14 +43,14 @@ type Memory struct { // the memory. // 2. a node does not exist in Memory.predecessors, if it doesn't have any predecessors // in the memory. - predecessors map[descriptor.Descriptor]set.Set[descriptor.Descriptor] + predecessors map[digest.Digest]set.Set[digest.Digest] // successors has the following properties and behaviors: // 1. a node exists in Memory.successors if and only if it exists in the memory. // 2. a node's entry in Memory.successors is always consistent with the actual // content of the node, regardless of whether or not each successor exists // in the memory. - successors map[descriptor.Descriptor]set.Set[descriptor.Descriptor] + successors map[digest.Digest]set.Set[digest.Digest] lock sync.RWMutex } @@ -59,9 +58,9 @@ type Memory struct { // NewMemory creates a new memory PredecessorFinder. func NewMemory() *Memory { return &Memory{ - nodes: make(map[descriptor.Descriptor]ocispec.Descriptor), - predecessors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]), - successors: make(map[descriptor.Descriptor]set.Set[descriptor.Descriptor]), + nodes: make(map[digest.Digest]ocispec.Descriptor), + predecessors: make(map[digest.Digest]set.Set[digest.Digest]), + successors: make(map[digest.Digest]set.Set[digest.Digest]), } } @@ -108,14 +107,13 @@ func (m *Memory) Predecessors(_ context.Context, node ocispec.Descriptor) ([]oci m.lock.RLock() defer m.lock.RUnlock() - key := descriptor.FromOCI(node) - set, exists := m.predecessors[key] + set, exists := m.predecessors[node.Digest] if !exists { return nil, nil } var res []ocispec.Descriptor - for k := range set { - res = append(res, m.nodes[k]) + for digest := range set { + res = append(res, m.nodes[digest]) } return res, nil } @@ -126,25 +124,24 @@ func (m *Memory) Remove(node ocispec.Descriptor) []ocispec.Descriptor { m.lock.Lock() defer m.lock.Unlock() - nodeKey := descriptor.FromOCI(node) var danglings []ocispec.Descriptor // remove the node from its successors' predecessor list - for successorKey := range m.successors[nodeKey] { - predecessorEntry := m.predecessors[successorKey] - predecessorEntry.Delete(nodeKey) + for successorDigest := range m.successors[node.Digest] { + predecessorEntry := m.predecessors[successorDigest] + predecessorEntry.Delete(node.Digest) // if none of the predecessors of the node still exists, we remove the // predecessors entry and return it as a dangling node. Otherwise, we do // not remove the entry. if len(predecessorEntry) == 0 { - delete(m.predecessors, successorKey) - if _, exists := m.nodes[successorKey]; exists { - danglings = append(danglings, m.nodes[successorKey]) + delete(m.predecessors, successorDigest) + if _, exists := m.nodes[successorDigest]; exists { + danglings = append(danglings, m.nodes[successorDigest]) } } } - delete(m.successors, nodeKey) - delete(m.nodes, nodeKey) + delete(m.successors, node.Digest) + delete(m.nodes, node.Digest) return danglings } @@ -154,8 +151,8 @@ func (m *Memory) DigestSet() set.Set[digest.Digest] { defer m.lock.RUnlock() s := set.New[digest.Digest]() - for desc := range m.nodes { - s.Add(desc.Digest) + for digest := range m.nodes { + s.Add(digest) } return s } @@ -170,22 +167,20 @@ func (m *Memory) index(ctx context.Context, fetcher content.Fetcher, node ocispe defer m.lock.Unlock() // index the node - nodeKey := descriptor.FromOCI(node) - m.nodes[nodeKey] = node + m.nodes[node.Digest] = node // for each successor, put it into the node's successors list, and // put node into the succeesor's predecessors list - successorSet := set.New[descriptor.Descriptor]() - m.successors[nodeKey] = successorSet + successorSet := set.New[digest.Digest]() + m.successors[node.Digest] = successorSet for _, successor := range successors { - successorKey := descriptor.FromOCI(successor) - successorSet.Add(successorKey) - predecessorSet, exists := m.predecessors[successorKey] + successorSet.Add(successor.Digest) + predecessorSet, exists := m.predecessors[successor.Digest] if !exists { - predecessorSet = set.New[descriptor.Descriptor]() - m.predecessors[successorKey] = predecessorSet + predecessorSet = set.New[digest.Digest]() + m.predecessors[successor.Digest] = predecessorSet } - predecessorSet.Add(nodeKey) + predecessorSet.Add(node.Digest) } return successors, nil } @@ -195,7 +190,6 @@ func (m *Memory) Exists(node ocispec.Descriptor) bool { m.lock.RLock() defer m.lock.RUnlock() - nodeKey := descriptor.FromOCI(node) - _, exists := m.nodes[nodeKey] + _, exists := m.nodes[node.Digest] return exists } diff --git a/vendor/oras.land/oras-go/v2/internal/platform/platform.go b/vendor/oras.land/oras-go/v2/internal/platform/platform.go index 3aea3a1b..60d000d1 100644 --- a/vendor/oras.land/oras-go/v2/internal/platform/platform.go +++ b/vendor/oras.land/oras-go/v2/internal/platform/platform.go @@ -127,7 +127,7 @@ func SelectManifest(ctx context.Context, src content.ReadOnlyStorage, root ocisp // fields in config blob. func getPlatformFromConfig(ctx context.Context, src content.ReadOnlyStorage, desc ocispec.Descriptor, targetConfigMediaType string) (*ocispec.Platform, error) { if desc.MediaType != targetConfigMediaType { - return nil, fmt.Errorf("fail to recognize platform from unknown config %s: expect %s", desc.MediaType, targetConfigMediaType) + return nil, fmt.Errorf("fail to recognize platform from unknown config %s: expect %s: %w", desc.MediaType, targetConfigMediaType, errdef.ErrUnsupported) } rc, err := src.Fetch(ctx, desc) diff --git a/vendor/oras.land/oras-go/v2/internal/resolver/memory.go b/vendor/oras.land/oras-go/v2/internal/resolver/memory.go index 092a29e9..df710e66 100644 --- a/vendor/oras.land/oras-go/v2/internal/resolver/memory.go +++ b/vendor/oras.land/oras-go/v2/internal/resolver/memory.go @@ -17,6 +17,7 @@ package resolver import ( "context" + "fmt" "maps" "sync" @@ -48,7 +49,7 @@ func (m *Memory) Resolve(_ context.Context, reference string) (ocispec.Descripto desc, ok := m.index[reference] if !ok { - return ocispec.Descriptor{}, errdef.ErrNotFound + return ocispec.Descriptor{}, fmt.Errorf("%s: %w", reference, errdef.ErrNotFound) } return desc, nil } diff --git a/vendor/oras.land/oras-go/v2/internal/syncutil/limit.go b/vendor/oras.land/oras-go/v2/internal/syncutil/limit.go index 2a05d4ea..e429f24f 100644 --- a/vendor/oras.land/oras-go/v2/internal/syncutil/limit.go +++ b/vendor/oras.land/oras-go/v2/internal/syncutil/limit.go @@ -67,18 +67,41 @@ type GoFunc[T any] func(ctx context.Context, region *LimitedRegion, t T) error // Go concurrently invokes fn on items. func Go[T any](ctx context.Context, limiter *semaphore.Weighted, fn GoFunc[T], items ...T) error { + ctx, cancel := context.WithCancelCause(ctx) + defer cancel(nil) + eg, egCtx := errgroup.WithContext(ctx) for _, item := range items { - region := LimitRegion(ctx, limiter) + region := LimitRegion(egCtx, limiter) if err := region.Start(); err != nil { - return err + cancel(err) + // break loop instead of returning to allow previously scheduled + // goroutines to finish their deferred region.End() calls + break } - eg.Go(func(t T) func() error { + + eg.Go(func(t T, lr *LimitedRegion) func() error { return func() error { - defer region.End() - return fn(egCtx, region, t) + defer lr.End() + + select { + case <-egCtx.Done(): + // skip the task if the context is already cancelled + return nil + default: + } + + if err := fn(egCtx, lr, t); err != nil { + cancel(err) + return err + } + return nil } - }(item)) + }(item, region)) + } + + if err := eg.Wait(); err != nil { + cancel(err) } - return eg.Wait() + return context.Cause(ctx) } diff --git a/vendor/oras.land/oras-go/v2/internal/syncutil/limitgroup.go b/vendor/oras.land/oras-go/v2/internal/syncutil/limitgroup.go index 1071bedc..4ef087dc 100644 --- a/vendor/oras.land/oras-go/v2/internal/syncutil/limitgroup.go +++ b/vendor/oras.land/oras-go/v2/internal/syncutil/limitgroup.go @@ -21,7 +21,7 @@ import ( "golang.org/x/sync/errgroup" ) -// A LimitedGroup is a collection of goroutines working on subtasks that are part of +// LimitedGroup is a collection of goroutines working on subtasks that are part of // the same overall task. type LimitedGroup struct { grp *errgroup.Group diff --git a/vendor/oras.land/oras-go/v2/internal/syncutil/once.go b/vendor/oras.land/oras-go/v2/internal/syncutil/once.go index e4497053..d46ebd75 100644 --- a/vendor/oras.land/oras-go/v2/internal/syncutil/once.go +++ b/vendor/oras.land/oras-go/v2/internal/syncutil/once.go @@ -24,7 +24,7 @@ import ( // Once is an object that will perform exactly one action. // Unlike sync.Once, this Once allows the action to have return values. type Once struct { - result interface{} + result any err error status chan bool } @@ -46,7 +46,7 @@ func NewOnce() *Once { // Besides the return value of the function f, including the error, Do returns // true if the function f passed is called first and is not cancelled, deadline // exceeded, or panicking. Otherwise, returns false. -func (o *Once) Do(ctx context.Context, f func() (interface{}, error)) (bool, interface{}, error) { +func (o *Once) Do(ctx context.Context, f func() (any, error)) (bool, any, error) { defer func() { if r := recover(); r != nil { o.status <- true diff --git a/vendor/oras.land/oras-go/v2/pack.go b/vendor/oras.land/oras-go/v2/pack.go index 1b995612..6d1635c9 100644 --- a/vendor/oras.land/oras-go/v2/pack.go +++ b/vendor/oras.land/oras-go/v2/pack.go @@ -48,8 +48,8 @@ const ( var ( // ErrInvalidDateTimeFormat is returned by [Pack] and [PackManifest] when - // AnnotationArtifactCreated or AnnotationCreated is provided, but its value - // is not in RFC 3339 format. + // "org.opencontainers.artifact.created" or "org.opencontainers.image.created" + // is provided, but its value is not in RFC 3339 format. // Reference: https://www.rfc-editor.org/rfc/rfc3339#section-5.6 ErrInvalidDateTimeFormat = errors.New("invalid date and time format") @@ -78,8 +78,8 @@ const ( PackManifestVersion1_1_RC4 PackManifestVersion = PackManifestVersion1_1 // PackManifestVersion1_1 represents the OCI Image Manifest defined in - // image-spec v1.1.0. - // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md + // image-spec v1.1.1. + // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.1/manifest.md PackManifestVersion1_1 PackManifestVersion = 2 ) @@ -93,7 +93,10 @@ type PackManifestOptions struct { // Layers is the layers of the manifest. Layers []ocispec.Descriptor - // ManifestAnnotations is the annotation map of the manifest. + // ManifestAnnotations is the annotation map of the manifest. In order to + // make [PackManifest] reproducible, set the key ocispec.AnnotationCreated + // (i.e. "org.opencontainers.image.created") to a fixed value. The value + // must conform to RFC 3339. ManifestAnnotations map[string]string // ConfigDescriptor is a pointer to the descriptor of the config blob. @@ -107,9 +110,9 @@ type PackManifestOptions struct { // mediaTypeRegexp checks the format of media types. // References: -// - https://github.com/opencontainers/image-spec/blob/v1.1.0/schema/defs-descriptor.json#L7 +// - https://github.com/opencontainers/image-spec/blob/v1.1.1/schema/defs-descriptor.json#L7 // - https://datatracker.ietf.org/doc/html/rfc6838#section-4.2 -var mediaTypeRegexp = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126}$`) +var mediaTypeRegexp = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}$`) // PackManifest generates an OCI Image Manifest based on the given parameters // and pushes the packed manifest to a content storage using pusher. The version @@ -126,6 +129,12 @@ var mediaTypeRegexp = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9!#$&-^_.+]{0,126 // // artifactType and opts.ConfigDescriptor.MediaType MUST comply with RFC 6838. // +// Each time when PackManifest is called, if a time stamp is not specified, a new time +// stamp is generated in the manifest annotations with the key ocispec.AnnotationCreated +// (i.e. "org.opencontainers.image.created"). To make [PackManifest] reproducible, +// set the key ocispec.AnnotationCreated to a fixed value in +// opts.ManifestAnnotations. The value MUST conform to RFC 3339. +// // If succeeded, returns a descriptor of the packed manifest. func PackManifest(ctx context.Context, pusher content.Pusher, packManifestVersion PackManifestVersion, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) { switch packManifestVersion { @@ -292,8 +301,8 @@ func packManifestV1_1_RC2(ctx context.Context, pusher content.Pusher, configMedi return pushManifest(ctx, pusher, manifest, manifest.MediaType, manifest.Config.MediaType, manifest.Annotations) } -// packManifestV1_1 packs an image manifest defined in image-spec v1.1.0. -// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#guidelines-for-artifact-usage +// packManifestV1_1 packs an image manifest defined in image-spec v1.1.1. +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.1/manifest.md#guidelines-for-artifact-usage func packManifestV1_1(ctx context.Context, pusher content.Pusher, artifactType string, opts PackManifestOptions) (ocispec.Descriptor, error) { if artifactType == "" && (opts.ConfigDescriptor == nil || opts.ConfigDescriptor.MediaType == ocispec.MediaTypeEmptyJSON) { // artifactType MUST be set when config.mediaType is set to the empty value diff --git a/vendor/oras.land/oras-go/v2/registry/reference.go b/vendor/oras.land/oras-go/v2/registry/reference.go index fc3e95e5..54c72fd2 100644 --- a/vendor/oras.land/oras-go/v2/registry/reference.go +++ b/vendor/oras.land/oras-go/v2/registry/reference.go @@ -34,13 +34,13 @@ var ( // // References: // - https://github.com/distribution/distribution/blob/v2.7.1/reference/regexp.go#L53 - // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pulling-manifests + // - https://github.com/opencontainers/distribution-spec/blob/v1.1.1/spec.md#pulling-manifests repositoryRegexp = regexp.MustCompile(`^[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*)*$`) // tagRegexp checks the tag name. // The docker and OCI spec have the same regular expression. // - // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#pulling-manifests + // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.1/spec.md#pulling-manifests tagRegexp = regexp.MustCompile(`^[\w][\w.-]{0,127}$`) ) diff --git a/vendor/oras.land/oras-go/v2/registry/registry.go b/vendor/oras.land/oras-go/v2/registry/registry.go index e1da0ab9..4736efa8 100644 --- a/vendor/oras.land/oras-go/v2/registry/registry.go +++ b/vendor/oras.land/oras-go/v2/registry/registry.go @@ -31,7 +31,7 @@ type Registry interface { // Note: When implemented by a remote registry, the catalog API is called. // However, not all registries supports pagination or conforms the // specification. - // Reference: https://docs.docker.com/registry/spec/api/#catalog + // Reference: https://distribution.github.io/distribution/spec/api/#catalog // See also `Repositories()` in this package. Repositories(ctx context.Context, last string, fn func(repos []string) error) error diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go index d11c092b..80091ddf 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/cache.go @@ -109,7 +109,7 @@ func (cc *concurrentCache) Set(ctx context.Context, registry string, scheme Sche }, " ") statusValue, _ := cc.status.LoadOrStore(statusKey, syncutil.NewOnce()) fetchOnce := statusValue.(*syncutil.Once) - fetchedFirst, result, err := fetchOnce.Do(ctx, func() (interface{}, error) { + fetchedFirst, result, err := fetchOnce.Do(ctx, func() (any, error) { return fetch(ctx) }) if fetchedFirst { diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/challenge.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/challenge.go index 58bdefda..ffc52f8e 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/challenge.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/challenge.go @@ -62,7 +62,7 @@ func (s Scheme) String() string { // parseChallenge parses the "WWW-Authenticate" header returned by the remote // registry, and extracts parameters if scheme is Bearer. // References: -// - https://docs.docker.com/registry/spec/auth/token/#how-to-authenticate +// - https://distribution.github.io/distribution/spec/auth/token/#how-to-authenticate // - https://tools.ietf.org/html/rfc7235#section-2.1 func parseChallenge(header string) (scheme Scheme, params map[string]string) { // as defined in RFC 7235 section 2.1, we have diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go index 8d9685a2..ce9297af 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/client.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/url" "strings" @@ -51,7 +52,7 @@ var DefaultClient = &Client{ // limit, which is usually 16 KiB. As specified by the distribution, the // response may contain 2 identical tokens, that is, 16 x 2 = 32 KiB. // Hence, 128 KiB should be sufficient. -// References: https://docs.docker.com/registry/spec/auth/token/ +// References: https://distribution.github.io/distribution/spec/auth/token/ var maxResponseBytes int64 = 128 * 1024 // 128 KiB // defaultClientID specifies the default client ID used in OAuth2. @@ -110,15 +111,15 @@ type Client struct { // ClientID used in fetching OAuth2 token as a required field. // If empty, a default client ID is used. - // Reference: https://docs.docker.com/registry/spec/auth/oauth/#getting-a-token + // Reference: https://distribution.github.io/distribution/spec/auth/oauth/#getting-a-token ClientID string // ForceAttemptOAuth2 controls whether to follow OAuth2 with password grant // instead the distribution spec when authenticating using username and // password. // References: - // - https://docs.docker.com/registry/spec/auth/jwt/ - // - https://docs.docker.com/registry/spec/auth/oauth/ + // - https://distribution.github.io/distribution/spec/auth/jwt/ + // - https://distribution.github.io/distribution/spec/auth/oauth/ ForceAttemptOAuth2 bool } @@ -136,7 +137,50 @@ func (c *Client) send(req *http.Request) (*http.Response, error) { for key, values := range c.Header { req.Header[key] = append(req.Header[key], values...) } - return c.client().Do(req) + // Drop the Authorization header when a redirect crosses an HTTP origin + // (scheme, host, or port). The standard library only strips sensitive + // headers when the hostname changes, so a redirect to a different port on + // the same host would otherwise forward credentials to an unintended + // endpoint. Any caller-provided CheckRedirect is preserved. + // Reference: https://github.com/oras-project/oras-go/security/advisories/GHSA-vh4v-2xq2-g5cg + client := c.client() + clientCopy := *client + checkRedirect := client.CheckRedirect + clientCopy.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if len(via) > 0 && !sameHTTPOrigin(via[len(via)-1].URL, req.URL) { + req.Header.Del("Authorization") + } + if checkRedirect != nil { + return checkRedirect(req, via) + } + return nil + } + return clientCopy.Do(req) +} + +// sameHTTPOrigin reports whether a and b share the same HTTP origin, i.e. the +// same scheme and host. Default ports are normalized so that, for example, +// "example.com" and "example.com:443" compare equal over https. +func sameHTTPOrigin(a, b *url.URL) bool { + if !strings.EqualFold(a.Scheme, b.Scheme) { + return false + } + return canonicalHost(a) == canonicalHost(b) +} + +// canonicalHost returns the lower-cased host of u with the default port for its +// scheme applied when no explicit port is present. +func canonicalHost(u *url.URL) string { + port := u.Port() + if port == "" { + switch strings.ToLower(u.Scheme) { + case "https": + port = "443" + case "http": + port = "80" + } + } + return strings.ToLower(u.Hostname()) + ":" + port } // credential resolves the credential for the given registry. @@ -156,6 +200,49 @@ func (c *Client) cache() Cache { return c.Cache } +// validateRealm rejects bearer token realm URLs that would have the client +// forward credentials to obviously unsafe destinations: +// +// - schemes other than http or https, +// - http realms when the registry was contacted over https (TLS downgrade), +// - hosts that are IP literals in loopback, link-local, private, or +// unspecified ranges (e.g. cloud instance metadata services such as +// 169.254.169.254). +// +// Cross-host realms with a public hostname are permitted, because the +// distribution spec allows a separate token endpoint (e.g. Docker Hub's +// auth.docker.io). When the registry itself is reached at the same hostname +// as the realm, the IP-literal check is skipped so loopback and in-cluster +// deployments continue to work. +func validateRealm(realm string, registryURL *url.URL) error { + if realm == "" { + return nil + } + realmURL, err := url.Parse(realm) + if err != nil { + return fmt.Errorf("failed to parse bearer realm %q: %w", realm, err) + } + switch realmURL.Scheme { + case "https": + // always allowed + case "http": + if registryURL != nil && registryURL.Scheme == "https" { + return fmt.Errorf("bearer realm %q uses http but registry was contacted over https", realm) + } + default: + return fmt.Errorf("bearer realm %q uses unsupported scheme %q", realm, realmURL.Scheme) + } + if ip := net.ParseIP(realmURL.Hostname()); ip != nil { + if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || + ip.IsPrivate() || ip.IsUnspecified() { + if registryURL == nil || realmURL.Hostname() != registryURL.Hostname() { + return fmt.Errorf("bearer realm host %q is a loopback, link-local, private, or unspecified address", realmURL.Hostname()) + } + } + } + return nil +} + // SetUserAgent sets the user agent for all out-going requests. func (c *Client) SetUserAgent(userAgent string) { if c.Header == nil { @@ -182,6 +269,9 @@ func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { var attemptedKey string cache := c.cache() host := originalReq.Host + if host == "" { + host = originalReq.URL.Host + } scheme, err := cache.GetScheme(ctx, host) if err == nil { switch scheme { @@ -207,6 +297,13 @@ func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { if resp.StatusCode != http.StatusUnauthorized { return resp, nil } + // If the challenge came from a different origin than originally requested + // (e.g. the request was redirected to another host or port), do not resolve + // or send the registry credentials to that origin. + // Reference: https://github.com/oras-project/oras-go/security/advisories/GHSA-vh4v-2xq2-g5cg + if resp.Request != nil && !sameHTTPOrigin(originalReq.URL, resp.Request.URL) { + return resp, nil + } // attempt again with credentials for recognized schemes challenge := resp.Header.Get("Www-Authenticate") @@ -257,6 +354,9 @@ func (c *Client) Do(originalReq *http.Request) (*http.Response, error) { // attempt with credentials realm := params["realm"] + if err := validateRealm(realm, originalReq.URL); err != nil { + return nil, fmt.Errorf("%s %q: %w", resp.Request.Method, resp.Request.URL, err) + } service := params["service"] token, err := cache.Set(ctx, host, SchemeBearer, key, func(ctx context.Context) (string, error) { return c.fetchBearerToken(ctx, host, realm, service, scopes) @@ -312,8 +412,8 @@ func (c *Client) fetchBearerToken(ctx context.Context, registry, realm, service // specification. // It fetches anonymous tokens if no credential is provided. // References: -// - https://docs.docker.com/registry/spec/auth/jwt/ -// - https://docs.docker.com/registry/spec/auth/token/ +// - https://distribution.github.io/distribution/spec/auth/jwt/ +// - https://distribution.github.io/distribution/spec/auth/token/ func (c *Client) fetchDistributionToken(ctx context.Context, realm, service string, scopes []string, username, password string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, realm, nil) if err != nil { @@ -340,7 +440,7 @@ func (c *Client) fetchDistributionToken(ctx context.Context, realm, service stri return "", errutil.ParseErrorResponse(resp) } - // As specified in https://docs.docker.com/registry/spec/auth/token/ section + // As specified in https://distribution.github.io/distribution/spec/auth/token/ section // "Token Response Fields", the token is either in `token` or // `access_token`. If both present, they are identical. var result struct { @@ -361,7 +461,7 @@ func (c *Client) fetchDistributionToken(ctx context.Context, realm, service stri } // fetchOAuth2Token fetches an OAuth2 access token. -// Reference: https://docs.docker.com/registry/spec/auth/oauth/ +// Reference: https://distribution.github.io/distribution/spec/auth/oauth/ func (c *Client) fetchOAuth2Token(ctx context.Context, realm, service string, scopes []string, cred Credential) (string, error) { form := url.Values{} if cred.RefreshToken != "" { diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/credential.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/credential.go index 013305f7..044bcaec 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/credential.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/credential.go @@ -30,11 +30,11 @@ type Credential struct { // RefreshToken is a bearer token to be sent to the authorization service // for fetching access tokens. // A refresh token is often referred as an identity token. - // Reference: https://docs.docker.com/registry/spec/auth/oauth/ + // Reference: https://distribution.github.io/distribution/spec/auth/oauth/ RefreshToken string // AccessToken is a bearer token to be sent to the registry. // An access token is often referred as a registry token. - // Reference: https://docs.docker.com/registry/spec/auth/token/ + // Reference: https://distribution.github.io/distribution/spec/auth/token/ AccessToken string } diff --git a/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go b/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go index d81cc0d4..4e515a81 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/auth/scope.go @@ -24,7 +24,7 @@ import ( ) // Actions used in scopes. -// Reference: https://docs.docker.com/registry/spec/auth/scope/ +// Reference: https://distribution.github.io/distribution/spec/auth/scope/ const ( // ActionPull represents generic read access for resources of the repository // type. @@ -43,7 +43,7 @@ const ( const ScopeRegistryCatalog = "registry:catalog:*" // ScopeRepository returns a repository scope with given actions. -// Reference: https://docs.docker.com/registry/spec/auth/scope/ +// Reference: https://distribution.github.io/distribution/spec/auth/scope/ func ScopeRepository(repository string, actions ...string) string { actions = cleanActions(actions) if repository == "" || len(actions) == 0 { @@ -98,7 +98,7 @@ type scopesContextKey struct{} // Passing an empty list of scopes will virtually remove the scope hints in the // context. // -// Reference: https://docs.docker.com/registry/spec/auth/scope/ +// Reference: https://distribution.github.io/distribution/spec/auth/scope/ func WithScopes(ctx context.Context, scopes ...string) context.Context { scopes = CleanScopes(scopes) return context.WithValue(ctx, scopesContextKey{}, scopes) @@ -143,7 +143,7 @@ type scopesForHostContextKey string // Passing an empty list of scopes will virtually remove the scope hints in the // context for the given host. // -// Reference: https://docs.docker.com/registry/spec/auth/scope/ +// Reference: https://distribution.github.io/distribution/spec/auth/scope/ func WithScopesForHost(ctx context.Context, host string, scopes ...string) context.Context { scopes = CleanScopes(scopes) return context.WithValue(ctx, scopesForHostContextKey(host), scopes) @@ -254,7 +254,7 @@ func CleanScopes(scopes []string) []string { actionSet = make(map[string]struct{}) namedActions[resourceName] = actionSet } - for _, action := range strings.Split(actions, ",") { + for action := range strings.SplitSeq(actions, ",") { if action != "" { actionSet[action] = struct{}{} } @@ -303,7 +303,7 @@ func cleanActions(actions []string) []string { // slow path slices.Sort(actions) n := 0 - for i := 0; i < len(actions); i++ { + for i := range len(actions) { if actions[i] == "*" { return []string{"*"} } diff --git a/vendor/oras.land/oras-go/v2/registry/remote/errcode/errors.go b/vendor/oras.land/oras-go/v2/registry/remote/errcode/errors.go index 9f87d86d..a32f1e5e 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/errcode/errors.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/errcode/errors.go @@ -24,8 +24,8 @@ import ( ) // References: -// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#error-codes -// - https://docs.docker.com/registry/spec/api/#errors-2 +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.1/spec.md#error-codes +// - https://distribution.github.io/distribution/spec/api/#errors-2 const ( ErrorCodeBlobUnknown = "BLOB_UNKNOWN" ErrorCodeBlobUploadInvalid = "BLOB_UPLOAD_INVALID" @@ -45,8 +45,8 @@ const ( // Error represents a response inner error returned by the remote // registry. // References: -// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#error-codes -// - https://docs.docker.com/registry/spec/api/#errors-2 +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.1/spec.md#error-codes +// - https://distribution.github.io/distribution/spec/api/#errors-2 type Error struct { Code string `json:"code"` Message string `json:"message"` @@ -73,8 +73,8 @@ func (e Error) Error() string { // Errors represents a list of response inner errors returned by the remote // server. // References: -// - https://github.com/opencontainers/distribution-spec/blob/v1.1.0/spec.md#error-codes -// - https://docs.docker.com/registry/spec/api/#errors-2 +// - https://github.com/opencontainers/distribution-spec/blob/v1.1.1/spec.md#error-codes +// - https://distribution.github.io/distribution/spec/api/#errors-2 type Errors []Error // Error returns a error string describing the error. diff --git a/vendor/oras.land/oras-go/v2/registry/remote/manifest.go b/vendor/oras.land/oras-go/v2/registry/remote/manifest.go index 0e10297c..845bf6c5 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/manifest.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/manifest.go @@ -16,6 +16,7 @@ limitations under the License. package remote import ( + "slices" "strings" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -41,12 +42,7 @@ func isManifest(manifestMediaTypes []string, desc ocispec.Descriptor) bool { if len(manifestMediaTypes) == 0 { manifestMediaTypes = defaultManifestMediaTypes } - for _, mediaType := range manifestMediaTypes { - if desc.MediaType == mediaType { - return true - } - } - return false + return slices.Contains(manifestMediaTypes, desc.MediaType) } // manifestAcceptHeader generates the set in the `Accept` header for resolving diff --git a/vendor/oras.land/oras-go/v2/registry/remote/referrers.go b/vendor/oras.land/oras-go/v2/registry/remote/referrers.go index 74668089..f2ad0b2d 100644 --- a/vendor/oras.land/oras-go/v2/registry/remote/referrers.go +++ b/vendor/oras.land/oras-go/v2/registry/remote/referrers.go @@ -17,6 +17,7 @@ package remote import ( "errors" + "fmt" "strings" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -102,11 +103,14 @@ func (e *ReferrersError) IsReferrersIndexDelete() bool { // buildReferrersTag builds the referrers tag for the given manifest descriptor. // Format: