12 Commits

Author SHA1 Message Date
Justin Cormack
3bf33c3a11 Merge pull request #4212 from europaul/fix/nil-deref-release-tag
pkg build: fix nil pointer dereference when releasing image only in registry
2026-03-27 12:26:05 +00:00
Paul Gaiduk
420d550a9e pkg build: fix nil pointer dereference when releasing image only in registry
When an image exists in the registry but not in local cache and a
release tag is requested, FindDescriptor returns nil causing a panic
at build.go:588. This was a regression introduced in 4129cc799 which
removed the early return for missing local cache images.

Fix by pulling the image into local cache when the descriptor is nil
and a release is needed. Also guard the targetDocker block against
nil descriptors, and fix the FindDescriptor mock to return nil,nil
(matching the real implementation) instead of an error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Paul Gaiduk <paulg@zededa.com>
2026-03-25 18:53:24 +01:00
Justin Cormack
4cfb70d3cc Merge pull request #4207 from europaul/fix/load-files-into-container
pkg build: fix builder config and certs not copied into new containers
2026-03-18 18:08:17 +00:00
Paul Gaiduk
3751bb6d79 pkg build: fix builder config and certs not copied into new containers
LoadConfigFiles() was only called inside the container-inspect block,
so filesToLoadIntoContainer was never populated when no builder
container existed yet. The subsequent copyFilesToContainer() call
received a nil map, sending an empty tar archive and leaving
/etc/buildkit/ empty inside the newly created container.

Move the LoadConfigFiles() call before the inspect check so the config
and certificate data is always available when creating a fresh builder.

Co-Authored-By: Claude <noreply@anthropic.com>

Signed-off-by: Paul Gaiduk <paulg@zededa.com>
2026-03-12 20:28:03 +01:00
Justin Cormack
bdef7e865a Merge pull request #4205 from rucoder/rucoder/env-vars-for-ci
pkg build: add env var support for mirror, org, builder image and config
2026-03-12 11:42:30 +00:00
Mikhail Malyshev
666bbfdbd5 pkg build: add env var support for mirror, org, builder image and config
Introduce environment variables for key CI/CD flags so that self-hosted
runners (e.g. GitHub Actions) can configure registry mirrors and push
targets without modifying calling Makefiles:

- LINUXKIT_MIRROR         - equivalent to --mirror (space/comma-separated);
                            CLI flags take precedence (last SetProxy wins)
- LINUXKIT_PKG_ORG        - equivalent to --org for all pkg subcommands
- LINUXKIT_BUILDER_IMAGE  - equivalent to --builder-image
- LINUXKIT_BUILDER_CONFIG - equivalent to --builder-config

All env var constants are consolidated in pkg_build.go alongside the
existing LINUXKIT_CACHE, LINUXKIT_BUILDER_NAME, LINUXKIT_BUILDERS.

Priority for all: CLI flag > env var > built-in default

Adds a new Environment Variables section to docs/packages.md with a
reference table covering all LINUXKIT_* vars and a note explaining the
two-layer mirror configuration required in CI (linuxkit pulls vs
buildkit Dockerfile pulls).

Signed-off-by: Roman Shaposhnik <rucoder@gmail.com>
Signed-off-by: Mikhail Malyshev <mike.malyshev@gmail.com>
2026-03-12 10:48:39 +00:00
Justin Cormack
c766f572ce Merge pull request #4204 from rucoder/rucoder/per-user-builder-name
pkg build: make buildkit builder container name configurable
2026-03-12 10:27:40 +00:00
Mikhail Malyshev
72a76e5b79 pkg build: use named volume to persist buildkit cache across restarts
The moby/buildkit image declares VOLUME /var/lib/buildkit, which causes
Docker to create an anonymous volume when no explicit mount is given.
These anonymous volumes are orphaned every time the builder container is
recreated (--builder-restart, config change, privilege fix), leaking
disk space.

Switch to a named volume (<builder-name>-state) that is explicitly
mounted on container creation. This:

- Preserves build cache across container restarts, config changes, and
  privilege fixes, making rebuilds faster.
- Eliminates anonymous volume leaks.
- Removes the state volume when the builder image version changes, since
  buildkit state compatibility across versions is not guaranteed.

Signed-off-by: Mikhail Malyshev <mike.malyshev@gmail.com>
2026-03-07 13:29:37 +00:00
Mikhail Malyshev
a85160e4d6 pkg build: make buildkit builder container name configurable
On shared servers where multiple users build packages against the same
Docker daemon, all users fight over a single hardcoded builder container
named "linuxkit-builder". One user's build can destroy another's
in-flight build when builder lifecycle management detects mismatches.

Make the builder container name configurable:

1. --builder-name CLI flag (highest priority)
2. LINUXKIT_BUILDER_NAME environment variable
3. "linuxkit-builder" default (original behavior, unchanged)

The flag is available on both "linuxkit pkg build" and
"linuxkit pkg builder" (du/prune) commands. Users on shared servers
can set LINUXKIT_BUILDER_NAME or pass --builder-name to get per-user
isolation (e.g. LINUXKIT_BUILDER_NAME=linuxkit-builder-$USER).

Signed-off-by: Mikhail Malyshev <mike.malyshev@gmail.com>
2026-03-07 12:58:36 +00:00
Mikhail Malyshev
ccb0787e2a pkg build: refactor builder parameters into BuilderConfig struct
Group the four builder-related fields (name, image, config path, restart)
that always travel together into a BuilderConfig struct. This simplifies:

- DockerRunner interface (Build() and Builder() lose 3 params each)
- buildOpts struct (4 fields -> 1)
- buildArch() function signature (3 fewer params)
- DiskUsage() / PruneBuilder() / getClientForPlatform() signatures
- 4 WithBuildBuilder*() option functions -> 1 WithBuildBuilderConfig()

Also rename the confusingly-named "builderName" local variables in
buildArch() and getClientForPlatform() to "dockerContext", which better
reflects their actual purpose (they hold a Docker context name, not the
builder container name).

No behavioral changes.

Signed-off-by: Mikhail Malyshev <mike.malyshev@gmail.com>
2026-02-26 08:20:01 +00:00
Avi Deitcher
e0151386c8 bump buildkit version and deps (#4202)
Signed-off-by: Avi Deitcher <avi@deitcher.net>
2026-01-18 14:32:24 +02:00
Avi Deitcher
4129cc7999 push release tags even when digest tag already is there (#4201)
Signed-off-by: Avi Deitcher <avi@deitcher.net>
2026-01-15 20:01:04 +02:00
635 changed files with 49991 additions and 14434 deletions

View File

@@ -382,6 +382,38 @@ ARG all_proxy
LinuxKit does not judge between lower-cased or upper-cased variants of these options, e.g. `http_proxy` vs `HTTP_PROXY`, LinuxKit does not judge between lower-cased or upper-cased variants of these options, e.g. `http_proxy` vs `HTTP_PROXY`,
as `docker build` does not either. It just passes them through "as-is". as `docker build` does not either. It just passes them through "as-is".
### Environment Variables
The following environment variables can be used to configure `linuxkit` without
modifying command-line invocations — useful for CI/CD runners and shared build
scripts. CLI flags always take precedence over env vars, which take precedence
over built-in defaults.
| Variable | Equivalent flag | Scope | Description |
|---|---|---|---|
| `LINUXKIT_MIRROR` | `--mirror` | All commands | Space- or comma-separated list of mirror specs, each in `[<registry>=]<url>` format (same as `--mirror`). E.g. `LINUXKIT_MIRROR=docker.io=http://mymirror.local` |
| `LINUXKIT_PKG_ORG` | `--org` | `pkg` subcommands | Override the registry organisation used when tagging and pushing packages. E.g. `LINUXKIT_PKG_ORG=myorg/lfedge` |
| `LINUXKIT_BUILDER_IMAGE` | `--builder-image` | `pkg build` | buildkit container image to use. Useful when the builder image must come from an internal mirror. |
| `LINUXKIT_BUILDER_CONFIG` | `--builder-config` | `pkg build` | Path to a buildkit `config.toml` file. The primary way to configure buildkit's own registry mirrors for `FROM` pulls inside Dockerfiles. |
| `LINUXKIT_BUILDER_NAME` | `--builder-name` | `pkg build` | Name of the buildkit builder container. |
| `LINUXKIT_BUILDERS` | `--builders` | `pkg build` | Platform-to-builder-context mapping; see [Providing native builder nodes](#providing-native-builder-nodes). |
| `LINUXKIT_CACHE` | `--cache` | All commands | Path to the linuxkit OCI image cache directory (default `~/.linuxkit/cache`). |
#### Registry mirrors in CI
There are two layers of registry access in `linuxkit pkg build`, each requiring
its own mirror configuration:
1. **linuxkit's own pulls** (cache lookups, `show-tag`, `cache pull`, etc.) —
configure via `LINUXKIT_MIRROR` or `--mirror`.
2. **buildkit's pulls** (`FROM` and `COPY --from` inside Dockerfiles) —
buildkit has its own registry client. Configure its mirrors via a
`config.toml` file passed through `LINUXKIT_BUILDER_CONFIG` or
`--builder-config`. See the
[buildkit registry configuration docs](https://github.com/moby/buildkit/blob/master/docs/buildkitd.toml.md)
for the file format.
## Build Args ## Build Args
`linuxkit` does not support passing random CLI flags for build arguments when building packages. `linuxkit` does not support passing random CLI flags for build arguments when building packages.

View File

@@ -58,6 +58,12 @@ func newCmd() *cobra.Command {
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
readConfig() readConfig()
// prepend mirrors from env var so CLI flags take precedence (last SetProxy call wins)
if envMirrors := os.Getenv(envVarMirror); envMirrors != "" {
envList := strings.FieldsFunc(envMirrors, func(r rune) bool { return r == ',' || r == ' ' })
mirrorsRaw = append(envList, mirrorsRaw...)
}
// convert the provided mirrors to a map // convert the provided mirrors to a map
for _, m := range mirrorsRaw { for _, m := range mirrorsRaw {
if m == "" { if m == "" {
@@ -115,7 +121,7 @@ func newCmd() *cobra.Command {
cmd.AddCommand(versionCmd()) cmd.AddCommand(versionCmd())
cmd.PersistentFlags().StringVar(&cacheDir, "cache", defaultLinuxkitCache(), fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir)) cmd.PersistentFlags().StringVar(&cacheDir, "cache", defaultLinuxkitCache(), fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir))
cmd.PersistentFlags().StringArrayVar(&mirrorsRaw, "mirror", nil, "Mirror to use for pulling images, format is <registry>=<mirror>, e.g. docker.io=http://mymirror.io, or just http://mymirror.io for all not otherwise specified; must include protocol. Can be provided multiple times.") cmd.PersistentFlags().StringArrayVar(&mirrorsRaw, "mirror", nil, fmt.Sprintf("Mirror to use for pulling images, format is <registry>=<mirror>, e.g. docker.io=http://mymirror.io, or just http://mymirror.io for all not otherwise specified; must include protocol. Can be provided multiple times. Also read from env var %s (space/comma-separated list); CLI flags take precedence.", envVarMirror))
cmd.PersistentFlags().StringArrayVar(&certFiles, "cert-file", nil, "Path to certificate files to use for pulling images, can be provided multiple times. Will augment system-provided certs.") cmd.PersistentFlags().StringArrayVar(&certFiles, "cert-file", nil, "Path to certificate files to use for pulling images, can be provided multiple times. Will augment system-provided certs.")
cmd.PersistentFlags().BoolVarP(&flagQuiet, "quiet", "q", false, "Quiet execution") cmd.PersistentFlags().BoolVarP(&flagQuiet, "quiet", "q", false, "Quiet execution")
cmd.PersistentFlags().IntVarP(&flagVerbose, flagVerboseName, "v", 1, "Verbosity of logging: 0 = quiet, 1 = info, 2 = debug, 3 = trace. Default is info. Setting it explicitly will create structured logging lines.") cmd.PersistentFlags().IntVarP(&flagVerbose, flagVerboseName, "v", 1, "Verbosity of logging: 0 = quiet, 1 = info, 2 = debug, 3 = trace. Default is info. Setting it explicitly will create structured logging lines.")

View File

@@ -1,6 +1,8 @@
module github.com/linuxkit/linuxkit/src/cmd/linuxkit module github.com/linuxkit/linuxkit/src/cmd/linuxkit
go 1.24.2 go 1.24.3
toolchain go1.24.12
require ( require (
github.com/Azure/azure-sdk-for-go v56.3.0+incompatible github.com/Azure/azure-sdk-for-go v56.3.0+incompatible
@@ -11,7 +13,7 @@ require (
github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/go-winio v0.6.2
github.com/ScaleFT/sshkeys v0.0.0-20181112160850-82451a803681 github.com/ScaleFT/sshkeys v0.0.0-20181112160850-82451a803681
github.com/aws/aws-sdk-go v1.44.82 github.com/aws/aws-sdk-go v1.44.82
github.com/docker/cli v28.2.2+incompatible github.com/docker/cli v28.5.0+incompatible
github.com/docker/docker v28.2.2+incompatible github.com/docker/docker v28.2.2+incompatible
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0
github.com/google/go-containerregistry v0.20.3 github.com/google/go-containerregistry v0.20.3
@@ -19,7 +21,7 @@ require (
github.com/gophercloud/gophercloud v0.1.0 github.com/gophercloud/gophercloud v0.1.0
github.com/gophercloud/utils v0.0.0-20181029231510-34f5991525d1 github.com/gophercloud/utils v0.0.0-20181029231510-34f5991525d1
github.com/klauspost/pgzip v1.2.5 github.com/klauspost/pgzip v1.2.5
github.com/moby/buildkit v0.23.1 github.com/moby/buildkit v0.26.3
github.com/moby/hyperkit v0.0.0-20180416161519-d65b09c1c28a github.com/moby/hyperkit v0.0.0-20180416161519-d65b09c1c28a
//github.com/moby/moby v20.10.3-0.20220728162118-71cb54cec41e+incompatible // indirect //github.com/moby/moby v20.10.3-0.20220728162118-71cb54cec41e+incompatible // indirect
github.com/moby/term v0.5.2 github.com/moby/term v0.5.2
@@ -33,36 +35,36 @@ require (
github.com/rn/iso9660wrap v0.0.0-20171120145750-baf8d62ad315 github.com/rn/iso9660wrap v0.0.0-20171120145750-baf8d62ad315
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.11.1
github.com/surma/gocpio v1.0.2-0.20160926205914-fcb68777e7dc github.com/surma/gocpio v1.0.2-0.20160926205914-fcb68777e7dc
github.com/vmware/govmomi v0.20.3 github.com/vmware/govmomi v0.20.3
github.com/xeipuuv/gojsonschema v1.2.0 github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.37.0 golang.org/x/crypto v0.42.0
golang.org/x/net v0.39.0 golang.org/x/net v0.44.0
golang.org/x/oauth2 v0.27.0 golang.org/x/oauth2 v0.30.0
golang.org/x/sync v0.14.0 golang.org/x/sync v0.17.0
golang.org/x/sys v0.33.0 golang.org/x/sys v0.37.0
golang.org/x/term v0.31.0 golang.org/x/term v0.35.0
google.golang.org/api v0.149.0 google.golang.org/api v0.149.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
require ( require (
github.com/Code-Hex/vz/v3 v3.0.0 github.com/Code-Hex/vz/v3 v3.0.0
github.com/containerd/containerd/v2 v2.1.3 github.com/containerd/containerd/v2 v2.2.0
github.com/containerd/platforms v1.0.0-rc.1 github.com/containerd/platforms v1.0.0-rc.2
github.com/docker/buildx v0.21.1 github.com/docker/buildx v0.21.1
github.com/equinix/equinix-sdk-go v0.42.0 github.com/equinix/equinix-sdk-go v0.42.0
github.com/hashicorp/go-version v1.7.0 github.com/hashicorp/go-version v1.7.0
github.com/in-toto/in-toto-golang v0.9.0 github.com/in-toto/in-toto-golang v0.9.0
github.com/moby/sys/capability v0.3.0 github.com/moby/sys/capability v0.4.0
github.com/spdx/tools-golang v0.5.5 github.com/spdx/tools-golang v0.5.5
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/compute/metadata v0.7.0 // indirect
github.com/Azure/go-autorest v14.2.1-0.20210115164004-c0fe8b0fea3d+incompatible // indirect github.com/Azure/go-autorest v14.2.1-0.20210115164004-c0fe8b0fea3d+incompatible // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
@@ -70,14 +72,14 @@ require (
github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/agext/levenshtein v1.2.3 // indirect github.com/agext/levenshtein v1.2.3 // indirect
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/containerd/console v1.0.5 // indirect github.com/containerd/console v1.0.5 // indirect
github.com/containerd/containerd/api v1.9.0 // indirect github.com/containerd/containerd/api v1.10.0 // indirect
github.com/containerd/continuity v0.4.5 // indirect github.com/containerd/continuity v0.4.5 // indirect
github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect github.com/containerd/stargz-snapshotter/estargz v0.17.0 // indirect
github.com/containerd/ttrpc v1.2.7 // indirect github.com/containerd/ttrpc v1.2.7 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/creack/goselect v0.0.0-20180501195510-58854f77ee8d // indirect github.com/creack/goselect v0.0.0-20180501195510-58854f77ee8d // indirect
@@ -89,26 +91,24 @@ require (
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fvbommel/sortorder v1.0.1 // indirect github.com/fvbommel/sortorder v1.0.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/gofrs/flock v0.12.1 // indirect github.com/gofrs/flock v0.13.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect github.com/google/s2a-go v0.1.7 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.1 // indirect
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3 // indirect github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
@@ -127,7 +127,7 @@ require (
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.6.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/smartystreets/goconvey v1.8.1 // indirect github.com/smartystreets/goconvey v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
@@ -136,28 +136,28 @@ require (
github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
github.com/vbatts/tar-split v0.12.1 // indirect github.com/vbatts/tar-split v0.12.2 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.35.0 // indirect go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect
golang.org/x/mod v0.24.0 // indirect golang.org/x/mod v0.29.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.11.0 // indirect golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 // indirect
google.golang.org/grpc v1.72.2 // indirect google.golang.org/grpc v1.76.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.10 // indirect
) )

View File

@@ -1,6 +1,6 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/azure-sdk-for-go v56.3.0+incompatible h1:DmhwMrUIvpeoTDiWRDtNHqelNUd3Og8JCkrLHQK795c= github.com/Azure/azure-sdk-for-go v56.3.0+incompatible h1:DmhwMrUIvpeoTDiWRDtNHqelNUd3Og8JCkrLHQK795c=
@@ -31,8 +31,8 @@ github.com/Code-Hex/vz/v3 v3.0.0 h1:UuYAZz8NF8n+R4MLfn/lpUlepgzZ8BlhKtioQ+q9LXk=
github.com/Code-Hex/vz/v3 v3.0.0/go.mod h1:+xPQOXVzNaZ4OeIIhlJFumtbFhsvRfqKwX7wuZS4dFA= github.com/Code-Hex/vz/v3 v3.0.0/go.mod h1:+xPQOXVzNaZ4OeIIhlJFumtbFhsvRfqKwX7wuZS4dFA=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= github.com/Microsoft/hcsshim v0.14.0-rc.1 h1:qAPXKwGOkVn8LlqgBN8GS0bxZ83hOJpcjxzmlQKxKsQ=
github.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok= github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c=
github.com/ScaleFT/sshkeys v0.0.0-20181112160850-82451a803681 h1:JS2rl38kZmHgWa0xINSaSYH0Whtvem64/4+Ef0+Y5pE= github.com/ScaleFT/sshkeys v0.0.0-20181112160850-82451a803681 h1:JS2rl38kZmHgWa0xINSaSYH0Whtvem64/4+Ef0+Y5pE=
github.com/ScaleFT/sshkeys v0.0.0-20181112160850-82451a803681/go.mod h1:WfDateMPQ/55dPbZRp5Zxrux5WiEaHsjk9puUhz0KgY= github.com/ScaleFT/sshkeys v0.0.0-20181112160850-82451a803681/go.mod h1:WfDateMPQ/55dPbZRp5Zxrux5WiEaHsjk9puUhz0KgY=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
@@ -43,8 +43,8 @@ github.com/aws/aws-sdk-go v1.44.82 h1:Miji7nHIMxTWfa831nZf8XAcMWGLaT+PvsS6CdbMG7
github.com/aws/aws-sdk-go v1.44.82/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.44.82/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -52,14 +52,14 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE=
github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4=
github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= github.com/containerd/cgroups/v3 v3.1.0 h1:azxYVj+91ZgSnIBp2eI3k9y2iYQSR/ZQIgh9vKO+HSY=
github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/cgroups/v3 v3.1.0/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc= github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=
github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/containerd/containerd/api v1.9.0 h1:HZ/licowTRazus+wt9fM6r/9BQO7S0vD5lMcWspGIg0= github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o=
github.com/containerd/containerd/api v1.9.0/go.mod h1:GhghKFmTR3hNtyznBoQ0EMWr9ju5AqHjcZPsSpTKutI= github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM=
github.com/containerd/containerd/v2 v2.1.3 h1:eMD2SLcIQPdMlnlNF6fatlrlRLAeDaiGPGwmRKLZKNs= github.com/containerd/containerd/v2 v2.2.0 h1:K7TqcXy+LnFmZaui2DgHsnp2gAHhVNWYaHlx7HXfys8=
github.com/containerd/containerd/v2 v2.1.3/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM= github.com/containerd/containerd/v2 v2.2.0/go.mod h1:YCMjKjA4ZA7egdHNi3/93bJR1+2oniYlnS+c0N62HdE=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -70,14 +70,14 @@ github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/nydus-snapshotter v0.15.2 h1:qsHI4M+Wwrf6Jr4eBqhNx8qh+YU0dSiJ+WPmcLFWNcg= github.com/containerd/nydus-snapshotter v0.15.4 h1:l59kGRVMtwMLDLh322HsWhEsBCkRKMkGWYV5vBeLYCE=
github.com/containerd/nydus-snapshotter v0.15.2/go.mod h1:FfwH2KBkNYoisK/e+KsmNr7xTU53DmnavQHMFOcXwfM= github.com/containerd/nydus-snapshotter v0.15.4/go.mod h1:eRJqnxQDr48HNop15kZdLZpFF5B6vf6Q11Aq1K0E4Ms=
github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4=
github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y=
github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8=
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= github.com/containerd/stargz-snapshotter/estargz v0.17.0 h1:+TyQIsR/zSFI1Rm31EQBwpAA1ovYgIKHy7kctL3sLcE=
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= github.com/containerd/stargz-snapshotter/estargz v0.17.0/go.mod h1:s06tWAiJcXQo9/8AReBCIo/QxcXFZ2n4qfsRnpl71SM=
github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ=
github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
@@ -98,8 +98,8 @@ github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY=
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
github.com/docker/buildx v0.21.1 h1:YjV2k6CsSDbkDTOMsjARUIrj2xv+zZR+M2dtrRyzXhg= github.com/docker/buildx v0.21.1 h1:YjV2k6CsSDbkDTOMsjARUIrj2xv+zZR+M2dtrRyzXhg=
github.com/docker/buildx v0.21.1/go.mod h1:8V4UMnlKsaGYwz83BygmIbJIFEAYGHT6KAv8akDZmqo= github.com/docker/buildx v0.21.1/go.mod h1:8V4UMnlKsaGYwz83BygmIbJIFEAYGHT6KAv8akDZmqo=
github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A= github.com/docker/cli v28.5.0+incompatible h1:crVqLrtKsrhC9c00ythRx435H8LiQnUKRtJLRR+Auxk=
github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v28.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw= github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
@@ -125,12 +125,12 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE= github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE=
github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
@@ -139,8 +139,8 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -184,17 +184,12 @@ github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25d
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU= github.com/in-toto/in-toto-golang v0.9.0 h1:tHny7ac4KgtsfrG6ybU8gVOZux2H8jN05AXJ9EBM1XU=
@@ -209,8 +204,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -229,8 +224,8 @@ github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b h1:9+ke9YJ9KGWw5AN
github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/moby/buildkit v0.23.1 h1:CZtFmPRF+IFG1C8QfPnktGO1Dzzt5JSwtQ5eDqIh+ag= github.com/moby/buildkit v0.26.3 h1:D+ruZVAk/3ipRq5XRxBH9/DIFpRjSlTtMbghT5gQP9g=
github.com/moby/buildkit v0.23.1/go.mod h1:keNXljNmKX1T0AtM0bMObc8OV6mA9cOuquVbPcRpU/Y= github.com/moby/buildkit v0.26.3/go.mod h1:4T4wJzQS4kYWIfFRjsbJry4QoxDBjK+UGOEOs1izL7w=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/hyperkit v0.0.0-20180416161519-d65b09c1c28a h1:hExo7kltIidoitYnXsnqfvkJXG3YEMbHuQbf9nOMvow= github.com/moby/hyperkit v0.0.0-20180416161519-d65b09c1c28a h1:hExo7kltIidoitYnXsnqfvkJXG3YEMbHuQbf9nOMvow=
@@ -241,8 +236,8 @@ github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkV
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/capability v0.3.0 h1:kEP+y6te0gEXIaeQhIi0s7vKs/w0RPoH1qPa6jROcVg= github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
github.com/moby/sys/capability v0.3.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
@@ -281,15 +276,15 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgm
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/radu-matei/azure-sdk-for-go v5.0.0-beta.0.20161118192335-3b1282355199+incompatible h1:lh3lhjZXXLv7/8cJKQQtgWLJnszuN1xYD9QHTf/dKgE= github.com/radu-matei/azure-sdk-for-go v5.0.0-beta.0.20161118192335-3b1282355199+incompatible h1:lh3lhjZXXLv7/8cJKQQtgWLJnszuN1xYD9QHTf/dKgE=
github.com/radu-matei/azure-sdk-for-go v5.0.0-beta.0.20161118192335-3b1282355199+incompatible/go.mod h1:dK8sDtj2CPkz+pRG4nNF7+WzmyYjtJwfYrZApxB+zFg= github.com/radu-matei/azure-sdk-for-go v5.0.0-beta.0.20161118192335-3b1282355199+incompatible/go.mod h1:dK8sDtj2CPkz+pRG4nNF7+WzmyYjtJwfYrZApxB+zFg=
github.com/radu-matei/azure-vhd-utils v0.0.0-20170531165126-e52754d5569d h1:9aQ38JpJEOfipvLKM/NZ2MWxpZIlZgR+6AjotW+a2T8= github.com/radu-matei/azure-vhd-utils v0.0.0-20170531165126-e52754d5569d h1:9aQ38JpJEOfipvLKM/NZ2MWxpZIlZgR+6AjotW+a2T8=
@@ -298,13 +293,13 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rn/iso9660wrap v0.0.0-20171120145750-baf8d62ad315 h1:DjbO/+j3556fy07xoEM/MyLYN3WwwYyt4dHRC5U+KN8= github.com/rn/iso9660wrap v0.0.0-20171120145750-baf8d62ad315 h1:DjbO/+j3556fy07xoEM/MyLYN3WwwYyt4dHRC5U+KN8=
github.com/rn/iso9660wrap v0.0.0-20171120145750-baf8d62ad315/go.mod h1:qrZfINtl+sTGgS3elQWqWsD2Ke4Il5jDzBr2Q+lzuuE= github.com/rn/iso9660wrap v0.0.0-20171120145750-baf8d62ad315/go.mod h1:qrZfINtl+sTGgS3elQWqWsD2Ke4Il5jDzBr2Q+lzuuE=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6 h1:C1/pvkxkGN/H03mDxLzItaceYJDBk1HdClgR15suAzI= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6 h1:C1/pvkxkGN/H03mDxLzItaceYJDBk1HdClgR15suAzI=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA= github.com/secure-systems-lab/go-securesystemslib v0.9.1 h1:nZZaNz4DiERIQguNy0cL5qTdn9lR8XKHf4RUyG1Sx3g=
github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk= github.com/secure-systems-lab/go-securesystemslib v0.9.1/go.mod h1:np53YzT0zXGMv6x4iEWc9Z59uR+x+ndLwCLqPYpLXVU=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@@ -331,8 +326,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/surma/gocpio v1.0.2-0.20160926205914-fcb68777e7dc h1:iA3Eg1OVd2o0M4M+0PBsBBssMz98L8CUH7x0xVkuyUA= github.com/surma/gocpio v1.0.2-0.20160926205914-fcb68777e7dc h1:iA3Eg1OVd2o0M4M+0PBsBBssMz98L8CUH7x0xVkuyUA=
github.com/surma/gocpio v1.0.2-0.20160926205914-fcb68777e7dc/go.mod h1:zaLNaN+EDnfSnNdWPJJf9OZxWF817w5dt8JNzF9LCVI= github.com/surma/gocpio v1.0.2-0.20160926205914-fcb68777e7dc/go.mod h1:zaLNaN+EDnfSnNdWPJJf9OZxWF817w5dt8JNzF9LCVI=
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
@@ -347,8 +342,8 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc=
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
github.com/vmware/govmomi v0.20.3 h1:gpw/0Ku+6RgF3jsi7fnCLmlcikBHfKBCUcu1qgc16OU= github.com/vmware/govmomi v0.20.3 h1:gpw/0Ku+6RgF3jsi7fnCLmlcikBHfKBCUcu1qgc16OU=
github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -362,52 +357,54 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 h1:4BZHA+B1wXEQoGNHxW8mURaLhcdGwvRnmhGbm+odRbc= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0 h1:lREC4C0ilyP4WibDhQ7Gg2ygAQFP8oR07Fst/5cafwI=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0/go.mod h1:3qi2EEwMgB4xnKgPLqsDP3j9qxnHDZeHsnAxfjQqTko= go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.61.0/go.mod h1:HfvuU0kW9HewH14VCOLImqKvUgONodURG7Alj/IrnGI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6ei8GFW7kyPYdxJaV2rgI6M+4tvZzhYsQ2wgyVC08= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -420,18 +417,18 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-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.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -445,20 +442,20 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -471,6 +468,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY=
google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -478,17 +477,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -498,8 +497,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@@ -2,6 +2,8 @@ package main
import ( import (
"errors" "errors"
"fmt"
"os"
"github.com/linuxkit/linuxkit/src/cmd/linuxkit/pkglib" "github.com/linuxkit/linuxkit/src/cmd/linuxkit/pkglib"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -74,6 +76,8 @@ func pkgCmd() *cobra.Command {
} }
if cmd.Flags().Changed("org") { if cmd.Flags().Changed("org") {
pkglibConfig.Org = &argOrg pkglibConfig.Org = &argOrg
} else if org := os.Getenv(envVarPkgOrg); org != "" {
pkglibConfig.Org = &org
} }
if cmd.Flags().Changed("tag") { if cmd.Flags().Changed("tag") {
pkglibConfig.Tag = tag pkglibConfig.Tag = tag
@@ -100,7 +104,7 @@ func pkgCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar(&argNoNetwork, "nonetwork", !piBase.Network, "Disallow network use during build") cmd.PersistentFlags().BoolVar(&argNoNetwork, "nonetwork", !piBase.Network, "Disallow network use during build")
cmd.PersistentFlags().BoolVar(&argNetwork, "network", piBase.Network, "Allow network use during build") cmd.PersistentFlags().BoolVar(&argNetwork, "network", piBase.Network, "Allow network use during build")
cmd.PersistentFlags().StringVar(&argOrg, "org", piBase.Org, "Override the hub org") cmd.PersistentFlags().StringVar(&argOrg, "org", piBase.Org, fmt.Sprintf("Override the hub org. Also read from env var %s; CLI flag takes precedence.", envVarPkgOrg))
cmd.PersistentFlags().StringVar(&buildYML, "build-yml", defaultPkgBuildYML, "Override the name of the yml file") cmd.PersistentFlags().StringVar(&buildYML, "build-yml", defaultPkgBuildYML, "Override the name of the yml file")
cmd.PersistentFlags().StringVar(&hash, "hash", "", "Override the image hash (default is to query git for the package's tree-sh)") cmd.PersistentFlags().StringVar(&hash, "hash", "", "Override the image hash (default is to query git for the package's tree-sh)")
cmd.PersistentFlags().StringVar(&tag, "tag", piBase.Tag, "Override the tag using fixed strings and/or text templates. Acceptable are .Hash for the hash") cmd.PersistentFlags().StringVar(&tag, "tag", piBase.Tag, "Override the tag using fixed strings and/or text templates. Acceptable are .Hash for the hash")

View File

@@ -16,7 +16,12 @@ import (
const ( const (
buildersEnvVar = "LINUXKIT_BUILDERS" buildersEnvVar = "LINUXKIT_BUILDERS"
envVarCacheDir = "LINUXKIT_CACHE" envVarCacheDir = "LINUXKIT_CACHE"
defaultBuilderImage = "moby/buildkit:v0.23.1" envVarBuilderName = "LINUXKIT_BUILDER_NAME"
envVarBuilderImage = "LINUXKIT_BUILDER_IMAGE"
envVarBuilderConfig = "LINUXKIT_BUILDER_CONFIG"
envVarMirror = "LINUXKIT_MIRROR"
envVarPkgOrg = "LINUXKIT_PKG_ORG"
defaultBuilderImage = "moby/buildkit:v0.26.3"
) )
// some logic clarification: // some logic clarification:
@@ -40,8 +45,9 @@ func pkgBuildCmd() *cobra.Command {
platforms string platforms string
skipPlatforms string skipPlatforms string
builders string builders string
builderImage string builderName = flagOverEnvVarOverDefaultString{def: pkglib.DefaultBuilderName(), envVar: envVarBuilderName}
builderConfig string builderImage = flagOverEnvVarOverDefaultString{def: defaultBuilderImage, envVar: envVarBuilderImage}
builderConfig = flagOverEnvVarOverDefaultString{def: "", envVar: envVarBuilderConfig}
builderRestart bool builderRestart bool
preCacheImages bool preCacheImages bool
release string release string
@@ -191,16 +197,19 @@ func pkgBuildCmd() *cobra.Command {
if err != nil { if err != nil {
return fmt.Errorf("error in --builders flag: %w", err) return fmt.Errorf("error in --builders flag: %w", err)
} }
if builderConfig != "" { if builderConfig.String() != "" {
if _, err := os.Stat(builderConfig); err != nil { if _, err := os.Stat(builderConfig.String()); err != nil {
return fmt.Errorf("error reading builder config file %s: %w", builderConfig, err) return fmt.Errorf("error reading builder config file %s: %w", builderConfig.String(), err)
} }
opts = append(opts, pkglib.WithBuildBuilderConfig(builderConfig))
} }
opts = append(opts, pkglib.WithBuildBuilders(buildersMap)) opts = append(opts, pkglib.WithBuildBuilders(buildersMap))
opts = append(opts, pkglib.WithBuildBuilderImage(builderImage)) opts = append(opts, pkglib.WithBuildBuilderConfig(pkglib.BuilderConfig{
opts = append(opts, pkglib.WithBuildBuilderRestart(builderRestart)) Name: builderName.String(),
Image: builderImage.String(),
ConfigPath: builderConfig.String(),
Restart: builderRestart,
}))
opts = append(opts, pkglib.WithProgress(progress)) opts = append(opts, pkglib.WithProgress(progress))
if len(ssh) > 0 { if len(ssh) > 0 {
opts = append(opts, pkglib.WithSSH(ssh)) opts = append(opts, pkglib.WithSSH(ssh))
@@ -303,8 +312,9 @@ func pkgBuildCmd() *cobra.Command {
cmd.Flags().StringVar(&platforms, "platforms", "", "Which platforms to build for, defaults to all of those for which the package can be built") cmd.Flags().StringVar(&platforms, "platforms", "", "Which platforms to build for, defaults to all of those for which the package can be built")
cmd.Flags().StringVar(&skipPlatforms, "skip-platforms", "", "Platforms that should be skipped, even if present in build.yml") cmd.Flags().StringVar(&skipPlatforms, "skip-platforms", "", "Platforms that should be skipped, even if present in build.yml")
cmd.Flags().StringVar(&builders, "builders", "", "Which builders to use for which platforms, e.g. linux/arm64=docker-context-arm64, overrides defaults and environment variables, see https://github.com/linuxkit/linuxkit/blob/master/docs/packages.md#Providing-native-builder-nodes") cmd.Flags().StringVar(&builders, "builders", "", "Which builders to use for which platforms, e.g. linux/arm64=docker-context-arm64, overrides defaults and environment variables, see https://github.com/linuxkit/linuxkit/blob/master/docs/packages.md#Providing-native-builder-nodes")
cmd.Flags().StringVar(&builderImage, "builder-image", defaultBuilderImage, "buildkit builder container image to use") cmd.Flags().Var(&builderName, "builder-name", fmt.Sprintf("Name of the buildkit builder container, default: %s, overrides env var %s", pkglib.DefaultBuilderName(), envVarBuilderName))
cmd.Flags().StringVar(&builderConfig, "builder-config", "", "path to buildkit builder config.toml file to use, overrides the default config.toml in the builder image. When provided, copied over into builder, along with all certs. Use paths for certificates relative to your local host, they will be adjusted on copying into the container. USE WITH CAUTION") cmd.Flags().Var(&builderImage, "builder-image", fmt.Sprintf("buildkit builder container image to use, overrides env var %s", envVarBuilderImage))
cmd.Flags().Var(&builderConfig, "builder-config", fmt.Sprintf("path to buildkit builder config.toml file to use, overrides the default config.toml in the builder image. When provided, copied over into builder, along with all certs. Use paths for certificates relative to your local host, they will be adjusted on copying into the container. USE WITH CAUTION. Overrides env var %s", envVarBuilderConfig))
cmd.Flags().BoolVar(&builderRestart, "builder-restart", false, "force restarting builder, even if container with correct name and image exists") cmd.Flags().BoolVar(&builderRestart, "builder-restart", false, "force restarting builder, even if container with correct name and image exists")
cmd.Flags().BoolVar(&preCacheImages, "precache-images", false, "download all referenced images in the Dockerfile to the linuxkit cache before building, thus referencing the local cache instead of pulling from the registry; this is useful for handling mirrors and special connections") cmd.Flags().BoolVar(&preCacheImages, "precache-images", false, "download all referenced images in the Dockerfile to the linuxkit cache before building, thus referencing the local cache instead of pulling from the registry; this is useful for handling mirrors and special connections")
cmd.Flags().Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir)) cmd.Flags().Var(&cacheDir, "cache", fmt.Sprintf("Directory for caching and finding cached image, overrides env var %s", envVarCacheDir))

View File

@@ -13,6 +13,7 @@ import (
func pkgBuilderCmd() *cobra.Command { func pkgBuilderCmd() *cobra.Command {
var ( var (
builders string builders string
builderName = flagOverEnvVarOverDefaultString{def: pkglib.DefaultBuilderName(), envVar: envVarBuilderName}
platforms string platforms string
builderImage string builderImage string
builderConfigPath string builderConfigPath string
@@ -39,13 +40,18 @@ func pkgBuilderCmd() *cobra.Command {
} }
platformsToClean := strings.Split(platforms, ",") platformsToClean := strings.Split(platforms, ",")
bc := pkglib.BuilderConfig{
Name: builderName.String(),
Image: builderImage,
ConfigPath: builderConfigPath,
}
switch command { switch command {
case "du": case "du":
if err := pkglib.DiskUsage(buildersMap, builderImage, builderConfigPath, platformsToClean, verbose); err != nil { if err := pkglib.DiskUsage(buildersMap, bc, platformsToClean, verbose); err != nil {
return fmt.Errorf("unable to print disk usage of builder: %w", err) return fmt.Errorf("unable to print disk usage of builder: %w", err)
} }
case "prune": case "prune":
if err := pkglib.PruneBuilder(buildersMap, builderImage, builderConfigPath, platformsToClean, verbose); err != nil { if err := pkglib.PruneBuilder(buildersMap, bc, platformsToClean, verbose); err != nil {
return fmt.Errorf("unable to prune builder: %w", err) return fmt.Errorf("unable to prune builder: %w", err)
} }
default: default:
@@ -56,6 +62,7 @@ func pkgBuilderCmd() *cobra.Command {
} }
cmd.PersistentFlags().StringVar(&builders, "builders", "", "Which builders to use for which platforms, e.g. linux/arm64=docker-context-arm64, overrides defaults and environment variables, see https://github.com/linuxkit/linuxkit/blob/master/docs/packages.md#Providing-native-builder-nodes") cmd.PersistentFlags().StringVar(&builders, "builders", "", "Which builders to use for which platforms, e.g. linux/arm64=docker-context-arm64, overrides defaults and environment variables, see https://github.com/linuxkit/linuxkit/blob/master/docs/packages.md#Providing-native-builder-nodes")
cmd.PersistentFlags().Var(&builderName, "builder-name", fmt.Sprintf("Name of the buildkit builder container, default: %s, overrides env var %s", pkglib.DefaultBuilderName(), envVarBuilderName))
cmd.PersistentFlags().StringVar(&platforms, "platforms", fmt.Sprintf("linux/%s", runtime.GOARCH), "Which platforms we built images for") cmd.PersistentFlags().StringVar(&platforms, "platforms", fmt.Sprintf("linux/%s", runtime.GOARCH), "Which platforms we built images for")
cmd.PersistentFlags().StringVar(&builderImage, "builder-image", defaultBuilderImage, "buildkit builder container image to use") cmd.PersistentFlags().StringVar(&builderImage, "builder-image", defaultBuilderImage, "buildkit builder container image to use")
cmd.Flags().StringVar(&builderConfigPath, "builder-config", "", "path to buildkit builder config.toml file to use, overrides the default config.toml in the builder image; USE WITH CAUTION") cmd.Flags().StringVar(&builderConfigPath, "builder-config", "", "path to buildkit builder config.toml file to use, overrides the default config.toml in the builder image; USE WITH CAUTION")

View File

@@ -24,32 +24,30 @@ import (
) )
type buildOpts struct { type buildOpts struct {
skipBuild bool skipBuild bool
force bool force bool
pull bool pull bool
ignoreCache bool ignoreCache bool
push bool push bool
dryRun bool dryRun bool
preCacheImages bool preCacheImages bool
release string release string
manifest bool manifest bool
targetDocker bool targetDocker bool
cacheDir string cacheDir string
cacheProvider spec.CacheProvider cacheProvider spec.CacheProvider
platforms []imagespec.Platform platforms []imagespec.Platform
builders map[string]string builders map[string]string
runner DockerRunner runner DockerRunner
writer io.Writer writer io.Writer
builderImage string builderConfig BuilderConfig
builderConfigPath string sbomScan bool
builderRestart bool sbomScannerImage string
sbomScan bool dockerfile string
sbomScannerImage string buildArgs []string
dockerfile string progress string
buildArgs []string ssh []string
progress string registryAuth map[string]spec.RegistryAuth
ssh []string
registryAuth map[string]spec.RegistryAuth
} }
// BuildOpt allows callers to specify options to Build // BuildOpt allows callers to specify options to Build
@@ -160,26 +158,10 @@ func WithBuildOutputWriter(w io.Writer) BuildOpt {
} }
} }
// WithBuildBuilderImage set the builder container image to use. // WithBuildBuilderConfig set the builder configuration to use.
func WithBuildBuilderImage(image string) BuildOpt { func WithBuildBuilderConfig(bc BuilderConfig) BuildOpt {
return func(bo *buildOpts) error { return func(bo *buildOpts) error {
bo.builderImage = image bo.builderConfig = bc
return nil
}
}
// WithBuildBuilderConfig set the contents of the
func WithBuildBuilderConfig(builderConfigPath string) BuildOpt {
return func(bo *buildOpts) error {
bo.builderConfigPath = builderConfigPath
return nil
}
}
// WithBuildBuilderRestart restart the builder container even if it already is running with the correct image version
func WithBuildBuilderRestart(restart bool) BuildOpt {
return func(bo *buildOpts) error {
bo.builderRestart = restart
return nil return nil
} }
} }
@@ -322,7 +304,7 @@ func (p Pkg) Build(bos ...BuildOpt) error {
case d == nil && bo.dryRun: case d == nil && bo.dryRun:
d = NewDockerDryRunner() d = NewDockerDryRunner()
case d == nil: case d == nil:
d = NewDockerRunner(p.cache) d = NewDockerRunner(p.cache, bo.builderConfig)
} }
c := bo.cacheProvider c := bo.cacheProvider
@@ -492,7 +474,7 @@ func (p Pkg) Build(bos ...BuildOpt) error {
// build for each arch and save in the linuxkit cache // build for each arch and save in the linuxkit cache
for _, platform := range platformsToBuild { for _, platform := range platformsToBuild {
builtDescs, err := p.buildArch(ctx, d, c, bo.builderImage, bo.builderConfigPath, platform.Architecture, bo.builderRestart, writer, bo, imageBuildOpts) builtDescs, err := p.buildArch(ctx, d, c, platform.Architecture, writer, bo, imageBuildOpts)
if err != nil { if err != nil {
return fmt.Errorf("error building for arch %s: %v", platform.Architecture, err) return fmt.Errorf("error building for arch %s: %v", platform.Architecture, err)
} }
@@ -527,7 +509,7 @@ func (p Pkg) Build(bos ...BuildOpt) error {
// if requested docker, load the image up // if requested docker, load the image up
// we will store images with arch suffix, i.e. -amd64 // we will store images with arch suffix, i.e. -amd64
// if one of the arch equals with system, we will add tag without suffix // if one of the arch equals with system, we will add tag without suffix
if bo.targetDocker { if bo.targetDocker && desc != nil {
for _, platform := range bo.platforms { for _, platform := range bo.platforms {
ref, err := reference.Parse(p.FullTag()) ref, err := reference.Parse(p.FullTag())
if err != nil { if err != nil {
@@ -555,18 +537,11 @@ func (p Pkg) Build(bos ...BuildOpt) error {
return nil return nil
} }
// we only will push if one of these is true: // determine if we should skip pushing the default tag (no build and no local cache)
// - we had at least one platform to build skipDefaultPush := false
// - we found an image in local cache
// if neither is true, there is nothing to push
if len(platformsToBuild) == 0 { if len(platformsToBuild) == 0 {
// if we did not yet find the image in local cache, // if we did not yet find the image in local cache, recheck
// check, in case we have it and would need to push.
// If we did not build it because we were not requested to do so,
// then we might not know we have it in local cache.
if !imageInLocalCache { if !imageInLocalCache {
// we need this to know whether or not we might push
for _, platform := range bo.platforms { for _, platform := range bo.platforms {
exists, err := c.ImageInCache(&ref, "", platform.Architecture) exists, err := c.ImageInCache(&ref, "", platform.Architecture)
if err == nil && exists { if err == nil && exists {
@@ -577,17 +552,22 @@ func (p Pkg) Build(bos ...BuildOpt) error {
} }
if !imageInLocalCache { if !imageInLocalCache {
_, _ = fmt.Fprintf(writer, "No new platforms to push, skipping.\n") _, _ = fmt.Fprintf(writer, "No new platforms to push, skipping.\n")
return nil if bo.release == "" {
return nil
}
skipDefaultPush = true
} }
} }
if p.dirty { if p.dirty {
return fmt.Errorf("build complete, refusing to push dirty package") return fmt.Errorf("build complete, refusing to push dirty package")
} }
// push the manifest for the default tag if needed
// push the manifest if !skipDefaultPush {
if err := c.Push(p.FullTag(), "", bo.manifest, true); err != nil { if err := c.Push(p.FullTag(), "", bo.manifest, true); err != nil {
return err return err
}
} else {
_, _ = fmt.Fprintf(writer, "Skipping push of default tag %q\n", p.FullTag())
} }
if bo.release == "" { if bo.release == "" {
@@ -595,6 +575,21 @@ func (p Pkg) Build(bos ...BuildOpt) error {
return nil return nil
} }
if desc == nil {
// Image exists in registry but not in local cache. Pull it so we can create the release tag.
_, _ = fmt.Fprintf(writer, "Pulling %s into local cache for release...\n", p.FullTag())
if err := c.ImagePull(&ref, bo.platforms, false); err != nil {
return fmt.Errorf("unable to pull image for release: %v", err)
}
desc, err = c.FindDescriptor(&ref)
if err != nil {
return err
}
if desc == nil {
return fmt.Errorf("unable to find image descriptor in local cache for %s after pull, cannot release", p.FullTag())
}
}
relTag, err := p.ReleaseTag(bo.release) relTag, err := p.ReleaseTag(bo.release)
if err != nil { if err != nil {
return err return err
@@ -608,7 +603,8 @@ func (p Pkg) Build(bos ...BuildOpt) error {
if err := c.DescriptorWrite(fullRelTag, *desc); err != nil { if err := c.DescriptorWrite(fullRelTag, *desc); err != nil {
return err return err
} }
if err := c.Push(fullRelTag, "", bo.manifest, true); err != nil { // push the release tag without overriding existing (skip if already present)
if err := c.Push(fullRelTag, "", bo.manifest, false); err != nil {
return err return err
} }
@@ -646,7 +642,7 @@ func (p Pkg) Build(bos ...BuildOpt) error {
// C - manifest, saved in cache as is, referenced by the index (E), and returned as a descriptor // C - manifest, saved in cache as is, referenced by the index (E), and returned as a descriptor
// D - attestations (if any), saved in cache as is, referenced by the index (E), and returned as a descriptor // D - attestations (if any), saved in cache as is, referenced by the index (E), and returned as a descriptor
// E - index, saved in cache as is, stored in cache as tag "image:tag-arch", *not* returned as a descriptor // E - index, saved in cache as is, stored in cache as tag "image:tag-arch", *not* returned as a descriptor
func (p Pkg) buildArch(ctx context.Context, d DockerRunner, c spec.CacheProvider, builderImage, builderConfigPath, arch string, restart bool, writer io.Writer, bo buildOpts, imageBuildOpts spec.ImageBuildOptions) ([]registry.Descriptor, error) { func (p Pkg) buildArch(ctx context.Context, d DockerRunner, c spec.CacheProvider, arch string, writer io.Writer, bo buildOpts, imageBuildOpts spec.ImageBuildOptions) ([]registry.Descriptor, error) {
var ( var (
tagArch string tagArch string
tag = p.FullTag() tag = p.FullTag()
@@ -675,8 +671,8 @@ func (p Pkg) buildArch(ctx context.Context, d DockerRunner, c spec.CacheProvider
return nil, err return nil, err
} }
// find the desired builder // find the desired docker context for the builder
builderName := getBuilderForPlatform(arch, bo.builders) dockerContext := getBuilderForPlatform(arch, bo.builders)
// set the target // set the target
var ( var (
@@ -715,7 +711,7 @@ func (p Pkg) buildArch(ctx context.Context, d DockerRunner, c spec.CacheProvider
imageBuildOpts.Dockerfile = bo.dockerfile imageBuildOpts.Dockerfile = bo.dockerfile
if err := d.Build(ctx, tagArch, p.path, builderName, builderImage, builderConfigPath, platform, restart, bo.preCacheImages, passCache, buildCtx.Reader(), stdout, bo.sbomScan, bo.sbomScannerImage, bo.progress, imageBuildOpts); err != nil { if err := d.Build(ctx, tagArch, p.path, dockerContext, platform, bo.preCacheImages, passCache, buildCtx.Reader(), stdout, bo.sbomScan, bo.sbomScannerImage, bo.progress, imageBuildOpts); err != nil {
stdoutCloser() stdoutCloser()
if strings.Contains(err.Error(), "executor failed running [/dev/.buildkit_qemu_emulator") { if strings.Contains(err.Error(), "executor failed running [/dev/.buildkit_qemu_emulator") {
return nil, fmt.Errorf("buildkit was unable to emulate %s. check binfmt has been set up and works for this platform: %v", platform, err) return nil, fmt.Errorf("buildkit was unable to emulate %s. check binfmt has been set up and works for this platform: %v", platform, err)

View File

@@ -53,10 +53,10 @@ func (d *dockerMocker) ContextSupportCheck() error {
} }
return errors.New("contexts not supported") return errors.New("contexts not supported")
} }
func (d *dockerMocker) Builder(_ context.Context, _, _, _, _ string, _ bool) (*buildkitClient.Client, error) { func (d *dockerMocker) Builder(_ context.Context, _, _ string) (*buildkitClient.Client, error) {
return nil, fmt.Errorf("not implemented") return nil, fmt.Errorf("not implemented")
} }
func (d *dockerMocker) Build(ctx context.Context, tag, pkg, dockerContext, builderImage, builderConfigPath, platform string, builderRestart, preCacheImages bool, c spec.CacheProvider, r io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, progress string, imageBuildOpts spec.ImageBuildOptions) error { func (d *dockerMocker) Build(ctx context.Context, tag, pkg, dockerContext, platform string, preCacheImages bool, c spec.CacheProvider, r io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, progress string, imageBuildOpts spec.ImageBuildOptions) error {
if !d.enableBuild { if !d.enableBuild {
return errors.New("build disabled") return errors.New("build disabled")
} }
@@ -240,13 +240,24 @@ type cacheMocker struct {
enableIndexWrite bool enableIndexWrite bool
images map[string][]v1.Descriptor images map[string][]v1.Descriptor
hashes map[string][]byte hashes map[string][]byte
registryImages map[string][]v1.Descriptor
} }
func (c *cacheMocker) ImagePull(ref *reference.Spec, platforms []imagespec.Platform, alwaysPull bool) error { func (c *cacheMocker) ImagePull(ref *reference.Spec, platforms []imagespec.Platform, alwaysPull bool) error {
if !c.enableImagePull { if !c.enableImagePull {
return errors.New("ImagePull disabled") return errors.New("ImagePull disabled")
} }
// make some random data for a layer image := ref.String()
// simulate pulling from registry: copy registry descriptors into local cache
if c.registryImages != nil {
if descs, ok := c.registryImages[image]; ok {
for _, d := range descs {
c.appendImage(image, d)
}
return nil
}
}
// fallback: make some random data for a layer
b := make([]byte, 256) b := make([]byte, 256)
_, _ = rand.Read(b) _, _ = rand.Read(b)
descs, err := c.imageWriteStream(bytes.NewReader(b)) descs, err := c.imageWriteStream(bytes.NewReader(b))
@@ -277,6 +288,19 @@ func (c *cacheMocker) ImageInCache(ref *reference.Spec, trustedRef, architecture
} }
func (c *cacheMocker) ImageInRegistry(ref *reference.Spec, trustedRef, architecture string) (bool, error) { func (c *cacheMocker) ImageInRegistry(ref *reference.Spec, trustedRef, architecture string) (bool, error) {
if c.registryImages == nil {
return false, nil
}
image := ref.String()
descs, ok := c.registryImages[image]
if !ok {
return false, nil
}
for _, d := range descs {
if d.Platform != nil && d.Platform.Architecture == architecture {
return true, nil
}
}
return false, nil return false, nil
} }
@@ -445,7 +469,7 @@ func (c *cacheMocker) FindDescriptor(ref *reference.Spec) (*v1.Descriptor, error
if desc, ok := c.images[name]; ok && len(desc) > 0 { if desc, ok := c.images[name]; ok && len(desc) > 0 {
return &desc[0], nil return &desc[0], nil
} }
return nil, fmt.Errorf("not found %s", name) return nil, nil
} }
func (c *cacheMocker) NewSource(ref *reference.Spec, platform *imagespec.Platform, descriptor *v1.Descriptor) spec.ImageSource { func (c *cacheMocker) NewSource(ref *reference.Spec, platform *imagespec.Platform, descriptor *v1.Descriptor) spec.ImageSource {
return cacheMockerSource{c, ref, platform, descriptor} return cacheMockerSource{c, ref, platform, descriptor}
@@ -581,6 +605,52 @@ func TestBuild(t *testing.T) {
} }
} }
// TestPushReleaseImageInRegistry tests that push+release works when the image
// already exists in the registry but not in the local cache. This is a
// regression test for a nil pointer dereference introduced in 4129cc79.
func TestPushReleaseImageInRegistry(t *testing.T) {
cacheDir := "somecachedir"
// simulate an image that exists in the registry for amd64
// FullTag() for org:"foo", image:"bar" with no tag set is "docker.io/foo/bar:latest"
registryDescs := map[string][]v1.Descriptor{
"docker.io/foo/bar:latest": {
{
MediaType: types.OCIManifestSchema1,
Size: 100,
Digest: v1.Hash{Algorithm: "sha256", Hex: "aabbccdd"},
Platform: &v1.Platform{Architecture: "amd64", OS: "linux"},
},
},
}
c := &cacheMocker{
enablePush: true,
enabledDescriptorWrite: true,
enableImagePull: true,
enableIndexWrite: true,
registryImages: registryDescs,
}
runner := &dockerMocker{supportContexts: true}
p := Pkg{org: "foo", image: "bar", hash: "abc", arches: []string{"amd64"}, commitHash: "HEAD", dockerfile: "testdata/Dockerfile"}
opts := []BuildOpt{
WithBuildCacheDir(cacheDir),
WithBuildPush(),
WithRelease("snapshot"),
WithBuildDocker(runner),
WithBuildCacheProvider(c),
WithBuildOutputWriter(io.Discard),
WithBuildPlatforms(imagespec.Platform{OS: "linux", Architecture: "amd64"}),
}
err := p.Build(opts...)
if err != nil {
t.Fatalf("expected no error, got: %v", err)
}
// verify the release tag was written to cache
relTag := "docker.io/foo/bar:snapshot"
if _, ok := c.images[relTag]; !ok {
t.Errorf("expected release tag %q to be written to cache", relTag)
}
}
// testCheckBuildRun check the output of a build run // testCheckBuildRun check the output of a build run
func testCheckBuildRun(build buildLog, platforms map[string]bool) error { func testCheckBuildRun(build buildLog, platforms map[string]bool) error {
platform := build.platform platform := build.platform

View File

@@ -18,12 +18,25 @@ import (
_ "github.com/moby/buildkit/client/connhelper/ssh" _ "github.com/moby/buildkit/client/connhelper/ssh"
) )
// BuilderConfig holds configuration for the buildkit builder container.
type BuilderConfig struct {
// Name is the container name for the buildkit builder (e.g., "linuxkit-builder-alice").
Name string
// Image is the container image to run (e.g., "moby/buildkit:v0.26.3").
Image string
// ConfigPath is an optional path to a buildkitd.toml config file.
ConfigPath string
// Restart forces recreation of the builder container even if one with the
// correct name and image already exists.
Restart bool
}
type DockerRunner interface { type DockerRunner interface {
Tag(ref, tag string) error Tag(ref, tag string) error
Build(ctx context.Context, tag, pkg, dockerContext, builderImage, builderConfigPath, platform string, restart, preCacheImages bool, c spec.CacheProvider, r io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, platformType string, imageBuildOpts spec.ImageBuildOptions) error Build(ctx context.Context, tag, pkg, dockerContext, platform string, preCacheImages bool, c spec.CacheProvider, r io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, platformType string, imageBuildOpts spec.ImageBuildOptions) error
Save(tgt string, refs ...string) error Save(tgt string, refs ...string) error
Load(src io.Reader) error Load(src io.Reader) error
Pull(img string) (bool, error) Pull(img string) (bool, error)
ContextSupportCheck() error ContextSupportCheck() error
Builder(ctx context.Context, dockerContext, builderImage, builderConfigPath, platform string, restart bool) (*buildkitClient.Client, error) Builder(ctx context.Context, dockerContext, platform string) (*buildkitClient.Client, error)
} }

View File

@@ -46,7 +46,7 @@ func (dr *dockerDryRunnerImpl) ContextSupportCheck() error {
// 1. if dockerContext is provided, try to create a builder with that context; if it succeeds, we are done; if not, return an error. // 1. if dockerContext is provided, try to create a builder with that context; if it succeeds, we are done; if not, return an error.
// 2. try to find an existing named runner with the pattern; if it succeeds, we are done; if not, try next. // 2. try to find an existing named runner with the pattern; if it succeeds, we are done; if not, try next.
// 3. try to create a generic builder using the default context named "linuxkit". // 3. try to create a generic builder using the default context named "linuxkit".
func (dr *dockerDryRunnerImpl) Builder(ctx context.Context, dockerContext, builderImage, builderConfigPath, platform string, restart bool) (*buildkitClient.Client, error) { func (dr *dockerDryRunnerImpl) Builder(ctx context.Context, dockerContext, platform string) (*buildkitClient.Client, error) {
return nil, nil return nil, nil
} }
@@ -58,7 +58,7 @@ func (dr *dockerDryRunnerImpl) Tag(ref, tag string) error {
return errors.New("not implemented") return errors.New("not implemented")
} }
func (dr *dockerDryRunnerImpl) Build(ctx context.Context, tag, pkg, dockerContext, builderImage, builderConfigPath, platform string, restart, preCacheImages bool, c spec.CacheProvider, stdin io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, progressType string, imageBuildOpts spec.ImageBuildOptions) error { func (dr *dockerDryRunnerImpl) Build(ctx context.Context, tag, pkg, dockerContext, platform string, preCacheImages bool, c spec.CacheProvider, stdin io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, progressType string, imageBuildOpts spec.ImageBuildOptions) error {
// build args // build args
var buildArgs []string var buildArgs []string
for k, v := range imageBuildOpts.BuildArgs { for k, v := range imageBuildOpts.BuildArgs {

View File

@@ -67,12 +67,27 @@ const (
buildkitConfigPath = buildkitConfigDir + "/" + buildkitConfigFileName buildkitConfigPath = buildkitConfigDir + "/" + buildkitConfigFileName
) )
type dockerRunnerImpl struct { // DefaultBuilderName returns the default builder container name.
cache bool func DefaultBuilderName() string {
return buildkitBuilderName
} }
func NewDockerRunner(cache bool) DockerRunner { // builderVolumeName returns the named volume used to persist buildkit state
return &dockerRunnerImpl{cache: cache} // (build cache, snapshots, content store) across container recreations.
func builderVolumeName(containerName string) string {
return containerName + "-state"
}
type dockerRunnerImpl struct {
cache bool
builder BuilderConfig
}
func NewDockerRunner(cache bool, bc BuilderConfig) DockerRunner {
if bc.Name == "" {
bc.Name = DefaultBuilderName()
}
return &dockerRunnerImpl{cache: cache, builder: bc}
} }
func isExecErrNotFound(err error) bool { func isExecErrNotFound(err error) bool {
@@ -219,14 +234,15 @@ func (dr *dockerRunnerImpl) ContextSupportCheck() error {
// 1. if dockerContext is provided, try to create a builder with that context; if it succeeds, we are done; if not, return an error. // 1. if dockerContext is provided, try to create a builder with that context; if it succeeds, we are done; if not, return an error.
// 2. try to find an existing named runner with the pattern; if it succeeds, we are done; if not, try next. // 2. try to find an existing named runner with the pattern; if it succeeds, we are done; if not, try next.
// 3. try to create a generic builder using the default context named "linuxkit". // 3. try to create a generic builder using the default context named "linuxkit".
func (dr *dockerRunnerImpl) Builder(ctx context.Context, dockerContext, builderImage, builderConfigPath, platform string, restart bool) (*buildkitClient.Client, error) { func (dr *dockerRunnerImpl) Builder(ctx context.Context, dockerContext, platform string) (*buildkitClient.Client, error) {
bc := dr.builder
// if we were given a context, we must find a builder and use it, or create one and use it // if we were given a context, we must find a builder and use it, or create one and use it
if dockerContext != "" { if dockerContext != "" {
// does the context exist? // does the context exist?
if err := dr.command(nil, io.Discard, io.Discard, "context", "inspect", dockerContext); err != nil { if err := dr.command(nil, io.Discard, io.Discard, "context", "inspect", dockerContext); err != nil {
return nil, fmt.Errorf("provided docker context '%s' not found", dockerContext) return nil, fmt.Errorf("provided docker context '%s' not found", dockerContext)
} }
client, err := dr.builderEnsureContainer(ctx, buildkitBuilderName, builderImage, builderConfigPath, platform, dockerContext, restart) client, err := dr.builderEnsureContainer(ctx, bc.Name, bc.Image, bc.ConfigPath, platform, dockerContext, bc.Restart)
if err != nil { if err != nil {
return nil, fmt.Errorf("error preparing builder based on context '%s': %v", dockerContext, err) return nil, fmt.Errorf("error preparing builder based on context '%s': %v", dockerContext, err)
} }
@@ -237,13 +253,13 @@ func (dr *dockerRunnerImpl) Builder(ctx context.Context, dockerContext, builderI
dockerContext = fmt.Sprintf("%s-%s", "linuxkit", strings.ReplaceAll(platform, "/", "-")) dockerContext = fmt.Sprintf("%s-%s", "linuxkit", strings.ReplaceAll(platform, "/", "-"))
if err := dr.command(nil, io.Discard, io.Discard, "context", "inspect", dockerContext); err == nil { if err := dr.command(nil, io.Discard, io.Discard, "context", "inspect", dockerContext); err == nil {
// we found an appropriately named context, so let us try to use it or error out // we found an appropriately named context, so let us try to use it or error out
if client, err := dr.builderEnsureContainer(ctx, buildkitBuilderName, builderImage, builderConfigPath, platform, dockerContext, restart); err == nil { if client, err := dr.builderEnsureContainer(ctx, bc.Name, bc.Image, bc.ConfigPath, platform, dockerContext, bc.Restart); err == nil {
return client, nil return client, nil
} }
} }
// create a generic builder // create a generic builder
client, err := dr.builderEnsureContainer(ctx, buildkitBuilderName, builderImage, builderConfigPath, "", "default", restart) client, err := dr.builderEnsureContainer(ctx, bc.Name, bc.Image, bc.ConfigPath, "", "default", bc.Restart)
if err != nil { if err != nil {
return nil, fmt.Errorf("error ensuring builder container in default context: %v", err) return nil, fmt.Errorf("error ensuring builder container in default context: %v", err)
} }
@@ -262,9 +278,10 @@ func (dr *dockerRunnerImpl) builderEnsureContainer(ctx context.Context, name, im
// recreate by default (true) unless we already have one that meets all of the requirements - image, permissions, etc. // recreate by default (true) unless we already have one that meets all of the requirements - image, permissions, etc.
recreate = true recreate = true
// stop existing one // stop existing one
stop = false stop = false
remove = false remove = false
found = false removeVolume = false
found = false
) )
const ( const (
@@ -278,7 +295,16 @@ func (dr *dockerRunnerImpl) builderEnsureContainer(ctx context.Context, name, im
for range buildKitCheckRetryCount { for range buildKitCheckRetryCount {
var b bytes.Buffer var b bytes.Buffer
var cid string var cid string
// load config files up front so they are available both when checking
// an existing container and when creating a brand-new one
var filesToLoadIntoContainer map[string][]byte var filesToLoadIntoContainer map[string][]byte
if configPath != "" {
var err error
filesToLoadIntoContainer, err = confutil.LoadConfigFiles(configPath)
if err != nil {
return nil, fmt.Errorf("failed to load buildkit config file %s: %v", configPath, err)
}
}
if err := dr.command(nil, &b, io.Discard, "--context", dockerContext, "container", "inspect", name); err == nil { if err := dr.command(nil, &b, io.Discard, "--context", dockerContext, "container", "inspect", name); err == nil {
// we already have a container named "linuxkit-builder" in the provided context. // we already have a container named "linuxkit-builder" in the provided context.
// get its state and config // get its state and config
@@ -298,12 +324,6 @@ func (dr *dockerRunnerImpl) builderEnsureContainer(ctx context.Context, name, im
log.Debugf("checking if configPath %s is correct in container %s", configPath, name) log.Debugf("checking if configPath %s is correct in container %s", configPath, name)
configPathCorrect = false configPathCorrect = false
var configB bytes.Buffer var configB bytes.Buffer
// we cannot exactly use the local config file, as it gets modified to get loaded into the container
// so we preprocess it using the same library that would load it up
filesToLoadIntoContainer, err = confutil.LoadConfigFiles(configPath)
if err != nil {
return nil, fmt.Errorf("failed to load buildkit config file %s: %v", configPath, err)
}
if err := dr.command(nil, &configB, io.Discard, "--context", dockerContext, "container", "exec", name, "cat", buildkitConfigPath); err == nil { if err := dr.command(nil, &configB, io.Discard, "--context", dockerContext, "container", "exec", name, "cat", buildkitConfigPath); err == nil {
// sha256sum the config file to see if it matches the provided configPath // sha256sum the config file to see if it matches the provided configPath
containerConfigFileHash := sha256.Sum256(configB.Bytes()) containerConfigFileHash := sha256.Sum256(configB.Bytes())
@@ -335,11 +355,13 @@ func (dr *dockerRunnerImpl) builderEnsureContainer(ctx context.Context, name, im
stop = isRunning stop = isRunning
remove = true remove = true
case existingImage != image: case existingImage != image:
// if image mismatches, recreate // if image mismatches, recreate and remove the state volume since we
// cannot guarantee buildkit state compatibility across versions
fmt.Printf("existing container %s is running image %s instead of target %s, replacing\n", name, existingImage, image) fmt.Printf("existing container %s is running image %s instead of target %s, replacing\n", name, existingImage, image)
recreate = true recreate = true
stop = isRunning stop = isRunning
remove = true remove = true
removeVolume = true
case !containerJSON[0].HostConfig.Privileged: case !containerJSON[0].HostConfig.Privileged:
// if unprivileged, we need to remove it and start a new container with the right permissions // if unprivileged, we need to remove it and start a new container with the right permissions
fmt.Printf("existing container %s is unprivileged, replacing\n", name) fmt.Printf("existing container %s is unprivileged, replacing\n", name)
@@ -394,11 +416,24 @@ func (dr *dockerRunnerImpl) builderEnsureContainer(ctx context.Context, name, im
continue continue
} }
} }
if removeVolume {
volName := builderVolumeName(name)
fmt.Printf("removing builder state volume %s\n", volName)
// best-effort: volume may not exist yet on first run
_ = dr.command(nil, io.Discard, io.Discard, "--context", dockerContext, "volume", "rm", volName)
}
if recreate { if recreate {
// create the builder // create the builder
// this could be a single line, but it would be long. And it is easier to read when the // this could be a single line, but it would be long. And it is easier to read when the
// docker command args, the image name, and the image args are all on separate lines. // docker command args, the image name, and the image args are all on separate lines.
args := []string{"--context", dockerContext, "container", "create", "--name", name, "--privileged"} volName := builderVolumeName(name)
if removeVolume {
fmt.Printf("creating fresh builder state volume %s\n", volName)
} else {
fmt.Printf("reusing builder state volume %s\n", volName)
}
volMount := volName + ":/var/lib/buildkit"
args := []string{"--context", dockerContext, "container", "create", "--name", name, "--privileged", "-v", volMount}
args = append(args, image) args = append(args, image)
args = append(args, "--allow-insecure-entitlement", "network.host", "--addr", fmt.Sprintf("unix://%s", buildkitSocketPath), "--debug") args = append(args, "--allow-insecure-entitlement", "network.host", "--addr", fmt.Sprintf("unix://%s", buildkitSocketPath), "--debug")
if configPath != "" { if configPath != "" {
@@ -507,9 +542,9 @@ func (dr *dockerRunnerImpl) Tag(ref, tag string) error {
return dr.command(nil, nil, nil, "image", "tag", ref, tag) return dr.command(nil, nil, nil, "image", "tag", ref, tag)
} }
func (dr *dockerRunnerImpl) Build(ctx context.Context, tag, pkg, dockerContext, builderImage, builderConfigPath, platform string, restart, preCacheImages bool, c spec.CacheProvider, stdin io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, progressType string, imageBuildOpts spec.ImageBuildOptions) error { func (dr *dockerRunnerImpl) Build(ctx context.Context, tag, pkg, dockerContext, platform string, preCacheImages bool, c spec.CacheProvider, stdin io.Reader, stdout io.Writer, sbomScan bool, sbomScannerImage, progressType string, imageBuildOpts spec.ImageBuildOptions) error {
// ensure we have a builder // ensure we have a builder
client, err := dr.Builder(ctx, dockerContext, builderImage, builderConfigPath, platform, restart) client, err := dr.Builder(ctx, dockerContext, platform)
if err != nil { if err != nil {
return fmt.Errorf("unable to ensure builder container: %v", err) return fmt.Errorf("unable to ensure builder container: %v", err)
} }

View File

@@ -93,14 +93,14 @@ func printVerbose(tw *tabwriter.Writer, du []*buildkitClient.UsageInfo) {
_ = tw.Flush() _ = tw.Flush()
} }
func getClientForPlatform(ctx context.Context, buildersMap map[string]string, builderImage, builderConfigPath, platform string) (*buildkitClient.Client, error) { func getClientForPlatform(ctx context.Context, buildersMap map[string]string, bc BuilderConfig, platform string) (*buildkitClient.Client, error) {
p, err := platforms.Parse(platform) p, err := platforms.Parse(platform)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse platform: %s", err) return nil, fmt.Errorf("failed to parse platform: %s", err)
} }
dr := NewDockerRunner(false) dr := NewDockerRunner(false, bc)
builderName := getBuilderForPlatform(p.Architecture, buildersMap) dockerContext := getBuilderForPlatform(p.Architecture, buildersMap)
client, err := dr.Builder(ctx, builderName, builderImage, builderConfigPath, platform, false) client, err := dr.Builder(ctx, dockerContext, platform)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to ensure builder container: %v", err) return nil, fmt.Errorf("unable to ensure builder container: %v", err)
} }
@@ -108,11 +108,11 @@ func getClientForPlatform(ctx context.Context, buildersMap map[string]string, bu
} }
// DiskUsage of builder // DiskUsage of builder
func DiskUsage(buildersMap map[string]string, builderImage, builderConfigPath string, platformsToClean []string, verbose bool) error { func DiskUsage(buildersMap map[string]string, bc BuilderConfig, platformsToClean []string, verbose bool) error {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
for _, platform := range platformsToClean { for _, platform := range platformsToClean {
client, err := getClientForPlatform(ctx, buildersMap, builderImage, builderConfigPath, platform) client, err := getClientForPlatform(ctx, buildersMap, bc, platform)
if err != nil { if err != nil {
return fmt.Errorf("cannot get client: %s", err) return fmt.Errorf("cannot get client: %s", err)
} }
@@ -143,12 +143,12 @@ func DiskUsage(buildersMap map[string]string, builderImage, builderConfigPath st
} }
// PruneBuilder clean build cache of builder // PruneBuilder clean build cache of builder
func PruneBuilder(buildersMap map[string]string, builderImage, builderConfigPath string, platformsToClean []string, verbose bool) error { func PruneBuilder(buildersMap map[string]string, bc BuilderConfig, platformsToClean []string, verbose bool) error {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
total := int64(0) total := int64(0)
for _, platform := range platformsToClean { for _, platform := range platformsToClean {
client, err := getClientForPlatform(ctx, buildersMap, builderImage, builderConfigPath, platform) client, err := getClientForPlatform(ctx, buildersMap, bc, platform)
if err != nil { if err != nil {
return fmt.Errorf("cannot get client: %s", err) return fmt.Errorf("cannot get client: %s", err)
} }

View File

@@ -1,5 +1,12 @@
# Changes # Changes
## [0.7.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.6.0...compute/metadata/v0.7.0) (2025-05-13)
### Features
* **compute/metadata:** Allow canceling GCE detection ([#11786](https://github.com/googleapis/google-cloud-go/issues/11786)) ([78100fe](https://github.com/googleapis/google-cloud-go/commit/78100fe7e28cd30f1e10b47191ac3c9839663b64))
## [0.6.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.5.2...compute/metadata/v0.6.0) (2024-12-13) ## [0.6.0](https://github.com/googleapis/google-cloud-go/compare/compute/metadata/v0.5.2...compute/metadata/v0.6.0) (2024-12-13)

View File

@@ -117,82 +117,20 @@ var (
// NOTE: True returned from `OnGCE` does not guarantee that the metadata server // NOTE: True returned from `OnGCE` does not guarantee that the metadata server
// is accessible from this process and have all the metadata defined. // is accessible from this process and have all the metadata defined.
func OnGCE() bool { func OnGCE() bool {
onGCEOnce.Do(initOnGCE) return OnGCEWithContext(context.Background())
}
// OnGCEWithContext reports whether this process is running on Google Compute Platforms.
// This function's return value is memoized for better performance.
// NOTE: True returned from `OnGCEWithContext` does not guarantee that the metadata server
// is accessible from this process and have all the metadata defined.
func OnGCEWithContext(ctx context.Context) bool {
onGCEOnce.Do(func() {
onGCE = defaultClient.OnGCEWithContext(ctx)
})
return onGCE return onGCE
} }
func initOnGCE() {
onGCE = testOnGCE()
}
func testOnGCE() bool {
// The user explicitly said they're on GCE, so trust them.
if os.Getenv(metadataHostEnv) != "" {
return true
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
resc := make(chan bool, 2)
// Try two strategies in parallel.
// See https://github.com/googleapis/google-cloud-go/issues/194
go func() {
req, _ := http.NewRequest("GET", "http://"+metadataIP, nil)
req.Header.Set("User-Agent", userAgent)
res, err := newDefaultHTTPClient().Do(req.WithContext(ctx))
if err != nil {
resc <- false
return
}
defer res.Body.Close()
resc <- res.Header.Get("Metadata-Flavor") == "Google"
}()
go func() {
resolver := &net.Resolver{}
addrs, err := resolver.LookupHost(ctx, "metadata.google.internal.")
if err != nil || len(addrs) == 0 {
resc <- false
return
}
resc <- strsContains(addrs, metadataIP)
}()
tryHarder := systemInfoSuggestsGCE()
if tryHarder {
res := <-resc
if res {
// The first strategy succeeded, so let's use it.
return true
}
// Wait for either the DNS or metadata server probe to
// contradict the other one and say we are running on
// GCE. Give it a lot of time to do so, since the system
// info already suggests we're running on a GCE BIOS.
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
select {
case res = <-resc:
return res
case <-timer.C:
// Too slow. Who knows what this system is.
return false
}
}
// There's no hint from the system info that we're running on
// GCE, so use the first probe's result as truth, whether it's
// true or false. The goal here is to optimize for speed for
// users who are NOT running on GCE. We can't assume that
// either a DNS lookup or an HTTP request to a blackholed IP
// address is fast. Worst case this should return when the
// metaClient's Transport.ResponseHeaderTimeout or
// Transport.Dial.Timeout fires (in two seconds).
return <-resc
}
// Subscribe calls Client.SubscribeWithContext on the default client. // Subscribe calls Client.SubscribeWithContext on the default client.
// //
// Deprecated: Please use the context aware variant [SubscribeWithContext]. // Deprecated: Please use the context aware variant [SubscribeWithContext].
@@ -450,6 +388,84 @@ func NewWithOptions(opts *Options) *Client {
return &Client{hc: client, logger: logger} return &Client{hc: client, logger: logger}
} }
// NOTE: metadataRequestStrategy is assigned to a variable for test stubbing purposes.
var metadataRequestStrategy = func(ctx context.Context, httpClient *http.Client, resc chan bool) {
req, _ := http.NewRequest("GET", "http://"+metadataIP, nil)
req.Header.Set("User-Agent", userAgent)
res, err := httpClient.Do(req.WithContext(ctx))
if err != nil {
resc <- false
return
}
defer res.Body.Close()
resc <- res.Header.Get("Metadata-Flavor") == "Google"
}
// NOTE: dnsRequestStrategy is assigned to a variable for test stubbing purposes.
var dnsRequestStrategy = func(ctx context.Context, resc chan bool) {
resolver := &net.Resolver{}
addrs, err := resolver.LookupHost(ctx, "metadata.google.internal.")
if err != nil || len(addrs) == 0 {
resc <- false
return
}
resc <- strsContains(addrs, metadataIP)
}
// OnGCEWithContext reports whether this process is running on Google Compute Platforms.
// NOTE: True returned from `OnGCEWithContext` does not guarantee that the metadata server
// is accessible from this process and have all the metadata defined.
func (c *Client) OnGCEWithContext(ctx context.Context) bool {
// The user explicitly said they're on GCE, so trust them.
if os.Getenv(metadataHostEnv) != "" {
return true
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
resc := make(chan bool, 2)
// Try two strategies in parallel.
// See https://github.com/googleapis/google-cloud-go/issues/194
go metadataRequestStrategy(ctx, c.hc, resc)
go dnsRequestStrategy(ctx, resc)
tryHarder := systemInfoSuggestsGCE()
if tryHarder {
res := <-resc
if res {
// The first strategy succeeded, so let's use it.
return true
}
// Wait for either the DNS or metadata server probe to
// contradict the other one and say we are running on
// GCE. Give it a lot of time to do so, since the system
// info already suggests we're running on a GCE BIOS.
// Ensure cancellations from the calling context are respected.
waitContext, cancelWait := context.WithTimeout(ctx, 5*time.Second)
defer cancelWait()
select {
case res = <-resc:
return res
case <-waitContext.Done():
// Too slow. Who knows what this system is.
return false
}
}
// There's no hint from the system info that we're running on
// GCE, so use the first probe's result as truth, whether it's
// true or false. The goal here is to optimize for speed for
// users who are NOT running on GCE. We can't assume that
// either a DNS lookup or an HTTP request to a blackholed IP
// address is fast. Worst case this should return when the
// metaClient's Transport.ResponseHeaderTimeout or
// Transport.Dial.Timeout fires (in two seconds).
return <-resc
}
// getETag returns a value from the metadata service as well as the associated ETag. // getETag returns a value from the metadata service as well as the associated ETag.
// This func is otherwise equivalent to Get. // This func is otherwise equivalent to Get.
func (c *Client) getETag(ctx context.Context, suffix string) (value, etag string, err error) { func (c *Client) getETag(ctx context.Context, suffix string) (value, etag string, err error) {

View File

@@ -20,7 +20,9 @@ package metadata
// doing network requests) suggests that we're running on GCE. If this // doing network requests) suggests that we're running on GCE. If this
// returns true, testOnGCE tries a bit harder to reach its metadata // returns true, testOnGCE tries a bit harder to reach its metadata
// server. // server.
func systemInfoSuggestsGCE() bool { //
// NOTE: systemInfoSuggestsGCE is assigned to a varible for test stubbing purposes.
var systemInfoSuggestsGCE = func() bool {
// We don't currently have checks for other GOOS // We don't currently have checks for other GOOS
return false return false
} }

View File

@@ -21,8 +21,10 @@ import (
"strings" "strings"
) )
func systemInfoSuggestsGCE() bool { // NOTE: systemInfoSuggestsGCE is assigned to a varible for test stubbing purposes.
var systemInfoSuggestsGCE = func() bool {
b, _ := os.ReadFile("/sys/class/dmi/id/product_name") b, _ := os.ReadFile("/sys/class/dmi/id/product_name")
name := strings.TrimSpace(string(b)) name := strings.TrimSpace(string(b))
return name == "Google" || name == "Google Compute Engine" return name == "Google" || name == "Google Compute Engine"
} }

View File

@@ -22,7 +22,8 @@ import (
"golang.org/x/sys/windows/registry" "golang.org/x/sys/windows/registry"
) )
func systemInfoSuggestsGCE() bool { // NOTE: systemInfoSuggestsGCE is assigned to a varible for test stubbing purposes.
var systemInfoSuggestsGCE = func() bool {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\HardwareConfig\Current`, registry.QUERY_VALUE) k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\HardwareConfig\Current`, registry.QUERY_VALUE)
if err != nil { if err != nil {
return false return false

View File

@@ -1,62 +0,0 @@
package backoff
import (
"context"
"time"
)
// BackOffContext is a backoff policy that stops retrying after the context
// is canceled.
type BackOffContext interface { // nolint: golint
BackOff
Context() context.Context
}
type backOffContext struct {
BackOff
ctx context.Context
}
// WithContext returns a BackOffContext with context ctx
//
// ctx must not be nil
func WithContext(b BackOff, ctx context.Context) BackOffContext { // nolint: golint
if ctx == nil {
panic("nil context")
}
if b, ok := b.(*backOffContext); ok {
return &backOffContext{
BackOff: b.BackOff,
ctx: ctx,
}
}
return &backOffContext{
BackOff: b,
ctx: ctx,
}
}
func getContext(b BackOff) context.Context {
if cb, ok := b.(BackOffContext); ok {
return cb.Context()
}
if tb, ok := b.(*backOffTries); ok {
return getContext(tb.delegate)
}
return context.Background()
}
func (b *backOffContext) Context() context.Context {
return b.ctx
}
func (b *backOffContext) NextBackOff() time.Duration {
select {
case <-b.ctx.Done():
return Stop
default:
return b.BackOff.NextBackOff()
}
}

View File

@@ -1,216 +0,0 @@
package backoff
import (
"math/rand"
"time"
)
/*
ExponentialBackOff is a backoff implementation that increases the backoff
period for each retry attempt using a randomization function that grows exponentially.
NextBackOff() is calculated using the following formula:
randomized interval =
RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor])
In other words NextBackOff() will range between the randomization factor
percentage below and above the retry interval.
For example, given the following parameters:
RetryInterval = 2
RandomizationFactor = 0.5
Multiplier = 2
the actual backoff period used in the next retry attempt will range between 1 and 3 seconds,
multiplied by the exponential, that is, between 2 and 6 seconds.
Note: MaxInterval caps the RetryInterval and not the randomized interval.
If the time elapsed since an ExponentialBackOff instance is created goes past the
MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop.
The elapsed time can be reset by calling Reset().
Example: Given the following default arguments, for 10 tries the sequence will be,
and assuming we go over the MaxElapsedTime on the 10th try:
Request # RetryInterval (seconds) Randomized Interval (seconds)
1 0.5 [0.25, 0.75]
2 0.75 [0.375, 1.125]
3 1.125 [0.562, 1.687]
4 1.687 [0.8435, 2.53]
5 2.53 [1.265, 3.795]
6 3.795 [1.897, 5.692]
7 5.692 [2.846, 8.538]
8 8.538 [4.269, 12.807]
9 12.807 [6.403, 19.210]
10 19.210 backoff.Stop
Note: Implementation is not thread-safe.
*/
type ExponentialBackOff struct {
InitialInterval time.Duration
RandomizationFactor float64
Multiplier float64
MaxInterval time.Duration
// After MaxElapsedTime the ExponentialBackOff returns Stop.
// It never stops if MaxElapsedTime == 0.
MaxElapsedTime time.Duration
Stop time.Duration
Clock Clock
currentInterval time.Duration
startTime time.Time
}
// Clock is an interface that returns current time for BackOff.
type Clock interface {
Now() time.Time
}
// ExponentialBackOffOpts is a function type used to configure ExponentialBackOff options.
type ExponentialBackOffOpts func(*ExponentialBackOff)
// Default values for ExponentialBackOff.
const (
DefaultInitialInterval = 500 * time.Millisecond
DefaultRandomizationFactor = 0.5
DefaultMultiplier = 1.5
DefaultMaxInterval = 60 * time.Second
DefaultMaxElapsedTime = 15 * time.Minute
)
// NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
func NewExponentialBackOff(opts ...ExponentialBackOffOpts) *ExponentialBackOff {
b := &ExponentialBackOff{
InitialInterval: DefaultInitialInterval,
RandomizationFactor: DefaultRandomizationFactor,
Multiplier: DefaultMultiplier,
MaxInterval: DefaultMaxInterval,
MaxElapsedTime: DefaultMaxElapsedTime,
Stop: Stop,
Clock: SystemClock,
}
for _, fn := range opts {
fn(b)
}
b.Reset()
return b
}
// WithInitialInterval sets the initial interval between retries.
func WithInitialInterval(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.InitialInterval = duration
}
}
// WithRandomizationFactor sets the randomization factor to add jitter to intervals.
func WithRandomizationFactor(randomizationFactor float64) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.RandomizationFactor = randomizationFactor
}
}
// WithMultiplier sets the multiplier for increasing the interval after each retry.
func WithMultiplier(multiplier float64) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.Multiplier = multiplier
}
}
// WithMaxInterval sets the maximum interval between retries.
func WithMaxInterval(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.MaxInterval = duration
}
}
// WithMaxElapsedTime sets the maximum total time for retries.
func WithMaxElapsedTime(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.MaxElapsedTime = duration
}
}
// WithRetryStopDuration sets the duration after which retries should stop.
func WithRetryStopDuration(duration time.Duration) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.Stop = duration
}
}
// WithClockProvider sets the clock used to measure time.
func WithClockProvider(clock Clock) ExponentialBackOffOpts {
return func(ebo *ExponentialBackOff) {
ebo.Clock = clock
}
}
type systemClock struct{}
func (t systemClock) Now() time.Time {
return time.Now()
}
// SystemClock implements Clock interface that uses time.Now().
var SystemClock = systemClock{}
// Reset the interval back to the initial retry interval and restarts the timer.
// Reset must be called before using b.
func (b *ExponentialBackOff) Reset() {
b.currentInterval = b.InitialInterval
b.startTime = b.Clock.Now()
}
// NextBackOff calculates the next backoff interval using the formula:
// Randomized interval = RetryInterval * (1 ± RandomizationFactor)
func (b *ExponentialBackOff) NextBackOff() time.Duration {
// Make sure we have not gone over the maximum elapsed time.
elapsed := b.GetElapsedTime()
next := getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval)
b.incrementCurrentInterval()
if b.MaxElapsedTime != 0 && elapsed+next > b.MaxElapsedTime {
return b.Stop
}
return next
}
// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance
// is created and is reset when Reset() is called.
//
// The elapsed time is computed using time.Now().UnixNano(). It is
// safe to call even while the backoff policy is used by a running
// ticker.
func (b *ExponentialBackOff) GetElapsedTime() time.Duration {
return b.Clock.Now().Sub(b.startTime)
}
// Increments the current interval by multiplying it with the multiplier.
func (b *ExponentialBackOff) incrementCurrentInterval() {
// Check for overflow, if overflow is detected set the current interval to the max interval.
if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier {
b.currentInterval = b.MaxInterval
} else {
b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier)
}
}
// Returns a random value from the following interval:
// [currentInterval - randomizationFactor * currentInterval, currentInterval + randomizationFactor * currentInterval].
func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration {
if randomizationFactor == 0 {
return currentInterval // make sure no randomness is used when randomizationFactor is 0.
}
var delta = randomizationFactor * float64(currentInterval)
var minInterval = float64(currentInterval) - delta
var maxInterval = float64(currentInterval) + delta
// Get a random value from the range [minInterval, maxInterval].
// The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then
// we want a 33% chance for selecting either 1, 2 or 3.
return time.Duration(minInterval + (random * (maxInterval - minInterval + 1)))
}

View File

@@ -1,146 +0,0 @@
package backoff
import (
"errors"
"time"
)
// An OperationWithData is executing by RetryWithData() or RetryNotifyWithData().
// The operation will be retried using a backoff policy if it returns an error.
type OperationWithData[T any] func() (T, error)
// An Operation is executing by Retry() or RetryNotify().
// The operation will be retried using a backoff policy if it returns an error.
type Operation func() error
func (o Operation) withEmptyData() OperationWithData[struct{}] {
return func() (struct{}, error) {
return struct{}{}, o()
}
}
// Notify is a notify-on-error function. It receives an operation error and
// backoff delay if the operation failed (with an error).
//
// NOTE that if the backoff policy stated to stop retrying,
// the notify function isn't called.
type Notify func(error, time.Duration)
// Retry the operation o until it does not return error or BackOff stops.
// o is guaranteed to be run at least once.
//
// If o returns a *PermanentError, the operation is not retried, and the
// wrapped error is returned.
//
// Retry sleeps the goroutine for the duration returned by BackOff after a
// failed operation returns.
func Retry(o Operation, b BackOff) error {
return RetryNotify(o, b, nil)
}
// RetryWithData is like Retry but returns data in the response too.
func RetryWithData[T any](o OperationWithData[T], b BackOff) (T, error) {
return RetryNotifyWithData(o, b, nil)
}
// RetryNotify calls notify function with the error and wait duration
// for each failed attempt before sleep.
func RetryNotify(operation Operation, b BackOff, notify Notify) error {
return RetryNotifyWithTimer(operation, b, notify, nil)
}
// RetryNotifyWithData is like RetryNotify but returns data in the response too.
func RetryNotifyWithData[T any](operation OperationWithData[T], b BackOff, notify Notify) (T, error) {
return doRetryNotify(operation, b, notify, nil)
}
// RetryNotifyWithTimer calls notify function with the error and wait duration using the given Timer
// for each failed attempt before sleep.
// A default timer that uses system timer is used when nil is passed.
func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer) error {
_, err := doRetryNotify(operation.withEmptyData(), b, notify, t)
return err
}
// RetryNotifyWithTimerAndData is like RetryNotifyWithTimer but returns data in the response too.
func RetryNotifyWithTimerAndData[T any](operation OperationWithData[T], b BackOff, notify Notify, t Timer) (T, error) {
return doRetryNotify(operation, b, notify, t)
}
func doRetryNotify[T any](operation OperationWithData[T], b BackOff, notify Notify, t Timer) (T, error) {
var (
err error
next time.Duration
res T
)
if t == nil {
t = &defaultTimer{}
}
defer func() {
t.Stop()
}()
ctx := getContext(b)
b.Reset()
for {
res, err = operation()
if err == nil {
return res, nil
}
var permanent *PermanentError
if errors.As(err, &permanent) {
return res, permanent.Err
}
if next = b.NextBackOff(); next == Stop {
if cerr := ctx.Err(); cerr != nil {
return res, cerr
}
return res, err
}
if notify != nil {
notify(err, next)
}
t.Start(next)
select {
case <-ctx.Done():
return res, ctx.Err()
case <-t.C():
}
}
}
// PermanentError signals that the operation should not be retried.
type PermanentError struct {
Err error
}
func (e *PermanentError) Error() string {
return e.Err.Error()
}
func (e *PermanentError) Unwrap() error {
return e.Err
}
func (e *PermanentError) Is(target error) bool {
_, ok := target.(*PermanentError)
return ok
}
// Permanent wraps the given err in a *PermanentError.
func Permanent(err error) error {
if err == nil {
return nil
}
return &PermanentError{
Err: err,
}
}

View File

@@ -1,38 +0,0 @@
package backoff
import "time"
/*
WithMaxRetries creates a wrapper around another BackOff, which will
return Stop if NextBackOff() has been called too many times since
the last time Reset() was called
Note: Implementation is not thread-safe.
*/
func WithMaxRetries(b BackOff, max uint64) BackOff {
return &backOffTries{delegate: b, maxTries: max}
}
type backOffTries struct {
delegate BackOff
maxTries uint64
numTries uint64
}
func (b *backOffTries) NextBackOff() time.Duration {
if b.maxTries == 0 {
return Stop
}
if b.maxTries > 0 {
if b.maxTries <= b.numTries {
return Stop
}
b.numTries++
}
return b.delegate.NextBackOff()
}
func (b *backOffTries) Reset() {
b.numTries = 0
b.delegate.Reset()
}

View File

@@ -0,0 +1,29 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [5.0.0] - 2024-12-19
### Added
- RetryAfterError can be returned from an operation to indicate how long to wait before the next retry.
### Changed
- Retry function now accepts additional options for specifying max number of tries and max elapsed time.
- Retry function now accepts a context.Context.
- Operation function signature changed to return result (any type) and error.
### Removed
- RetryNotify* and RetryWithData functions. Only single Retry function remains.
- Optional arguments from ExponentialBackoff constructor.
- Clock and Timer interfaces.
### Fixed
- The original error is returned from Retry if there's a PermanentError. (#144)
- The Retry function respects the wrapped PermanentError. (#140)

View File

@@ -1,4 +1,4 @@
# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Coverage Status][coveralls image]][coveralls] # Exponential Backoff [![GoDoc][godoc image]][godoc]
This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client]. This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client].
@@ -9,9 +9,11 @@ The retries exponentially increase and stop increasing when a certain threshold
## Usage ## Usage
Import path is `github.com/cenkalti/backoff/v4`. Please note the version part at the end. Import path is `github.com/cenkalti/backoff/v5`. Please note the version part at the end.
Use https://pkg.go.dev/github.com/cenkalti/backoff/v4 to view the documentation. For most cases, use `Retry` function. See [example_test.go][example] for an example.
If you have specific needs, copy `Retry` function (from [retry.go][retry-src]) into your code and modify it as needed.
## Contributing ## Contributing
@@ -19,12 +21,11 @@ Use https://pkg.go.dev/github.com/cenkalti/backoff/v4 to view the documentation.
* Please don't send a PR without opening an issue and discussing it first. * Please don't send a PR without opening an issue and discussing it first.
* If proposed change is not a common use case, I will probably not accept it. * If proposed change is not a common use case, I will probably not accept it.
[godoc]: https://pkg.go.dev/github.com/cenkalti/backoff/v4 [godoc]: https://pkg.go.dev/github.com/cenkalti/backoff/v5
[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png [godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png
[coveralls]: https://coveralls.io/github/cenkalti/backoff?branch=master
[coveralls image]: https://coveralls.io/repos/github/cenkalti/backoff/badge.svg?branch=master
[google-http-java-client]: https://github.com/google/google-http-java-client/blob/da1aa993e90285ec18579f1553339b00e19b3ab5/google-http-client/src/main/java/com/google/api/client/util/ExponentialBackOff.java [google-http-java-client]: https://github.com/google/google-http-java-client/blob/da1aa993e90285ec18579f1553339b00e19b3ab5/google-http-client/src/main/java/com/google/api/client/util/ExponentialBackOff.java
[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff [exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff
[advanced example]: https://pkg.go.dev/github.com/cenkalti/backoff/v4?tab=doc#pkg-examples [retry-src]: https://github.com/cenkalti/backoff/blob/v5/retry.go
[example]: https://github.com/cenkalti/backoff/blob/v5/example_test.go

View File

@@ -15,16 +15,16 @@ import "time"
// BackOff is a backoff policy for retrying an operation. // BackOff is a backoff policy for retrying an operation.
type BackOff interface { type BackOff interface {
// NextBackOff returns the duration to wait before retrying the operation, // NextBackOff returns the duration to wait before retrying the operation,
// or backoff. Stop to indicate that no more retries should be made. // backoff.Stop to indicate that no more retries should be made.
// //
// Example usage: // Example usage:
// //
// duration := backoff.NextBackOff(); // duration := backoff.NextBackOff()
// if (duration == backoff.Stop) { // if duration == backoff.Stop {
// // Do not retry operation. // // Do not retry operation.
// } else { // } else {
// // Sleep for duration and retry operation. // // Sleep for duration and retry operation.
// } // }
// //
NextBackOff() time.Duration NextBackOff() time.Duration

View File

@@ -0,0 +1,46 @@
package backoff
import (
"fmt"
"time"
)
// PermanentError signals that the operation should not be retried.
type PermanentError struct {
Err error
}
// Permanent wraps the given err in a *PermanentError.
func Permanent(err error) error {
if err == nil {
return nil
}
return &PermanentError{
Err: err,
}
}
// Error returns a string representation of the Permanent error.
func (e *PermanentError) Error() string {
return e.Err.Error()
}
// Unwrap returns the wrapped error.
func (e *PermanentError) Unwrap() error {
return e.Err
}
// RetryAfterError signals that the operation should be retried after the given duration.
type RetryAfterError struct {
Duration time.Duration
}
// RetryAfter returns a RetryAfter error that specifies how long to wait before retrying.
func RetryAfter(seconds int) error {
return &RetryAfterError{Duration: time.Duration(seconds) * time.Second}
}
// Error returns a string representation of the RetryAfter error.
func (e *RetryAfterError) Error() string {
return fmt.Sprintf("retry after %s", e.Duration)
}

View File

@@ -0,0 +1,118 @@
package backoff
import (
"math/rand/v2"
"time"
)
/*
ExponentialBackOff is a backoff implementation that increases the backoff
period for each retry attempt using a randomization function that grows exponentially.
NextBackOff() is calculated using the following formula:
randomized interval =
RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor])
In other words NextBackOff() will range between the randomization factor
percentage below and above the retry interval.
For example, given the following parameters:
RetryInterval = 2
RandomizationFactor = 0.5
Multiplier = 2
the actual backoff period used in the next retry attempt will range between 1 and 3 seconds,
multiplied by the exponential, that is, between 2 and 6 seconds.
Note: MaxInterval caps the RetryInterval and not the randomized interval.
Example: Given the following default arguments, for 9 tries the sequence will be:
Request # RetryInterval (seconds) Randomized Interval (seconds)
1 0.5 [0.25, 0.75]
2 0.75 [0.375, 1.125]
3 1.125 [0.562, 1.687]
4 1.687 [0.8435, 2.53]
5 2.53 [1.265, 3.795]
6 3.795 [1.897, 5.692]
7 5.692 [2.846, 8.538]
8 8.538 [4.269, 12.807]
9 12.807 [6.403, 19.210]
Note: Implementation is not thread-safe.
*/
type ExponentialBackOff struct {
InitialInterval time.Duration
RandomizationFactor float64
Multiplier float64
MaxInterval time.Duration
currentInterval time.Duration
}
// Default values for ExponentialBackOff.
const (
DefaultInitialInterval = 500 * time.Millisecond
DefaultRandomizationFactor = 0.5
DefaultMultiplier = 1.5
DefaultMaxInterval = 60 * time.Second
)
// NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
func NewExponentialBackOff() *ExponentialBackOff {
return &ExponentialBackOff{
InitialInterval: DefaultInitialInterval,
RandomizationFactor: DefaultRandomizationFactor,
Multiplier: DefaultMultiplier,
MaxInterval: DefaultMaxInterval,
}
}
// Reset the interval back to the initial retry interval and restarts the timer.
// Reset must be called before using b.
func (b *ExponentialBackOff) Reset() {
b.currentInterval = b.InitialInterval
}
// NextBackOff calculates the next backoff interval using the formula:
//
// Randomized interval = RetryInterval * (1 ± RandomizationFactor)
func (b *ExponentialBackOff) NextBackOff() time.Duration {
if b.currentInterval == 0 {
b.currentInterval = b.InitialInterval
}
next := getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval)
b.incrementCurrentInterval()
return next
}
// Increments the current interval by multiplying it with the multiplier.
func (b *ExponentialBackOff) incrementCurrentInterval() {
// Check for overflow, if overflow is detected set the current interval to the max interval.
if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier {
b.currentInterval = b.MaxInterval
} else {
b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier)
}
}
// Returns a random value from the following interval:
//
// [currentInterval - randomizationFactor * currentInterval, currentInterval + randomizationFactor * currentInterval].
func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration {
if randomizationFactor == 0 {
return currentInterval // make sure no randomness is used when randomizationFactor is 0.
}
var delta = randomizationFactor * float64(currentInterval)
var minInterval = float64(currentInterval) - delta
var maxInterval = float64(currentInterval) + delta
// Get a random value from the range [minInterval, maxInterval].
// The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then
// we want a 33% chance for selecting either 1, 2 or 3.
return time.Duration(minInterval + (random * (maxInterval - minInterval + 1)))
}

View File

@@ -0,0 +1,139 @@
package backoff
import (
"context"
"errors"
"time"
)
// DefaultMaxElapsedTime sets a default limit for the total retry duration.
const DefaultMaxElapsedTime = 15 * time.Minute
// Operation is a function that attempts an operation and may be retried.
type Operation[T any] func() (T, error)
// Notify is a function called on operation error with the error and backoff duration.
type Notify func(error, time.Duration)
// retryOptions holds configuration settings for the retry mechanism.
type retryOptions struct {
BackOff BackOff // Strategy for calculating backoff periods.
Timer timer // Timer to manage retry delays.
Notify Notify // Optional function to notify on each retry error.
MaxTries uint // Maximum number of retry attempts.
MaxElapsedTime time.Duration // Maximum total time for all retries.
}
type RetryOption func(*retryOptions)
// WithBackOff configures a custom backoff strategy.
func WithBackOff(b BackOff) RetryOption {
return func(args *retryOptions) {
args.BackOff = b
}
}
// withTimer sets a custom timer for managing delays between retries.
func withTimer(t timer) RetryOption {
return func(args *retryOptions) {
args.Timer = t
}
}
// WithNotify sets a notification function to handle retry errors.
func WithNotify(n Notify) RetryOption {
return func(args *retryOptions) {
args.Notify = n
}
}
// WithMaxTries limits the number of all attempts.
func WithMaxTries(n uint) RetryOption {
return func(args *retryOptions) {
args.MaxTries = n
}
}
// WithMaxElapsedTime limits the total duration for retry attempts.
func WithMaxElapsedTime(d time.Duration) RetryOption {
return func(args *retryOptions) {
args.MaxElapsedTime = d
}
}
// Retry attempts the operation until success, a permanent error, or backoff completion.
// It ensures the operation is executed at least once.
//
// Returns the operation result or error if retries are exhausted or context is cancelled.
func Retry[T any](ctx context.Context, operation Operation[T], opts ...RetryOption) (T, error) {
// Initialize default retry options.
args := &retryOptions{
BackOff: NewExponentialBackOff(),
Timer: &defaultTimer{},
MaxElapsedTime: DefaultMaxElapsedTime,
}
// Apply user-provided options to the default settings.
for _, opt := range opts {
opt(args)
}
defer args.Timer.Stop()
startedAt := time.Now()
args.BackOff.Reset()
for numTries := uint(1); ; numTries++ {
// Execute the operation.
res, err := operation()
if err == nil {
return res, nil
}
// Stop retrying if maximum tries exceeded.
if args.MaxTries > 0 && numTries >= args.MaxTries {
return res, err
}
// Handle permanent errors without retrying.
var permanent *PermanentError
if errors.As(err, &permanent) {
return res, permanent.Unwrap()
}
// Stop retrying if context is cancelled.
if cerr := context.Cause(ctx); cerr != nil {
return res, cerr
}
// Calculate next backoff duration.
next := args.BackOff.NextBackOff()
if next == Stop {
return res, err
}
// Reset backoff if RetryAfterError is encountered.
var retryAfter *RetryAfterError
if errors.As(err, &retryAfter) {
next = retryAfter.Duration
args.BackOff.Reset()
}
// Stop retrying if maximum elapsed time exceeded.
if args.MaxElapsedTime > 0 && time.Since(startedAt)+next > args.MaxElapsedTime {
return res, err
}
// Notify on error if a notifier function is provided.
if args.Notify != nil {
args.Notify(err, next)
}
// Wait for the next backoff period or context cancellation.
args.Timer.Start(next)
select {
case <-args.Timer.C():
case <-ctx.Done():
return res, context.Cause(ctx)
}
}
}

View File

@@ -1,7 +1,6 @@
package backoff package backoff
import ( import (
"context"
"sync" "sync"
"time" "time"
) )
@@ -14,8 +13,7 @@ type Ticker struct {
C <-chan time.Time C <-chan time.Time
c chan time.Time c chan time.Time
b BackOff b BackOff
ctx context.Context timer timer
timer Timer
stop chan struct{} stop chan struct{}
stopOnce sync.Once stopOnce sync.Once
} }
@@ -27,22 +25,12 @@ type Ticker struct {
// provided backoff policy (notably calling NextBackOff or Reset) // provided backoff policy (notably calling NextBackOff or Reset)
// while the ticker is running. // while the ticker is running.
func NewTicker(b BackOff) *Ticker { func NewTicker(b BackOff) *Ticker {
return NewTickerWithTimer(b, &defaultTimer{})
}
// NewTickerWithTimer returns a new Ticker with a custom timer.
// A default timer that uses system timer is used when nil is passed.
func NewTickerWithTimer(b BackOff, timer Timer) *Ticker {
if timer == nil {
timer = &defaultTimer{}
}
c := make(chan time.Time) c := make(chan time.Time)
t := &Ticker{ t := &Ticker{
C: c, C: c,
c: c, c: c,
b: b, b: b,
ctx: getContext(b), timer: &defaultTimer{},
timer: timer,
stop: make(chan struct{}), stop: make(chan struct{}),
} }
t.b.Reset() t.b.Reset()
@@ -73,8 +61,6 @@ func (t *Ticker) run() {
case <-t.stop: case <-t.stop:
t.c = nil // Prevent future ticks from being sent to the channel. t.c = nil // Prevent future ticks from being sent to the channel.
return return
case <-t.ctx.Done():
return
} }
} }
} }

View File

@@ -2,7 +2,7 @@ package backoff
import "time" import "time"
type Timer interface { type timer interface {
Start(duration time.Duration) Start(duration time.Duration)
Stop() Stop()
C() <-chan time.Time C() <-chan time.Time

View File

@@ -170,6 +170,11 @@ type Syncer interface {
Sync() error Sync() error
} }
// ReferrersProvider handles looking up additional referrer objects for a given descriptor.
type ReferrersProvider interface {
Referrers(context.Context, ocispec.Descriptor) ([]ocispec.Descriptor, error)
}
// Opt is used to alter the mutable properties of content // Opt is used to alter the mutable properties of content
type Opt func(*Info) error type Opt func(*Info) error

View File

@@ -20,4 +20,10 @@ const (
// AnnotationImageName is an annotation on a Descriptor in an index.json // AnnotationImageName is an annotation on a Descriptor in an index.json
// containing the `Name` value as used by an `Image` struct // containing the `Name` value as used by an `Image` struct
AnnotationImageName = "io.containerd.image.name" AnnotationImageName = "io.containerd.image.name"
// AnnotationManifestSubject is an annotation on a Descriptor that means
// that current descriptor is a referrer to the subject manifest.
// If descriptor in image.json has this annotation, it will not create
// a new image.
AnnotationManifestSubject = "io.containerd.manifest.subject"
) )

View File

@@ -20,7 +20,9 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"slices"
"sort" "sort"
"strings"
"github.com/containerd/containerd/v2/core/content" "github.com/containerd/containerd/v2/core/content"
"github.com/containerd/errdefs" "github.com/containerd/errdefs"
@@ -198,6 +200,35 @@ func ChildrenHandler(provider content.Provider) HandlerFunc {
} }
} }
// SetReferrers is a handler wrapper which adds referrer descriptors
// from the provided ReferrersProvider and adds them to the children
// returned by the handler. The referrers will have a container-specific
// annotation added to indicate the subject descriptor.
func SetReferrers(refProvider content.ReferrersProvider, f HandlerFunc) HandlerFunc {
return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
children, err := f(ctx, desc)
if err != nil {
return children, err
}
if !IsManifestType(desc.MediaType) && !IsIndexType(desc.MediaType) {
return children, nil
}
refs, err := refProvider.Referrers(ctx, desc)
if err != nil {
return children, err
}
children = slices.Grow(children, len(refs))
for _, ref := range refs {
if ref.Annotations == nil {
ref.Annotations = map[string]string{}
}
ref.Annotations[AnnotationManifestSubject] = desc.Digest.String()
children = append(children, ref)
}
return children, nil
}
}
// SetChildrenLabels is a handler wrapper which sets labels for the content on // SetChildrenLabels is a handler wrapper which sets labels for the content on
// the children returned by the handler and passes through the children. // the children returned by the handler and passes through the children.
// Must follow a handler that returns the children to be labeled. // Must follow a handler that returns the children to be labeled.
@@ -235,7 +266,9 @@ func SetChildrenMappedLabels(manager content.Manager, f HandlerFunc, labelMap fu
for _, key := range labelKeys { for _, key := range labelKeys {
idx := keys[key] idx := keys[key]
keys[key] = idx + 1 keys[key] = idx + 1
if idx > 0 || key[len(key)-1] == '.' { if strings.HasSuffix(key, ".sha256.") {
key = fmt.Sprintf("%s%s", key, ch.Digest.Hex()[:12])
} else if idx > 0 || key[len(key)-1] == '.' {
key = fmt.Sprintf("%s%d", key, idx) key = fmt.Sprintf("%s%d", key, idx)
} }

View File

@@ -208,6 +208,9 @@ func IsAttestationType(mt string) bool {
// ChildGCLabels returns the label for a given descriptor to reference it // ChildGCLabels returns the label for a given descriptor to reference it
func ChildGCLabels(desc ocispec.Descriptor) []string { func ChildGCLabels(desc ocispec.Descriptor) []string {
if _, ok := desc.Annotations[AnnotationManifestSubject]; ok {
return []string{"containerd.io/gc.ref.content.referrer.sha256."}
}
mt := desc.MediaType mt := desc.MediaType
if IsKnownConfig(mt) { if IsKnownConfig(mt) {
return []string{"containerd.io/gc.ref.content.config"} return []string{"containerd.io/gc.ref.content.config"}

View File

@@ -46,7 +46,7 @@ func (ec ErrorCode) ErrorCode() ErrorCode {
// Error returns the ID/Value // Error returns the ID/Value
func (ec ErrorCode) Error() string { func (ec ErrorCode) Error() string {
// NOTE(stevvooe): Cannot use message here since it may have unpopulated args. // NOTE(stevvooe): Cannot use message here since it may have unpopulated args.
return strings.ToLower(strings.Replace(ec.String(), "_", " ", -1)) return strings.ToLower(strings.ReplaceAll(ec.String(), "_", " "))
} }
// Descriptor returns the descriptor for the error code. // Descriptor returns the descriptor for the error code.

View File

@@ -26,7 +26,6 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"sync" "sync"
@@ -260,7 +259,7 @@ func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.R
req.path = req.path + "?" + u.RawQuery req.path = req.path + "?" + u.RawQuery
} }
rc, err := r.open(ctx, req, desc.MediaType, offset, false) rc, _, err := r.open(ctx, req, desc.MediaType, offset, false)
if err != nil { if err != nil {
if errdefs.IsNotFound(err) { if errdefs.IsNotFound(err) {
continue // try one of the other urls. continue // try one of the other urls.
@@ -282,7 +281,7 @@ func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.R
return nil, err return nil, err
} }
rc, err := r.open(ctx, req, desc.MediaType, offset, i == len(r.hosts)-1) rc, _, err := r.open(ctx, req, desc.MediaType, offset, i == len(r.hosts)-1)
if err != nil { if err != nil {
// Store the error for referencing later // Store the error for referencing later
if firstErr == nil { if firstErr == nil {
@@ -305,7 +304,7 @@ func (r dockerFetcher) Fetch(ctx context.Context, desc ocispec.Descriptor) (io.R
return nil, err return nil, err
} }
rc, err := r.open(ctx, req, desc.MediaType, offset, i == len(r.hosts)-1) rc, _, err := r.open(ctx, req, desc.MediaType, offset, i == len(r.hosts)-1)
if err != nil { if err != nil {
// Store the error for referencing later // Store the error for referencing later
if firstErr == nil { if firstErr == nil {
@@ -419,8 +418,9 @@ func (r dockerFetcher) FetchByDigest(ctx context.Context, dgst digest.Digest, op
return nil, desc, firstErr return nil, desc, firstErr
} }
seeker, err := newHTTPReadSeeker(sz, func(offset int64) (io.ReadCloser, error) { seeker, err := newHTTPReadSeeker(sz, func(offset int64) (rc io.ReadCloser, err error) {
return r.open(ctx, getReq, config.Mediatype, offset, true) rc, _, err = r.open(ctx, getReq, config.Mediatype, offset, true)
return
}) })
if err != nil { if err != nil {
return nil, desc, err return nil, desc, err
@@ -437,7 +437,7 @@ func (r dockerFetcher) FetchByDigest(ctx context.Context, dgst digest.Digest, op
return seeker, desc, nil return seeker, desc, nil
} }
func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string, offset int64, lastHost bool) (_ io.ReadCloser, retErr error) { func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string, offset int64, lastHost bool) (_ io.ReadCloser, _ int64, retErr error) {
const minChunkSize = 512 const minChunkSize = 512
chunkSize := int64(r.performances.ConcurrentLayerFetchBuffer) chunkSize := int64(r.performances.ConcurrentLayerFetchBuffer)
@@ -457,29 +457,42 @@ func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string,
} }
if err := r.Acquire(ctx, 1); err != nil { if err := r.Acquire(ctx, 1); err != nil {
return nil, err return nil, 0, err
} }
resp, err := req.doWithRetries(ctx, lastHost, withErrorCheck, withOffsetCheck(offset)) var remaining int64
resp, err := req.doWithRetries(ctx, lastHost, withErrorCheck, withOffsetCheck(offset, parallelism))
switch err { switch err {
case nil: case nil:
// all good // all good
remaining = resp.ContentLength
case errContentRangeIgnored: case errContentRangeIgnored:
if parallelism != 1 { if parallelism != 1 {
log.G(ctx).WithError(err).Info("remote host ignored content range, forcing parallelism to 1") log.G(ctx).WithError(err).Info("remote host ignored content range, forcing parallelism to 1")
parallelism = 1 parallelism = 1
} }
remaining = resp.ContentLength - offset
default: default:
log.G(ctx).WithError(err).Debug("fetch failed") log.G(ctx).WithError(err).Debug("fetch failed")
r.Release(1) r.Release(1)
return nil, err return nil, 0, err
} }
body := resp.Body body := &fnOnClose{
BeforeClose: func() {
r.Release(1)
},
ReadCloser: resp.Body,
}
defer func() {
if retErr != nil {
body.Close()
}
}()
encoding := strings.FieldsFunc(resp.Header.Get("Content-Encoding"), func(r rune) bool { encoding := strings.FieldsFunc(resp.Header.Get("Content-Encoding"), func(r rune) bool {
return r == ' ' || r == '\t' || r == ',' return r == ' ' || r == '\t' || r == ','
}) })
remaining, _ := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 0)
if remaining <= chunkSize { if remaining <= chunkSize {
parallelism = 1 parallelism = 1
} }
@@ -505,29 +518,33 @@ func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string,
for i := range numChunks { for i := range numChunks {
readers[i], writers[i] = newPipeWriter(bufPool) readers[i], writers[i] = newPipeWriter(bufPool)
} }
// keep reference of the initial body value to ensure it is closed
ibody := body
go func() { go func() {
for i := range numChunks { for i := range numChunks {
select { select {
case queue <- i: case queue <- i:
case <-done: case <-done:
if i == 0 {
ibody.Close()
}
return // avoid leaking a goroutine if we exit early. return // avoid leaking a goroutine if we exit early.
} }
} }
close(queue) close(queue)
}() }()
r.Release(1)
for range parallelism { for range parallelism {
go func() { go func() {
for i := range queue { // first in first out for i := range queue { // first in first out
copy := func() error { copy := func() error {
if err := r.Acquire(ctx, 1); err != nil {
return err
}
defer r.Release(1)
var body io.ReadCloser var body io.ReadCloser
if i == 0 { if i == 0 {
body = resp.Body body = ibody
} else { } else {
if err := r.Acquire(ctx, 1); err != nil {
return err
}
defer r.Release(1)
reqClone := req.clone() reqClone := req.clone()
reqClone.setOffset(offset + i*chunkSize) reqClone.setOffset(offset + i*chunkSize)
nresp, err := reqClone.doWithRetries(ctx, lastHost, withErrorCheck) nresp, err := reqClone.doWithRetries(ctx, lastHost, withErrorCheck)
@@ -564,40 +581,35 @@ func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string,
}, },
ReadCloser: io.NopCloser(io.MultiReader(readers...)), ReadCloser: io.NopCloser(io.MultiReader(readers...)),
} }
} else {
body = &fnOnClose{
BeforeClose: func() {
r.Release(1)
},
ReadCloser: body,
}
} }
for i := len(encoding) - 1; i >= 0; i-- { for i := len(encoding) - 1; i >= 0; i-- {
algorithm := strings.ToLower(encoding[i]) algorithm := strings.ToLower(encoding[i])
switch algorithm { switch algorithm {
case "zstd": case "zstd":
r, err := zstd.NewReader(body, r, err := zstd.NewReader(body.ReadCloser,
zstd.WithDecoderLowmem(false), zstd.WithDecoderLowmem(false),
) )
if err != nil { if err != nil {
return nil, err return nil, 0, err
} }
body = r.IOReadCloser() body.ReadCloser = r.IOReadCloser()
case "gzip": case "gzip":
body, err = gzip.NewReader(body) r, err := gzip.NewReader(body.ReadCloser)
if err != nil { if err != nil {
return nil, err return nil, 0, err
} }
body.ReadCloser = r
case "deflate": case "deflate":
body = flate.NewReader(body) body.ReadCloser = flate.NewReader(body.ReadCloser)
case "identity", "": case "identity", "":
// no content-encoding applied, use raw body // no content-encoding applied, use raw body
default: default:
return nil, errors.New("unsupported Content-Encoding algorithm: " + algorithm) return nil, 0, errors.New("unsupported Content-Encoding algorithm: " + algorithm)
} }
} }
return body, nil return body, remaining, nil
} }
type fnOnClose struct { type fnOnClose struct {

View File

@@ -59,7 +59,8 @@ func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) {
if n > 0 || err == nil { if n > 0 || err == nil {
hrs.errsWithNoProgress = 0 hrs.errsWithNoProgress = 0
} }
if err == io.ErrUnexpectedEOF { switch err {
case io.ErrUnexpectedEOF:
// connection closed unexpectedly. try reconnecting. // connection closed unexpectedly. try reconnecting.
if n == 0 { if n == 0 {
hrs.errsWithNoProgress++ hrs.errsWithNoProgress++
@@ -76,7 +77,7 @@ func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) {
if _, err2 := hrs.reader(); err2 == nil { if _, err2 := hrs.reader(); err2 == nil {
return n, nil return n, nil
} }
} else if err == io.EOF { case io.EOF:
// The CRI's imagePullProgressTimeout relies on responseBody.Close to // The CRI's imagePullProgressTimeout relies on responseBody.Close to
// update the process monitor's status. If the err is io.EOF, close // update the process monitor's status. If the err is io.EOF, close
// the connection since there is no more available data. // the connection since there is no more available data.
@@ -102,6 +103,36 @@ func (hrs *httpReadSeeker) Close() error {
return nil return nil
} }
func (hrs *httpReadSeeker) ReadAt(p []byte, offset int64) (n int, err error) {
if hrs.closed {
return 0, fmt.Errorf("httpReadSeeker.ReadAt: closed: %w", errdefs.ErrUnavailable)
}
if offset < 0 {
return 0, fmt.Errorf("httpReadSeeker.ReadAt: negative offset: %w", errdefs.ErrInvalidArgument)
}
if hrs.size != -1 && offset >= hrs.size {
return 0, io.EOF
}
if hrs.open == nil {
return 0, fmt.Errorf("httpReadSeeker.ReadAt: cannot open: %w", errdefs.ErrNotImplemented)
}
rc, err := hrs.open(offset)
if err != nil {
return 0, fmt.Errorf("httpReadSeeker.ReadAt: failed to open at offset %d: %w", offset, err)
}
defer func() {
if closeErr := rc.Close(); closeErr != nil {
log.L.WithError(closeErr).Error("httpReadSeeker.ReadAt: failed to close ReadCloser")
}
}()
return io.ReadFull(rc, p)
}
func (hrs *httpReadSeeker) Seek(offset int64, whence int) (int64, error) { func (hrs *httpReadSeeker) Seek(offset int64, whence int) (int64, error) {
if hrs.closed { if hrs.closed {
return 0, fmt.Errorf("Fetcher.Seek: closed: %w", errdefs.ErrUnavailable) return 0, fmt.Errorf("Fetcher.Seek: closed: %w", errdefs.ErrUnavailable)

View File

@@ -526,15 +526,21 @@ func (pw *pushWriter) Commit(ctx context.Context, size int64, expected digest.Di
if expected == "" { if expected == "" {
expected = status.Expected expected = status.Expected
} else if expected != status.Expected {
return fmt.Errorf("unexpected digest received: got %q, expected %q", status.Expected, expected)
} }
actual, err := digest.Parse(resp.Header.Get("Docker-Content-Digest")) if dgstHdr := resp.Header.Get("Docker-Content-Digest"); dgstHdr != "" {
if err != nil { actual, err := digest.Parse(dgstHdr)
return fmt.Errorf("invalid content digest in response: %w", err) if err != nil {
} return fmt.Errorf("invalid content digest in response: %w", err)
}
if actual != expected { if actual != expected {
return fmt.Errorf("got digest %s, expected %s", actual, expected) return fmt.Errorf("got digest %s, expected %s", actual, expected)
}
} else {
log.G(ctx).Info("registry did not send a Docker-Content-Digest header")
} }
status.Committed = true status.Committed = true

View File

@@ -0,0 +1,158 @@
/*
Copyright The containerd 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 docker
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/containerd/containerd/v2/core/remotes"
"github.com/containerd/errdefs"
"github.com/containerd/log"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
func (r dockerFetcher) FetchReferrers(ctx context.Context, dgst digest.Digest, opts ...remotes.FetchReferrersOpt) ([]ocispec.Descriptor, error) {
var config remotes.FetchReferrersConfig
for _, opt := range opts {
opt(ctx, &config)
}
rc, size, err := r.openReferrers(ctx, dgst, config)
if err != nil {
if errdefs.IsNotFound(err) {
return []ocispec.Descriptor{}, nil
}
return nil, err
}
defer rc.Close()
if size < 0 {
size = MaxManifestSize
} else if size > MaxManifestSize {
return nil, fmt.Errorf("referrers index size %d exceeds maximum allowed %d: %w", size, MaxManifestSize, errdefs.ErrNotFound)
}
var index ocispec.Index
dec := json.NewDecoder(io.LimitReader(rc, size))
if err := dec.Decode(&index); err != nil {
return nil, fmt.Errorf("failed to decode referrers index: %w", err)
}
if _, err := dec.Token(); !errors.Is(err, io.EOF) {
return nil, fmt.Errorf("unexpected data after JSON object")
}
if len(config.ArtifactTypes) == 0 {
return index.Manifests, nil
}
var referrers []ocispec.Descriptor
tFilter := map[string]struct{}{}
for _, t := range config.ArtifactTypes {
tFilter[t] = struct{}{}
}
for _, desc := range index.Manifests {
if _, ok := tFilter[desc.ArtifactType]; ok {
referrers = append(referrers, desc)
}
}
return referrers, nil
}
func (r dockerFetcher) openReferrers(ctx context.Context, dgst digest.Digest, config remotes.FetchReferrersConfig) (io.ReadCloser, int64, error) {
mediaType := ocispec.MediaTypeImageIndex
ctx = log.WithLogger(ctx, log.G(ctx).WithField("digest", dgst))
hosts := r.filterHosts(HostCapabilityReferrers)
var fallbackHosts []RegistryHost
if len(hosts) == 0 {
fallbackHosts = r.filterHosts(HostCapabilityResolve)
if len(fallbackHosts) == 0 {
return nil, 0, fmt.Errorf("no referrers hosts: %w", errdefs.ErrNotFound)
}
} else {
// If referrers are defined, use same hosts for fallback
fallbackHosts = hosts
}
ctx, err := ContextWithRepositoryScope(ctx, r.refspec, false)
if err != nil {
return nil, 0, err
}
var firstErr error
for i, host := range hosts {
req := r.request(host, http.MethodGet, "referrers", dgst.String())
for _, artifactType := range config.ArtifactTypes {
if err := req.addQuery("artifactType", artifactType); err != nil {
return nil, 0, err
}
}
for k, vs := range config.QueryFilters {
for _, v := range vs {
if err := req.addQuery(k, v); err != nil {
return nil, 0, err
}
}
}
if err := req.addNamespace(r.refspec.Hostname()); err != nil {
return nil, 0, err
}
rc, cl, err := r.open(ctx, req, mediaType, 0, i == len(hosts)-1)
if err != nil {
if !errdefs.IsNotFound(err) {
log.G(ctx).WithError(err).WithField("host", host.Host).Debug("error fetching referrers")
if firstErr == nil {
firstErr = err
}
}
} else {
return rc, cl, nil
}
}
for i, host := range fallbackHosts {
req := r.request(host, http.MethodGet, "manifests", strings.Replace(dgst.String(), ":", "-", 1))
if err := req.addNamespace(r.refspec.Hostname()); err != nil {
return nil, 0, err
}
rc, cl, err := r.open(ctx, req, mediaType, 0, i == len(fallbackHosts)-1)
if err != nil {
if errdefs.IsNotFound(err) {
// Equivalent to empty referrers list
firstErr = err
break
}
log.G(ctx).WithError(err).WithField("host", host.Host).Debug("error fetching referrers via fallback")
if firstErr == nil {
firstErr = err
}
} else {
return rc, cl, nil
}
}
if firstErr == nil {
firstErr = fmt.Errorf("could not be found at any host: %w", errdefs.ErrNotFound)
}
return nil, 0, firstErr
}

View File

@@ -57,6 +57,10 @@ const (
// manifests // manifests
HostCapabilityPush HostCapabilityPush
// HostCapabilityReferrers represents the capability to generate a
// list of referrers using the OCI Distribution referrers endpoint.
HostCapabilityReferrers
// Reserved for future capabilities (i.e. search, catalog, remove) // Reserved for future capabilities (i.e. search, catalog, remove)
) )
@@ -168,7 +172,7 @@ func ConfigureDefaultRegistries(ropts ...RegistryOpt) RegistryHosts {
Host: host, Host: host,
Scheme: "https", Scheme: "https",
Path: "/v2", Path: "/v2",
Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush, Capabilities: HostCapabilityPull | HostCapabilityResolve | HostCapabilityPush | HostCapabilityReferrers,
} }
if config.Client == nil { if config.Client == nil {
@@ -212,10 +216,10 @@ func MatchAllHosts(string) (bool, error) {
// Note: this does not handle matching of ip addresses in octal, // Note: this does not handle matching of ip addresses in octal,
// decimal or hex form. // decimal or hex form.
func MatchLocalhost(host string) (bool, error) { func MatchLocalhost(host string) (bool, error) {
switch { switch host {
case host == "::1": case "::1":
return true, nil return true, nil
case host == "[::1]": case "[::1]":
return true, nil return true, nil
} }
h, p, err := net.SplitHostPort(host) h, p, err := net.SplitHostPort(host)

View File

@@ -551,29 +551,33 @@ func (r *request) authorize(ctx context.Context, req *http.Request) error {
return nil return nil
} }
func (r *request) addNamespace(ns string) (err error) { func (r *request) addQuery(key, value string) (err error) {
if !r.host.isProxy(ns) {
return nil
}
var q url.Values var q url.Values
// Parse query // Parse query
if i := strings.IndexByte(r.path, '?'); i > 0 { if p, query, ok := strings.Cut(r.path, "?"); ok {
r.path = r.path[:i+1] q, err = url.ParseQuery(query)
q, err = url.ParseQuery(r.path[i+1:])
if err != nil { if err != nil {
return return
} }
r.path = p + "?"
} else { } else {
r.path = r.path + "?" r.path = r.path + "?"
q = url.Values{} q = url.Values{}
} }
q.Add("ns", ns) q.Add(key, value)
r.path = r.path + q.Encode() r.path = r.path + q.Encode()
return return
} }
func (r *request) addNamespace(ns string) error {
if !r.host.isProxy(ns) {
return nil
}
return r.addQuery("ns", ns)
}
type request struct { type request struct {
method string method string
path string path string
@@ -659,9 +663,9 @@ func withErrorCheck(r *request, resp *http.Response) error {
var errContentRangeIgnored = errors.New("content range requests ignored") var errContentRangeIgnored = errors.New("content range requests ignored")
func withOffsetCheck(offset int64) doChecks { func withOffsetCheck(offset, parallelism int64) doChecks {
return func(r *request, resp *http.Response) error { return func(r *request, resp *http.Response) error {
if offset == 0 { if parallelism <= 1 && offset == 0 {
return nil return nil
} }
if resp.StatusCode == http.StatusPartialContent { if resp.StatusCode == http.StatusPartialContent {

View File

@@ -19,6 +19,7 @@ package remotes
import ( import (
"context" "context"
"io" "io"
"net/url"
"github.com/containerd/containerd/v2/core/content" "github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/transfer" "github.com/containerd/containerd/v2/core/transfer"
@@ -75,6 +76,10 @@ type FetcherByDigest interface {
FetchByDigest(ctx context.Context, dgst digest.Digest, opts ...FetchByDigestOpts) (io.ReadCloser, ocispec.Descriptor, error) FetchByDigest(ctx context.Context, dgst digest.Digest, opts ...FetchByDigestOpts) (io.ReadCloser, ocispec.Descriptor, error)
} }
type ReferrersFetcher interface {
FetchReferrers(ctx context.Context, dgst digest.Digest, opts ...FetchReferrersOpt) ([]ocispec.Descriptor, error)
}
// Pusher pushes content // Pusher pushes content
type Pusher interface { type Pusher interface {
// Push returns a content writer for the given resource identified // Push returns a content writer for the given resource identified
@@ -116,3 +121,32 @@ func WithMediaType(mediatype string) FetchByDigestOpts {
return nil return nil
} }
} }
type FetchReferrersConfig struct {
// ArtifactTypes specifies the artifact types to filter referrers, this can be
// applied to registry queries or filtering the results after fetching.
ArtifactTypes []string
// QueryFilters specifies additional filters which may get sent as query parameters
QueryFilters url.Values
}
type FetchReferrersOpt func(context.Context, *FetchReferrersConfig) error
// WithReferrerArtifactTypes sets the artifact types to filter referrers
func WithReferrerArtifactTypes(artifactTypes ...string) FetchReferrersOpt {
return func(ctx context.Context, cfg *FetchReferrersConfig) error {
cfg.ArtifactTypes = artifactTypes
return nil
}
}
// WithReferrerQueryFilter sets additional query filters for referrer fetching
func WithReferrerQueryFilter(param, value string) FetchReferrersOpt {
return func(ctx context.Context, cfg *FetchReferrersConfig) error {
if cfg.QueryFilters == nil {
cfg.QueryFilters = url.Values{}
}
cfg.QueryFilters.Add(param, value)
return nil
}
}

View File

@@ -29,4 +29,9 @@ const (
// DefaultSnapshotterNSLabel defines the namespace label to check for the // DefaultSnapshotterNSLabel defines the namespace label to check for the
// default snapshotter // default snapshotter
DefaultSnapshotterNSLabel = "containerd.io/defaults/snapshotter" DefaultSnapshotterNSLabel = "containerd.io/defaults/snapshotter"
// DefaultSandboxerNSLabel defines the namespace label to check for the
// default sandboxcr
DefaultSandboxerNSLabel = "containerd.io/defaults/sandboxer"
// DefaultSandboxer defines the default sandboxer to use for creating sandboxes.
DefaultSandboxer = "shim"
) )

View File

@@ -17,6 +17,23 @@
package defaults package defaults
const ( const (
// DefaultRuntime would be a multiple of choices, thus empty // DefaultRuntime is the default darwin runtime for running containers
DefaultRuntime = "" DefaultRuntime = "io.containerd.nerdbox.v1"
// DefaultAddress is the default unix socket address
DefaultAddress = "/var/run/containerd/containerd.sock"
// DefaultDebugAddress is the default unix socket address for pprof data
DefaultDebugAddress = "/var/run/containerd/debug.sock"
// DefaultFIFODir is the default location used by client-side cio library
// to store FIFOs.
DefaultFIFODir = "/var/run/containerd/fifo"
// DefaultSnapshotter will set the default snapshotter for the platform.
// Since mounts are not supported on Darwin, use erofs as the default
// which does not require mount support.
DefaultSnapshotter = "erofs"
// DefaultStateDir is the default location used by containerd to store
// transient data
DefaultStateDir = "/var/run/containerd"
// DefaultDiffer will set the default differ for the platform, use the
// erofs differ which does not require mount support.
DefaultDiffer = "erofs"
) )

View File

@@ -24,4 +24,7 @@ const (
// DefaultRootDir is the default location used by containerd to store // DefaultRootDir is the default location used by containerd to store
// persistent data // persistent data
DefaultRootDir = "/var/lib/containerd" DefaultRootDir = "/var/lib/containerd"
// DefaultConfigIncludePattern is the default location for drop-in configuration files.
DefaultConfigIncludePattern = "/etc/containerd/conf.d/*.toml"
) )

View File

@@ -1,4 +1,4 @@
//go:build unix && !linux //go:build unix && !linux && !darwin
/* /*
Copyright The containerd Authors. Copyright The containerd Authors.

View File

@@ -31,6 +31,9 @@ var (
// DefaultConfigDir is the default location for config files. // DefaultConfigDir is the default location for config files.
DefaultConfigDir = filepath.Join(os.Getenv("programfiles"), "containerd") DefaultConfigDir = filepath.Join(os.Getenv("programfiles"), "containerd")
// DefaultConfigIncludePattern is the default location for drop-in configuration files.
DefaultConfigIncludePattern = filepath.Join(os.Getenv("programfiles"), "containerd\\conf.d\\*.toml")
) )
const ( const (

View File

@@ -139,6 +139,9 @@ func (w *writer) Commit(ctx context.Context, size int64, expected digest.Digest,
return err return err
} }
if err := syncDir(filepath.Dir(target)); err != nil {
return err
}
// Enable content blob integrity verification if supported // Enable content blob integrity verification if supported
if w.s.integritySupported { if w.s.integritySupported {

View File

@@ -0,0 +1,37 @@
//go:build !windows
/*
Copyright The containerd 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 local
import (
"fmt"
"os"
)
func syncDir(dir string) error {
dirF, err := os.Open(dir)
if err != nil {
return fmt.Errorf("failed to open dir %s: %w", dir, err)
}
err = dirF.Sync()
dirF.Close()
if err != nil {
return fmt.Errorf("failed to sync dir %s: %w", dir, err)
}
return nil
}

View File

@@ -0,0 +1,22 @@
/*
Copyright The containerd 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 local
// sync dir doesn't support in windows
func syncDir(dir string) error {
return nil
}

View File

@@ -24,7 +24,7 @@ var (
Package = "github.com/containerd/containerd/v2" Package = "github.com/containerd/containerd/v2"
// Version holds the complete version number. Filled in at linking time. // Version holds the complete version number. Filled in at linking time.
Version = "2.1.3+unknown" Version = "2.2.0+unknown"
// Revision is filled with the VCS (e.g. git) revision being used to build // Revision is filled with the VCS (e.g. git) revision being used to build
// the program at linking time. // the program at linking time.

View File

@@ -38,5 +38,5 @@ func DefaultSpec() specs.Platform {
// Default returns the current platform's default platform specification. // Default returns the current platform's default platform specification.
func Default() MatchComparer { func Default() MatchComparer {
return Only(DefaultSpec()) return &windowsMatchComparer{Matcher: NewMatcher(DefaultSpec())}
} }

View File

@@ -42,18 +42,30 @@ const (
// rs5 (version 1809, codename "Redstone 5") corresponds to Windows Server // rs5 (version 1809, codename "Redstone 5") corresponds to Windows Server
// 2019 (ltsc2019), and Windows 10 (October 2018 Update). // 2019 (ltsc2019), and Windows 10 (October 2018 Update).
rs5 = 17763 rs5 = 17763
// ltsc2019 (Windows Server 2019) is an alias for [RS5].
ltsc2019 = rs5
// v21H2Server corresponds to Windows Server 2022 (ltsc2022). // v21H2Server corresponds to Windows Server 2022 (ltsc2022).
v21H2Server = 20348 v21H2Server = 20348
// ltsc2022 (Windows Server 2022) is an alias for [v21H2Server]
ltsc2022 = v21H2Server
// v22H2Win11 corresponds to Windows 11 (2022 Update). // v22H2Win11 corresponds to Windows 11 (2022 Update).
v22H2Win11 = 22621 v22H2Win11 = 22621
// v23H2 is the 23H2 release in the Windows Server annual channel.
v23H2 = 25398
// Windows Server 2025 build 26100
v25H1Server = 26100
ltsc2025 = v25H1Server
) )
// List of stable ABI compliant ltsc releases // List of stable ABI compliant ltsc releases
// Note: List must be sorted in ascending order // Note: List must be sorted in ascending order
var compatLTSCReleases = []uint16{ var compatLTSCReleases = []uint16{
v21H2Server, ltsc2022,
ltsc2025,
} }
// CheckHostAndContainerCompat checks if given host and container // CheckHostAndContainerCompat checks if given host and container
@@ -70,18 +82,27 @@ func checkWindowsHostAndContainerCompat(host, ctr windowsOSVersion) bool {
} }
// If host is < WS 2022, exact version match is required // If host is < WS 2022, exact version match is required
if host.Build < v21H2Server { if host.Build < ltsc2022 {
return host.Build == ctr.Build return host.Build == ctr.Build
} }
var supportedLtscRelease uint16 // Find the latest LTSC version that is earlier than the host version.
// This is the earliest version of container that the host can run.
//
// If the host version is an LTSC, then it supports compatibility with
// everything from the previous LTSC up to itself, so we want supportedLTSCRelease
// to be the previous entry.
//
// If no match is found, then we know that the host is LTSC2022 exactly,
// since we already checked that it's not less than LTSC2022.
var supportedLTSCRelease uint16 = ltsc2022
for i := len(compatLTSCReleases) - 1; i >= 0; i-- { for i := len(compatLTSCReleases) - 1; i >= 0; i-- {
if host.Build >= compatLTSCReleases[i] { if host.Build > compatLTSCReleases[i] {
supportedLtscRelease = compatLTSCReleases[i] supportedLTSCRelease = compatLTSCReleases[i]
break break
} }
} }
return ctr.Build >= supportedLtscRelease && ctr.Build <= host.Build return supportedLTSCRelease <= ctr.Build && ctr.Build <= host.Build
} }
func getWindowsOSVersion(osVersionPrefix string) windowsOSVersion { func getWindowsOSVersion(osVersionPrefix string) windowsOSVersion {
@@ -114,18 +135,6 @@ func getWindowsOSVersion(osVersionPrefix string) windowsOSVersion {
} }
} }
func winRevision(v string) int {
parts := strings.Split(v, ".")
if len(parts) < 4 {
return 0
}
r, err := strconv.Atoi(parts[3])
if err != nil {
return 0
}
return r
}
type windowsVersionMatcher struct { type windowsVersionMatcher struct {
windowsOSVersion windowsOSVersion
} }
@@ -149,8 +158,7 @@ type windowsMatchComparer struct {
func (c *windowsMatchComparer) Less(p1, p2 specs.Platform) bool { func (c *windowsMatchComparer) Less(p1, p2 specs.Platform) bool {
m1, m2 := c.Match(p1), c.Match(p2) m1, m2 := c.Match(p1), c.Match(p2)
if m1 && m2 { if m1 && m2 {
r1, r2 := winRevision(p1.OSVersion), winRevision(p2.OSVersion) return p1.OSVersion > p2.OSVersion
return r1 > r2
} }
return m1 && !m2 return m1 && !m2
} }

View File

@@ -408,11 +408,11 @@ func readerFromEntries(entries ...*entry) io.Reader {
defer tw.Close() defer tw.Close()
for _, entry := range entries { for _, entry := range entries {
if err := tw.WriteHeader(entry.header); err != nil { if err := tw.WriteHeader(entry.header); err != nil {
pw.CloseWithError(fmt.Errorf("Failed to write tar header: %v", err)) pw.CloseWithError(fmt.Errorf("failed to write tar header: %v", err))
return return
} }
if _, err := io.Copy(tw, entry.payload); err != nil { if _, err := io.Copy(tw, entry.payload); err != nil {
pw.CloseWithError(fmt.Errorf("Failed to write tar payload: %v", err)) pw.CloseWithError(fmt.Errorf("failed to write tar payload: %v", err))
return return
} }
} }
@@ -627,12 +627,12 @@ func (cr *countReadSeeker) Seek(offset int64, whence int) (int64, error) {
switch whence { switch whence {
default: default:
return 0, fmt.Errorf("Unknown whence: %v", whence) return 0, fmt.Errorf("unknown whence: %v", whence)
case io.SeekStart: case io.SeekStart:
case io.SeekCurrent: case io.SeekCurrent:
offset += *cr.cPos offset += *cr.cPos
case io.SeekEnd: case io.SeekEnd:
return 0, fmt.Errorf("Unsupported whence: %v", whence) return 0, fmt.Errorf("unsupported whence: %v", whence)
} }
if offset < 0 { if offset < 0 {

View File

@@ -109,7 +109,7 @@ func gzipFooterBytes(tocOff int64) []byte {
header[0], header[1] = 'S', 'G' header[0], header[1] = 'S', 'G'
subfield := fmt.Sprintf("%016xSTARGZ", tocOff) subfield := fmt.Sprintf("%016xSTARGZ", tocOff)
binary.LittleEndian.PutUint16(header[2:4], uint16(len(subfield))) // little-endian per RFC1952 binary.LittleEndian.PutUint16(header[2:4], uint16(len(subfield))) // little-endian per RFC1952
gz.Header.Extra = append(header, []byte(subfield)...) gz.Extra = append(header, []byte(subfield)...)
gz.Close() gz.Close()
if buf.Len() != FooterSize { if buf.Len() != FooterSize {
panic(fmt.Sprintf("footer buffer = %d, not %d", buf.Len(), FooterSize)) panic(fmt.Sprintf("footer buffer = %d, not %d", buf.Len(), FooterSize))
@@ -136,7 +136,7 @@ func (gz *GzipDecompressor) ParseFooter(p []byte) (blobPayloadSize, tocOffset, t
return 0, 0, 0, err return 0, 0, 0, err
} }
defer zr.Close() defer zr.Close()
extra := zr.Header.Extra extra := zr.Extra
si1, si2, subfieldlen, subfield := extra[0], extra[1], extra[2:4], extra[4:] si1, si2, subfieldlen, subfield := extra[0], extra[1], extra[2:4], extra[4:]
if si1 != 'S' || si2 != 'G' { if si1 != 'S' || si2 != 'G' {
return 0, 0, 0, fmt.Errorf("invalid subfield IDs: %q, %q; want E, S", si1, si2) return 0, 0, 0, fmt.Errorf("invalid subfield IDs: %q, %q; want E, S", si1, si2)
@@ -181,7 +181,7 @@ func (gz *LegacyGzipDecompressor) ParseFooter(p []byte) (blobPayloadSize, tocOff
return 0, 0, 0, fmt.Errorf("legacy: failed to get footer gzip reader: %w", err) return 0, 0, 0, fmt.Errorf("legacy: failed to get footer gzip reader: %w", err)
} }
defer zr.Close() defer zr.Close()
extra := zr.Header.Extra extra := zr.Extra
if len(extra) != 16+len("STARGZ") { if len(extra) != 16+len("STARGZ") {
return 0, 0, 0, fmt.Errorf("legacy: invalid stargz's extra field size") return 0, 0, 0, fmt.Errorf("legacy: invalid stargz's extra field size")
} }

View File

@@ -357,14 +357,15 @@ func compressBlob(t *testing.T, src *io.SectionReader, srcCompression int) *io.S
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
var w io.WriteCloser var w io.WriteCloser
var err error var err error
if srcCompression == gzipType { switch srcCompression {
case gzipType:
w = gzip.NewWriter(buf) w = gzip.NewWriter(buf)
} else if srcCompression == zstdType { case zstdType:
w, err = zstd.NewWriter(buf) w, err = zstd.NewWriter(buf)
if err != nil { if err != nil {
t.Fatalf("failed to init zstd writer: %v", err) t.Fatalf("failed to init zstd writer: %v", err)
} }
} else { default:
return src return src
} }
src.Seek(0, io.SeekStart) src.Seek(0, io.SeekStart)
@@ -445,7 +446,7 @@ func contains(t *testing.T, a, b stargzEntry) bool {
bbytes, bnext, bok := readOffset(t, bf, nr, b) bbytes, bnext, bok := readOffset(t, bf, nr, b)
if !aok && !bok { if !aok && !bok {
break break
} else if !(aok && bok) || anext != bnext { } else if !aok || !bok || anext != bnext {
t.Logf("%q != %q (offset=%d): chunk existence a=%v vs b=%v, anext=%v vs bnext=%v", t.Logf("%q != %q (offset=%d): chunk existence a=%v vs b=%v, anext=%v vs bnext=%v",
ae.Name, be.Name, nr, aok, bok, anext, bnext) ae.Name, be.Name, nr, aok, bok, anext, bnext)
return false return false
@@ -2346,8 +2347,8 @@ func CheckGzipHasStreams(t *testing.T, b []byte, streams []int64) {
t.Fatalf("countStreams(gzip), Copy: %v", err) t.Fatalf("countStreams(gzip), Copy: %v", err)
} }
var extra string var extra string
if len(zr.Header.Extra) > 0 { if len(zr.Extra) > 0 {
extra = fmt.Sprintf("; extra=%q", zr.Header.Extra) extra = fmt.Sprintf("; extra=%q", zr.Extra)
} }
t.Logf(" [%d] at %d in stargz, uncompressed length %d%s", numStreams, zoff, n, extra) t.Logf(" [%d] at %d in stargz, uncompressed length %d%s", numStreams, zoff, n, extra)
delete(wants, int64(zoff)) delete(wants, int64(zoff))

View File

@@ -48,9 +48,7 @@ type Cli interface {
Apply(ops ...CLIOption) error Apply(ops ...CLIOption) error
config.Provider config.Provider
ServerInfo() ServerInfo ServerInfo() ServerInfo
DefaultVersion() string
CurrentVersion() string CurrentVersion() string
ContentTrustEnabled() bool
BuildKitEnabled() (bool, error) BuildKitEnabled() (bool, error)
ContextStore() store.Store ContextStore() store.Store
CurrentContext() string CurrentContext() string
@@ -78,6 +76,7 @@ type DockerCli struct {
dockerEndpoint docker.Endpoint dockerEndpoint docker.Endpoint
contextStoreConfig *store.Config contextStoreConfig *store.Config
initTimeout time.Duration initTimeout time.Duration
userAgent string
res telemetryResource res telemetryResource
// baseCtx is the base context used for internal operations. In the future // baseCtx is the base context used for internal operations. In the future
@@ -89,6 +88,8 @@ type DockerCli struct {
} }
// DefaultVersion returns [api.DefaultVersion]. // DefaultVersion returns [api.DefaultVersion].
//
// Deprecated: this function is no longer used and will be removed in the next release.
func (*DockerCli) DefaultVersion() string { func (*DockerCli) DefaultVersion() string {
return api.DefaultVersion return api.DefaultVersion
} }
@@ -159,6 +160,8 @@ func (cli *DockerCli) ServerInfo() ServerInfo {
// ContentTrustEnabled returns whether content trust has been enabled by an // ContentTrustEnabled returns whether content trust has been enabled by an
// environment variable. // environment variable.
//
// Deprecated: check the value of the DOCKER_CONTENT_TRUST environment variable to detect whether content-trust is enabled.
func (cli *DockerCli) ContentTrustEnabled() bool { func (cli *DockerCli) ContentTrustEnabled() bool {
return cli.contentTrust return cli.contentTrust
} }
@@ -269,7 +272,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
cli.contextStore = &ContextStoreWithDefault{ cli.contextStore = &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig), Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig),
Resolver: func() (*DefaultContext, error) { Resolver: func() (*DefaultContext, error) {
return ResolveDefaultContext(cli.options, *cli.contextStoreConfig) return resolveDefaultContext(cli.options, *cli.contextStoreConfig)
}, },
} }
@@ -282,6 +285,17 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
} }
filterResourceAttributesEnvvar() filterResourceAttributesEnvvar()
// early return if GODEBUG is already set or the docker context is
// the default context, i.e. is a virtual context where we won't override
// any GODEBUG values.
if v := os.Getenv("GODEBUG"); cli.currentContext == DefaultContextName || v != "" {
return nil
}
meta, err := cli.contextStore.GetMetadata(cli.currentContext)
if err == nil {
setGoDebug(meta)
}
return nil return nil
} }
@@ -295,17 +309,17 @@ func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.
contextStore := &ContextStoreWithDefault{ contextStore := &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), storeConfig), Store: store.New(config.ContextStoreDir(), storeConfig),
Resolver: func() (*DefaultContext, error) { Resolver: func() (*DefaultContext, error) {
return ResolveDefaultContext(opts, storeConfig) return resolveDefaultContext(opts, storeConfig)
}, },
} }
endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile)) endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile))
if err != nil { if err != nil {
return nil, errors.Wrap(err, "unable to resolve docker endpoint") return nil, errors.Wrap(err, "unable to resolve docker endpoint")
} }
return newAPIClientFromEndpoint(endpoint, configFile) return newAPIClientFromEndpoint(endpoint, configFile, client.WithUserAgent(UserAgent()))
} }
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) { func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile, extraOpts ...client.Opt) (client.APIClient, error) {
opts, err := ep.ClientOpts() opts, err := ep.ClientOpts()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -313,7 +327,14 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
if len(configFile.HTTPHeaders) > 0 { if len(configFile.HTTPHeaders) > 0 {
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders)) opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
} }
opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent())) withCustomHeaders, err := withCustomHeadersFromEnv()
if err != nil {
return nil, err
}
if withCustomHeaders != nil {
opts = append(opts, withCustomHeaders)
}
opts = append(opts, extraOpts...)
return client.NewClientWithOpts(opts...) return client.NewClientWithOpts(opts...)
} }
@@ -475,6 +496,57 @@ func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) {
return resolveDockerEndpoint(cli.contextStore, cn) return resolveDockerEndpoint(cli.contextStore, cn)
} }
// setGoDebug is an escape hatch that sets the GODEBUG environment
// variable value using docker context metadata.
//
// {
// "Name": "my-context",
// "Metadata": { "GODEBUG": "x509negativeserial=1" }
// }
//
// WARNING: Setting x509negativeserial=1 allows Go's x509 library to accept
// X.509 certificates with negative serial numbers.
// This behavior is deprecated and non-compliant with current security
// standards (RFC 5280). Accepting negative serial numbers can introduce
// serious security vulnerabilities, including the risk of certificate
// collision or bypass attacks.
// This option should only be used for legacy compatibility and never in
// production environments.
// Use at your own risk.
func setGoDebug(meta store.Metadata) {
fieldName := "GODEBUG"
godebugEnv := os.Getenv(fieldName)
// early return if GODEBUG is already set. We don't want to override what
// the user already sets.
if godebugEnv != "" {
return
}
var cfg any
var ok bool
switch m := meta.Metadata.(type) {
case DockerContext:
cfg, ok = m.AdditionalFields[fieldName]
if !ok {
return
}
case map[string]any:
cfg, ok = m[fieldName]
if !ok {
return
}
default:
return
}
v, ok := cfg.(string)
if !ok {
return
}
// set the GODEBUG environment variable with whatever was in the context
_ = os.Setenv(fieldName, v)
}
func (cli *DockerCli) initialize() error { func (cli *DockerCli) initialize() error {
cli.init.Do(func() { cli.init.Do(func() {
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint() cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
@@ -483,7 +555,8 @@ func (cli *DockerCli) initialize() error {
return return
} }
if cli.client == nil { if cli.client == nil {
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil { ops := []client.Opt{client.WithUserAgent(cli.userAgent)}
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, ops...); cli.initErr != nil {
return return
} }
} }
@@ -496,6 +569,8 @@ func (cli *DockerCli) initialize() error {
} }
// Apply all the operation on the cli // Apply all the operation on the cli
//
// Deprecated: this method is no longer used and will be removed in the next release if there are no remaining users.
func (cli *DockerCli) Apply(ops ...CLIOption) error { func (cli *DockerCli) Apply(ops ...CLIOption) error {
for _, op := range ops { for _, op := range ops {
if err := op(cli); err != nil { if err := op(cli); err != nil {
@@ -527,15 +602,18 @@ type ServerInfo struct {
// environment. // environment.
func NewDockerCli(ops ...CLIOption) (*DockerCli, error) { func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
defaultOps := []CLIOption{ defaultOps := []CLIOption{
WithContentTrustFromEnv(), withContentTrustFromEnv(),
WithDefaultContextStoreConfig(), WithDefaultContextStoreConfig(),
WithStandardStreams(), WithStandardStreams(),
WithUserAgent(UserAgent()),
} }
ops = append(defaultOps, ops...) ops = append(defaultOps, ops...)
cli := &DockerCli{baseCtx: context.Background()} cli := &DockerCli{baseCtx: context.Background()}
if err := cli.Apply(ops...); err != nil { for _, op := range ops {
return nil, err if err := op(cli); err != nil {
return nil, err
}
} }
return cli, nil return cli, nil
} }
@@ -551,7 +629,7 @@ func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
} }
} }
// UserAgent returns the user agent string used for making API requests // UserAgent returns the default user agent string used for making API requests.
func UserAgent() string { func UserAgent() string {
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")" return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
} }

View File

@@ -75,8 +75,8 @@ func WithErrorStream(err io.Writer) CLIOption {
} }
} }
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value. // withContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
func WithContentTrustFromEnv() CLIOption { func withContentTrustFromEnv() CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.contentTrust = false cli.contentTrust = false
if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
@@ -89,7 +89,16 @@ func WithContentTrustFromEnv() CLIOption {
} }
} }
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
//
// Deprecated: this option is no longer used, and will be removed in the next release.
func WithContentTrustFromEnv() CLIOption {
return withContentTrustFromEnv()
}
// WithContentTrust enables content trust on a cli. // WithContentTrust enables content trust on a cli.
//
// Deprecated: this option is no longer used, and will be removed in the next release.
func WithContentTrust(enabled bool) CLIOption { func WithContentTrust(enabled bool) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.contentTrust = enabled cli.contentTrust = enabled
@@ -180,61 +189,70 @@ const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS"
// override headers with the same name). // override headers with the same name).
// //
// TODO(thaJeztah): this is a client Option, and should be moved to the client. It is non-exported for that reason. // TODO(thaJeztah): this is a client Option, and should be moved to the client. It is non-exported for that reason.
func withCustomHeadersFromEnv() client.Opt { func withCustomHeadersFromEnv() (client.Opt, error) {
return func(apiClient *client.Client) error { value := os.Getenv(envOverrideHTTPHeaders)
value := os.Getenv(envOverrideHTTPHeaders) if value == "" {
if value == "" { return nil, nil
return nil }
} csvReader := csv.NewReader(strings.NewReader(value))
csvReader := csv.NewReader(strings.NewReader(value)) fields, err := csvReader.Read()
fields, err := csvReader.Read() if err != nil {
if err != nil { return nil, invalidParameter(errors.Errorf(
return invalidParameter(errors.Errorf( "failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs", envOverrideHTTPHeaders,
envOverrideHTTPHeaders, ))
}
if len(fields) == 0 {
return nil, nil
}
env := map[string]string{}
for _, kv := range fields {
k, v, hasValue := strings.Cut(kv, "=")
// Only strip whitespace in keys; preserve whitespace in values.
k = strings.TrimSpace(k)
if k == "" {
return nil, invalidParameter(errors.Errorf(
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
envOverrideHTTPHeaders, kv,
)) ))
} }
if len(fields) == 0 {
return nil // We don't currently allow empty key=value pairs, and produce an error.
// This is something we could allow in future (e.g. to read value
// from an environment variable with the same name). In the meantime,
// produce an error to prevent users from depending on this.
if !hasValue {
return nil, invalidParameter(errors.Errorf(
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
envOverrideHTTPHeaders, kv,
))
} }
env := map[string]string{} env[http.CanonicalHeaderKey(k)] = v
for _, kv := range fields { }
k, v, hasValue := strings.Cut(kv, "=")
// Only strip whitespace in keys; preserve whitespace in values. if len(env) == 0 {
k = strings.TrimSpace(k) // We should probably not hit this case, as we don't skip values
// (only return errors), but we don't want to discard existing
// headers with an empty set.
return nil, nil
}
if k == "" { // TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
return invalidParameter(errors.Errorf( // see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`, return client.WithHTTPHeaders(env), nil
envOverrideHTTPHeaders, kv, }
))
}
// We don't currently allow empty key=value pairs, and produce an error. // WithUserAgent configures the User-Agent string for cli HTTP requests.
// This is something we could allow in future (e.g. to read value func WithUserAgent(userAgent string) CLIOption {
// from an environment variable with the same name). In the meantime, return func(cli *DockerCli) error {
// produce an error to prevent users from depending on this. if userAgent == "" {
if !hasValue { return errors.New("user agent cannot be blank")
return invalidParameter(errors.Errorf(
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
envOverrideHTTPHeaders, kv,
))
}
env[http.CanonicalHeaderKey(k)] = v
} }
cli.userAgent = userAgent
if len(env) == 0 { return nil
// We should probably not hit this case, as we don't skip values
// (only return errors), but we don't want to discard existing
// headers with an empty set.
return nil
}
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
return client.WithHTTPHeaders(env)(apiClient)
} }
} }

View File

@@ -52,7 +52,14 @@ type EndpointDefaultResolver interface {
} }
// ResolveDefaultContext creates a Metadata for the current CLI invocation parameters // ResolveDefaultContext creates a Metadata for the current CLI invocation parameters
//
// Deprecated: this function is exported for testing and meant for internal use. It will be removed in the next release.
func ResolveDefaultContext(opts *cliflags.ClientOptions, config store.Config) (*DefaultContext, error) { func ResolveDefaultContext(opts *cliflags.ClientOptions, config store.Config) (*DefaultContext, error) {
return resolveDefaultContext(opts, config)
}
// resolveDefaultContext creates a Metadata for the current CLI invocation parameters
func resolveDefaultContext(opts *cliflags.ClientOptions, config store.Config) (*DefaultContext, error) {
contextTLSData := store.ContextTLSData{ contextTLSData := store.ContextTLSData{
Endpoints: make(map[string]store.EndpointTLSData), Endpoints: make(map[string]store.EndpointTLSData),
} }

View File

@@ -31,11 +31,13 @@ const (
// authConfigKey is the key used to store credentials for Docker Hub. It is // authConfigKey is the key used to store credentials for Docker Hub. It is
// a copy of [registry.IndexServer]. // a copy of [registry.IndexServer].
// //
// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker/registry#IndexServer // [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer
const authConfigKey = "https://index.docker.io/v1/" const authConfigKey = "https://index.docker.io/v1/"
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info // RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
// for the given command. // for the given command to prompt the user for username and password.
//
// Deprecated: this function is no longer used and will be removed in the next release.
func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig { func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig {
configKey := getAuthConfigKey(index.Name) configKey := getAuthConfigKey(index.Name)
isDefaultRegistry := configKey == authConfigKey || index.Official isDefaultRegistry := configKey == authConfigKey || index.Official
@@ -43,7 +45,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
_, _ = fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName) _, _ = fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName)
authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, configKey, isDefaultRegistry) authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, configKey, isDefaultRegistry)
if err != nil { if err != nil {
_, _ = fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", authConfigKey, err) _, _ = fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", configKey, err)
} }
select { select {
@@ -52,7 +54,7 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
default: default:
} }
authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, authConfigKey) authConfig, err = PromptUserForCredentials(ctx, cli, "", "", authConfig.Username, configKey)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -66,6 +68,8 @@ func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInf
// //
// It is similar to [registry.ResolveAuthConfig], but uses the credentials- // It is similar to [registry.ResolveAuthConfig], but uses the credentials-
// store, instead of looking up credentials from a map. // store, instead of looking up credentials from a map.
//
// [registry.ResolveAuthConfig]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#ResolveAuthConfig
func ResolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInfo) registrytypes.AuthConfig { func ResolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
configKey := index.Name configKey := index.Name
if index.Official { if index.Official {
@@ -73,7 +77,16 @@ func ResolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInf
} }
a, _ := cfg.GetAuthConfig(configKey) a, _ := cfg.GetAuthConfig(configKey)
return registrytypes.AuthConfig(a) return registrytypes.AuthConfig{
Username: a.Username,
Password: a.Password,
ServerAddress: a.ServerAddress,
// TODO(thaJeztah): Are these expected to be included?
Auth: a.Auth,
IdentityToken: a.IdentityToken,
RegistryToken: a.RegistryToken,
}
} }
// GetDefaultAuthConfig gets the default auth config given a serverAddress // GetDefaultAuthConfig gets the default auth config given a serverAddress
@@ -82,36 +95,27 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
if !isDefaultRegistry { if !isDefaultRegistry {
serverAddress = credentials.ConvertToHostname(serverAddress) serverAddress = credentials.ConvertToHostname(serverAddress)
} }
authconfig := configtypes.AuthConfig{} authCfg := configtypes.AuthConfig{}
var err error var err error
if checkCredStore { if checkCredStore {
authconfig, err = cfg.GetAuthConfig(serverAddress) authCfg, err = cfg.GetAuthConfig(serverAddress)
if err != nil { if err != nil {
return registrytypes.AuthConfig{ return registrytypes.AuthConfig{
ServerAddress: serverAddress, ServerAddress: serverAddress,
}, err }, err
} }
} }
authconfig.ServerAddress = serverAddress
authconfig.IdentityToken = ""
return registrytypes.AuthConfig(authconfig), nil
}
// ConfigureAuth handles prompting of user's username and password if needed. return registrytypes.AuthConfig{
// Username: authCfg.Username,
// Deprecated: use [PromptUserForCredentials] instead. Password: authCfg.Password,
func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, authConfig *registrytypes.AuthConfig, _ bool) error { ServerAddress: serverAddress,
defaultUsername := authConfig.Username
serverAddress := authConfig.ServerAddress
newAuthConfig, err := PromptUserForCredentials(ctx, cli, flUser, flPassword, defaultUsername, serverAddress) // TODO(thaJeztah): Are these expected to be included?
if err != nil { Auth: authCfg.Auth,
return err IdentityToken: "",
} RegistryToken: authCfg.RegistryToken,
}, nil
authConfig.Username = newAuthConfig.Username
authConfig.Password = newAuthConfig.Password
return nil
} }
// PromptUserForCredentials handles the CLI prompt for the user to input // PromptUserForCredentials handles the CLI prompt for the user to input
@@ -209,47 +213,47 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
}, nil }, nil
} }
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete // RetrieveAuthTokenFromImage retrieves an encoded auth token given a
// image. The auth configuration is serialized as a base64url encoded RFC4648, // complete image reference. The auth configuration is serialized as a
// section 5) JSON string for sending through the X-Registry-Auth header. // base64url encoded ([RFC 4648, Section 5]) JSON string for sending through
// the "X-Registry-Auth" header.
// //
// For details on base64url encoding, see: // [RFC 4648, Section 5]: https://tools.ietf.org/html/rfc4648#section-5
// - RFC4648, section 5: https://tools.ietf.org/html/rfc4648#section-5
func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (string, error) { func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (string, error) {
// Retrieve encoded auth token from the image reference registryRef, err := reference.ParseNormalizedNamed(image)
authConfig, err := resolveAuthConfigFromImage(cfg, image)
if err != nil { if err != nil {
return "", err return "", err
} }
encodedAuth, err := registrytypes.EncodeAuthConfig(authConfig) configKey := getAuthConfigKey(reference.Domain(registryRef))
authConfig, err := cfg.GetAuthConfig(configKey)
if err != nil {
return "", err
}
encodedAuth, err := registrytypes.EncodeAuthConfig(registrytypes.AuthConfig{
Username: authConfig.Username,
Password: authConfig.Password,
ServerAddress: authConfig.ServerAddress,
// TODO(thaJeztah): Are these expected to be included?
Auth: authConfig.Auth,
IdentityToken: authConfig.IdentityToken,
RegistryToken: authConfig.RegistryToken,
})
if err != nil { if err != nil {
return "", err return "", err
} }
return encodedAuth, nil return encodedAuth, nil
} }
// resolveAuthConfigFromImage retrieves that AuthConfig using the image string
func resolveAuthConfigFromImage(cfg *configfile.ConfigFile, image string) (registrytypes.AuthConfig, error) {
registryRef, err := reference.ParseNormalizedNamed(image)
if err != nil {
return registrytypes.AuthConfig{}, err
}
configKey := getAuthConfigKey(reference.Domain(registryRef))
a, err := cfg.GetAuthConfig(configKey)
if err != nil {
return registrytypes.AuthConfig{}, err
}
return registrytypes.AuthConfig(a), nil
}
// getAuthConfigKey special-cases using the full index address of the official // getAuthConfigKey special-cases using the full index address of the official
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes. // index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
// //
// It is similar to [registry.GetAuthConfigKey], but does not require on // It is similar to [registry.GetAuthConfigKey], but does not require on
// [registrytypes.IndexInfo] as intermediate. // [registrytypes.IndexInfo] as intermediate.
// //
// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker/registry#GetAuthConfigKey // [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#GetAuthConfigKey
// [registrytypes.IndexInfo]:https://pkg.go.dev/github.com/docker/docker/api/types/registry#IndexInfo // [registrytypes.IndexInfo]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/api/types/registry#IndexInfo
func getAuthConfigKey(domainName string) string { func getAuthConfigKey(domainName string) string {
if domainName == "docker.io" || domainName == "index.docker.io" { if domainName == "docker.io" || domainName == "index.docker.io" {
return authConfigKey return authConfigKey

View File

@@ -1,15 +0,0 @@
package command
import (
"github.com/spf13/pflag"
)
// AddTrustVerificationFlags adds content trust flags to the provided flagset
func AddTrustVerificationFlags(fs *pflag.FlagSet, v *bool, trusted bool) {
fs.BoolVar(v, "disable-content-trust", !trusted, "Skip image verification")
}
// AddTrustSigningFlags adds "signing" flags to the provided flagset
func AddTrustSigningFlags(fs *pflag.FlagSet, v *bool, trusted bool) {
fs.BoolVar(v, "disable-content-trust", !trusted, "Skip image signing")
}

View File

@@ -14,30 +14,20 @@ import (
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/prompt" "github.com/docker/cli/internal/prompt"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/moby/sys/atomicwriter"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/pflag"
) )
// CopyToFile writes the content of the reader to the specified file // ErrPromptTerminated is returned if the user terminated the prompt.
// //
// Deprecated: use [atomicwriter.New]. // Deprecated: this error is for internal use and will be removed in the next release.
func CopyToFile(outfile string, r io.Reader) error {
writer, err := atomicwriter.New(outfile, 0o600)
if err != nil {
return err
}
defer writer.Close()
_, err = io.Copy(writer, r)
return err
}
const ErrPromptTerminated = prompt.ErrTerminated const ErrPromptTerminated = prompt.ErrTerminated
// DisableInputEcho disables input echo on the provided streams.In. // DisableInputEcho disables input echo on the provided streams.In.
// This is useful when the user provides sensitive information like passwords. // This is useful when the user provides sensitive information like passwords.
// The function returns a restore function that should be called to restore the // The function returns a restore function that should be called to restore the
// terminal state. // terminal state.
//
// Deprecated: this function is for internal use and will be removed in the next release.
func DisableInputEcho(ins *streams.In) (restore func() error, err error) { func DisableInputEcho(ins *streams.In) (restore func() error, err error) {
return prompt.DisableInputEcho(ins) return prompt.DisableInputEcho(ins)
} }
@@ -49,6 +39,8 @@ func DisableInputEcho(ins *streams.In) (restore func() error, err error) {
// When the prompt returns an error, the caller should propagate the error up // When the prompt returns an error, the caller should propagate the error up
// the stack and close the io.Reader used for the prompt which will prevent the // the stack and close the io.Reader used for the prompt which will prevent the
// background goroutine from blocking indefinitely. // background goroutine from blocking indefinitely.
//
// Deprecated: this function is for internal use and will be removed in the next release.
func PromptForInput(ctx context.Context, in io.Reader, out io.Writer, message string) (string, error) { func PromptForInput(ctx context.Context, in io.Reader, out io.Writer, message string) (string, error) {
return prompt.ReadInput(ctx, in, out, message) return prompt.ReadInput(ctx, in, out, message)
} }
@@ -63,6 +55,8 @@ func PromptForInput(ctx context.Context, in io.Reader, out io.Writer, message st
// When the prompt returns an error, the caller should propagate the error up // When the prompt returns an error, the caller should propagate the error up
// the stack and close the io.Reader used for the prompt which will prevent the // the stack and close the io.Reader used for the prompt which will prevent the
// background goroutine from blocking indefinitely. // background goroutine from blocking indefinitely.
//
// Deprecated: this function is for internal use and will be removed in the next release.
func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, message string) (bool, error) { func PromptForConfirmation(ctx context.Context, ins io.Reader, outs io.Writer, message string) (bool, error) {
return prompt.Confirm(ctx, ins, outs, message) return prompt.Confirm(ctx, ins, outs, message)
} }
@@ -108,12 +102,6 @@ func PruneFilters(dockerCLI config.Provider, pruneFilters filters.Args) filters.
return pruneFilters return pruneFilters
} }
// AddPlatformFlag adds `platform` to a set of flags for API version 1.32 and later.
func AddPlatformFlag(flags *pflag.FlagSet, target *string) {
flags.StringVar(target, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
}
// ValidateOutputPath validates the output paths of the "docker cp" command. // ValidateOutputPath validates the output paths of the "docker cp" command.
func ValidateOutputPath(path string) error { func ValidateOutputPath(path string) error {
dir := filepath.Dir(filepath.Clean(path)) dir := filepath.Dir(filepath.Clean(path))

View File

@@ -3,12 +3,14 @@ package configfile
import ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/docker/cli/cli/config/credentials" "github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/memorystore"
"github.com/docker/cli/cli/config/types" "github.com/docker/cli/cli/config/types"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@@ -46,6 +48,31 @@ type ConfigFile struct {
Experimental string `json:"experimental,omitempty"` Experimental string `json:"experimental,omitempty"`
} }
type configEnvAuth struct {
Auth string `json:"auth"`
}
type configEnv struct {
AuthConfigs map[string]configEnvAuth `json:"auths"`
}
// DockerEnvConfigKey is an environment variable that contains a JSON encoded
// credential config. It only supports storing the credentials as a base64
// encoded string in the format base64("username:pat").
//
// Adding additional fields will produce a parsing error.
//
// Example:
//
// {
// "auths": {
// "example.test": {
// "auth": base64-encoded-username-pat
// }
// }
// }
const DockerEnvConfigKey = "DOCKER_AUTH_CONFIG"
// ProxyConfig contains proxy configuration settings // ProxyConfig contains proxy configuration settings
type ProxyConfig struct { type ProxyConfig struct {
HTTPProxy string `json:"httpProxy,omitempty"` HTTPProxy string `json:"httpProxy,omitempty"`
@@ -263,10 +290,64 @@ func decodeAuth(authStr string) (string, string, error) {
// GetCredentialsStore returns a new credentials store from the settings in the // GetCredentialsStore returns a new credentials store from the settings in the
// configuration file // configuration file
func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) credentials.Store { func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) credentials.Store {
store := credentials.NewFileStore(configFile)
if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" { if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" {
return newNativeStore(configFile, helper) store = newNativeStore(configFile, helper)
} }
return credentials.NewFileStore(configFile)
envConfig := os.Getenv(DockerEnvConfigKey)
if envConfig == "" {
return store
}
authConfig, err := parseEnvConfig(envConfig)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to create credential store from DOCKER_AUTH_CONFIG: ", err)
return store
}
// use DOCKER_AUTH_CONFIG if set
// it uses the native or file store as a fallback to fetch and store credentials
envStore, err := memorystore.New(
memorystore.WithAuthConfig(authConfig),
memorystore.WithFallbackStore(store),
)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "Failed to create credential store from DOCKER_AUTH_CONFIG: ", err)
return store
}
return envStore
}
func parseEnvConfig(v string) (map[string]types.AuthConfig, error) {
envConfig := &configEnv{}
decoder := json.NewDecoder(strings.NewReader(v))
decoder.DisallowUnknownFields()
if err := decoder.Decode(envConfig); err != nil && !errors.Is(err, io.EOF) {
return nil, err
}
if decoder.More() {
return nil, errors.New("DOCKER_AUTH_CONFIG does not support more than one JSON object")
}
authConfigs := make(map[string]types.AuthConfig)
for addr, envAuth := range envConfig.AuthConfigs {
if envAuth.Auth == "" {
return nil, fmt.Errorf("DOCKER_AUTH_CONFIG environment variable is missing key `auth` for %s", addr)
}
username, password, err := decodeAuth(envAuth.Auth)
if err != nil {
return nil, err
}
authConfigs[addr] = types.AuthConfig{
Username: username,
Password: password,
ServerAddress: addr,
}
}
return authConfigs, nil
} }
// var for unit testing. // var for unit testing.

View File

@@ -0,0 +1,130 @@
//go:build go1.23
package memorystore
import (
"fmt"
"maps"
"os"
"sync"
"github.com/docker/cli/cli/config/credentials"
"github.com/docker/cli/cli/config/types"
)
// notFoundErr is the error returned when a plugin could not be found.
type notFoundErr string
func (notFoundErr) NotFound() {}
func (e notFoundErr) Error() string {
return string(e)
}
var errValueNotFound notFoundErr = "value not found"
type Config struct {
lock sync.RWMutex
memoryCredentials map[string]types.AuthConfig
fallbackStore credentials.Store
}
func (e *Config) Erase(serverAddress string) error {
e.lock.Lock()
defer e.lock.Unlock()
delete(e.memoryCredentials, serverAddress)
if e.fallbackStore != nil {
err := e.fallbackStore.Erase(serverAddress)
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "memorystore: ", err)
}
}
return nil
}
func (e *Config) Get(serverAddress string) (types.AuthConfig, error) {
e.lock.RLock()
defer e.lock.RUnlock()
authConfig, ok := e.memoryCredentials[serverAddress]
if !ok {
if e.fallbackStore != nil {
return e.fallbackStore.Get(serverAddress)
}
return types.AuthConfig{}, errValueNotFound
}
return authConfig, nil
}
func (e *Config) GetAll() (map[string]types.AuthConfig, error) {
e.lock.RLock()
defer e.lock.RUnlock()
creds := make(map[string]types.AuthConfig)
if e.fallbackStore != nil {
fileCredentials, err := e.fallbackStore.GetAll()
if err != nil {
_, _ = fmt.Fprintln(os.Stderr, "memorystore: ", err)
} else {
creds = fileCredentials
}
}
maps.Copy(creds, e.memoryCredentials)
return creds, nil
}
func (e *Config) Store(authConfig types.AuthConfig) error {
e.lock.Lock()
defer e.lock.Unlock()
e.memoryCredentials[authConfig.ServerAddress] = authConfig
if e.fallbackStore != nil {
return e.fallbackStore.Store(authConfig)
}
return nil
}
// WithFallbackStore sets a fallback store.
//
// Write operations will be performed on both the memory store and the
// fallback store.
//
// Read operations will first check the memory store, and if the credential
// is not found, it will then check the fallback store.
//
// Retrieving all credentials will return from both the memory store and the
// fallback store, merging the results from both stores into a single map.
//
// Data stored in the memory store will take precedence over data in the
// fallback store.
func WithFallbackStore(store credentials.Store) Options {
return func(s *Config) error {
s.fallbackStore = store
return nil
}
}
// WithAuthConfig allows to set the initial credentials in the memory store.
func WithAuthConfig(config map[string]types.AuthConfig) Options {
return func(s *Config) error {
s.memoryCredentials = config
return nil
}
}
type Options func(*Config) error
// New creates a new in memory credential store
func New(opts ...Options) (credentials.Store, error) {
m := &Config{
memoryCredentials: make(map[string]types.AuthConfig),
}
for _, opt := range opts {
if err := opt(m); err != nil {
return nil, err
}
}
return m, nil
}

View File

@@ -7,8 +7,8 @@ type AuthConfig struct {
Auth string `json:"auth,omitempty"` Auth string `json:"auth,omitempty"`
// Email is an optional value associated with the username. // Email is an optional value associated with the username.
// This field is deprecated and will be removed in a later //
// version of docker. // Deprecated: This field is deprecated since docker 1.11 (API v1.23) and will be removed in the next release.
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
ServerAddress string `json:"serveraddress,omitempty"` ServerAddress string `json:"serveraddress,omitempty"`

View File

@@ -47,14 +47,19 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper
} }
sshFlags = addSSHTimeout(sshFlags) sshFlags = addSSHTimeout(sshFlags)
sshFlags = disablePseudoTerminalAllocation(sshFlags) sshFlags = disablePseudoTerminalAllocation(sshFlags)
remoteCommand := []string{"docker", "system", "dial-stdio"}
socketPath := sp.Path
if strings.Trim(sp.Path, "/") != "" {
remoteCommand = []string{"docker", "--host=unix://" + socketPath, "system", "dial-stdio"}
}
sshArgs, err := sp.Command(sshFlags, remoteCommand...)
if err != nil {
return nil, err
}
return &ConnectionHelper{ return &ConnectionHelper{
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) { Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
args := []string{"docker"} return commandconn.New(ctx, "ssh", sshArgs...)
if sp.Path != "" {
args = append(args, "--host", "unix://"+sp.Path)
}
args = append(args, "system", "dial-stdio")
return commandconn.New(ctx, "ssh", append(sshFlags, sp.Args(args...)...)...)
}, },
Host: "http://docker.example.com", Host: "http://docker.example.com",
}, nil }, nil

View File

@@ -0,0 +1,27 @@
Copyright (c) 2016, Daniel Martí. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,13 @@
// Package syntax is a fork of [mvdan.cc/sh/v3@v3.10.0/syntax].
//
// Copyright (c) 2016, Daniel Martí. All rights reserved.
//
// It is a reduced set of the package to only provide the [Quote] function,
// and contains the [LICENSE], [quote.go] and [parser.go] files at the given
// revision.
//
// [quote.go]: https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/syntax/quote.go
// [parser.go]: https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/syntax/parser.go
// [LICENSE]: https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/LICENSE
// [mvdan.cc/sh/v3@v3.10.0/syntax]: https://pkg.go.dev/mvdan.cc/sh/v3@v3.10.0/syntax
package syntax

View File

@@ -0,0 +1,95 @@
// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package syntax
// LangVariant describes a shell language variant to use when tokenizing and
// parsing shell code. The zero value is [LangBash].
type LangVariant int
const (
// LangBash corresponds to the GNU Bash language, as described in its
// manual at https://www.gnu.org/software/bash/manual/bash.html.
//
// We currently follow Bash version 5.2.
//
// Its string representation is "bash".
LangBash LangVariant = iota
// LangPOSIX corresponds to the POSIX Shell language, as described at
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html.
//
// Its string representation is "posix" or "sh".
LangPOSIX
// LangMirBSDKorn corresponds to the MirBSD Korn Shell, also known as
// mksh, as described at http://www.mirbsd.org/htman/i386/man1/mksh.htm.
// Note that it shares some features with Bash, due to the shared
// ancestry that is ksh.
//
// We currently follow mksh version 59.
//
// Its string representation is "mksh".
LangMirBSDKorn
// LangBats corresponds to the Bash Automated Testing System language,
// as described at https://github.com/bats-core/bats-core. Note that
// it's just a small extension of the Bash language.
//
// Its string representation is "bats".
LangBats
// LangAuto corresponds to automatic language detection,
// commonly used by end-user applications like shfmt,
// which can guess a file's language variant given its filename or shebang.
//
// At this time, [Variant] does not support LangAuto.
LangAuto
)
func (l LangVariant) String() string {
switch l {
case LangBash:
return "bash"
case LangPOSIX:
return "posix"
case LangMirBSDKorn:
return "mksh"
case LangBats:
return "bats"
case LangAuto:
return "auto"
}
return "unknown shell language variant"
}
// IsKeyword returns true if the given word is part of the language keywords.
func IsKeyword(word string) bool {
// This list has been copied from the bash 5.1 source code, file y.tab.c +4460
switch word {
case
"!",
"[[", // only if COND_COMMAND is defined
"]]", // only if COND_COMMAND is defined
"case",
"coproc", // only if COPROCESS_SUPPORT is defined
"do",
"done",
"else",
"esac",
"fi",
"for",
"function",
"if",
"in",
"select", // only if SELECT_COMMAND is defined
"then",
"time", // only if COMMAND_TIMING is defined
"until",
"while",
"{",
"}":
return true
}
return false
}

View File

@@ -0,0 +1,187 @@
// Copyright (c) 2021, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package syntax
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
type QuoteError struct {
ByteOffset int
Message string
}
func (e QuoteError) Error() string {
return fmt.Sprintf("cannot quote character at byte %d: %s", e.ByteOffset, e.Message)
}
const (
quoteErrNull = "shell strings cannot contain null bytes"
quoteErrPOSIX = "POSIX shell lacks escape sequences"
quoteErrRange = "rune out of range"
quoteErrMksh = "mksh cannot escape codepoints above 16 bits"
)
// Quote returns a quoted version of the input string,
// so that the quoted version is expanded or interpreted
// as the original string in the given language variant.
//
// Quoting is necessary when using arbitrary literal strings
// as words in a shell script or command.
// Without quoting, one can run into syntax errors,
// as well as the possibility of running unintended code.
//
// An error is returned when a string cannot be quoted for a variant.
// For instance, POSIX lacks escape sequences for non-printable characters,
// and no language variant can represent a string containing null bytes.
// In such cases, the returned error type will be *QuoteError.
//
// The quoting strategy is chosen on a best-effort basis,
// to minimize the amount of extra bytes necessary.
//
// Some strings do not require any quoting and are returned unchanged.
// Those strings can be directly surrounded in single quotes as well.
//
//nolint:gocyclo // ignore "cyclomatic complexity 35 of func `Quote` is high (> 16) (gocyclo)"
func Quote(s string, lang LangVariant) (string, error) {
if s == "" {
// Special case; an empty string must always be quoted,
// as otherwise it expands to zero fields.
return "''", nil
}
shellChars := false
nonPrintable := false
offs := 0
for rem := s; len(rem) > 0; {
r, size := utf8.DecodeRuneInString(rem)
switch r {
// Like regOps; token characters.
case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`',
// Whitespace; might result in multiple fields.
' ', '\t', '\r', '\n',
// Escape sequences would be expanded.
'\\',
// Would start a comment unless quoted.
'#',
// Might result in brace expansion.
'{',
// Might result in tilde expansion.
'~',
// Might result in globbing.
'*', '?', '[',
// Might result in an assignment.
'=':
shellChars = true
case '\x00':
return "", &QuoteError{ByteOffset: offs, Message: quoteErrNull}
}
if r == utf8.RuneError || !unicode.IsPrint(r) {
if lang == LangPOSIX {
return "", &QuoteError{ByteOffset: offs, Message: quoteErrPOSIX}
}
nonPrintable = true
}
rem = rem[size:]
offs += size
}
if !shellChars && !nonPrintable && !IsKeyword(s) {
// Nothing to quote; avoid allocating.
return s, nil
}
// Single quotes are usually best,
// as they don't require any escaping of characters.
// If we have any invalid utf8 or non-printable runes,
// use $'' so that we can escape them.
// Note that we can't use double quotes for those.
var b strings.Builder
if nonPrintable {
b.WriteString("$'")
lastRequoteIfHex := false
offs = 0
for rem := s; len(rem) > 0; {
nextRequoteIfHex := false
r, size := utf8.DecodeRuneInString(rem)
switch {
case r == '\'', r == '\\':
b.WriteByte('\\')
b.WriteRune(r)
case unicode.IsPrint(r) && r != utf8.RuneError:
if lastRequoteIfHex && isHex(r) {
b.WriteString("'$'")
}
b.WriteRune(r)
case r == '\a':
b.WriteString(`\a`)
case r == '\b':
b.WriteString(`\b`)
case r == '\f':
b.WriteString(`\f`)
case r == '\n':
b.WriteString(`\n`)
case r == '\r':
b.WriteString(`\r`)
case r == '\t':
b.WriteString(`\t`)
case r == '\v':
b.WriteString(`\v`)
case r < utf8.RuneSelf, r == utf8.RuneError && size == 1:
// \xXX, fixed at two hexadecimal characters.
fmt.Fprintf(&b, "\\x%02x", rem[0])
// Unfortunately, mksh allows \x to consume more hex characters.
// Ensure that we don't allow it to read more than two.
if lang == LangMirBSDKorn {
nextRequoteIfHex = true
}
case r > utf8.MaxRune:
// Not a valid Unicode code point?
return "", &QuoteError{ByteOffset: offs, Message: quoteErrRange}
case lang == LangMirBSDKorn && r > 0xFFFD:
// From the CAVEATS section in R59's man page:
//
// mksh currently uses OPTU-16 internally, which is the same as
// UTF-8 and CESU-8 with 0000..FFFD being valid codepoints.
return "", &QuoteError{ByteOffset: offs, Message: quoteErrMksh}
case r < 0x10000:
// \uXXXX, fixed at four hexadecimal characters.
fmt.Fprintf(&b, "\\u%04x", r)
default:
// \UXXXXXXXX, fixed at eight hexadecimal characters.
fmt.Fprintf(&b, "\\U%08x", r)
}
rem = rem[size:]
lastRequoteIfHex = nextRequoteIfHex
offs += size
}
b.WriteString("'")
return b.String(), nil
}
// Single quotes without any need for escaping.
if !strings.Contains(s, "'") {
return "'" + s + "'", nil
}
// The string contains single quotes,
// so fall back to double quotes.
b.WriteByte('"')
for _, r := range s {
switch r {
case '"', '\\', '`', '$':
b.WriteByte('\\')
}
b.WriteRune(r)
}
b.WriteByte('"')
return b.String(), nil
}
func isHex(r rune) bool {
return (r >= '0' && r <= '9') ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}

View File

@@ -5,6 +5,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
"github.com/docker/cli/cli/connhelper/internal/syntax"
) )
// ParseURL creates a [Spec] from the given ssh URL. It returns an error if // ParseURL creates a [Spec] from the given ssh URL. It returns an error if
@@ -76,16 +78,106 @@ type Spec struct {
Path string Path string
} }
// Args returns args except "ssh" itself combined with optional additional command args // Args returns args except "ssh" itself combined with optional additional
func (sp *Spec) Args(add ...string) []string { // command and args to be executed on the remote host. It attempts to quote
// the given arguments to account for ssh executing the remote command in a
// shell. It returns nil when unable to quote the remote command.
func (sp *Spec) Args(remoteCommandAndArgs ...string) []string {
// Format the remote command to run using the ssh connection, quoting
// values where needed because ssh executes these in a POSIX shell.
remoteCommand, err := quoteCommand(remoteCommandAndArgs...)
if err != nil {
return nil
}
sshArgs, err := sp.args()
if err != nil {
return nil
}
if remoteCommand != "" {
sshArgs = append(sshArgs, remoteCommand)
}
return sshArgs
}
func (sp *Spec) args(sshFlags ...string) ([]string, error) {
var args []string var args []string
if sp.Host == "" {
return nil, errors.New("no host specified")
}
if sp.User != "" { if sp.User != "" {
args = append(args, "-l", sp.User) // Quote user, as it's obtained from the URL.
usr, err := syntax.Quote(sp.User, syntax.LangPOSIX)
if err != nil {
return nil, fmt.Errorf("invalid user: %w", err)
}
args = append(args, "-l", usr)
} }
if sp.Port != "" { if sp.Port != "" {
args = append(args, "-p", sp.Port) // Quote port, as it's obtained from the URL.
port, err := syntax.Quote(sp.Port, syntax.LangPOSIX)
if err != nil {
return nil, fmt.Errorf("invalid port: %w", err)
}
args = append(args, "-p", port)
} }
args = append(args, "--", sp.Host)
args = append(args, add...) // We consider "sshFlags" to be "trusted", and set from code only,
return args // as they are not parsed from the DOCKER_HOST URL.
args = append(args, sshFlags...)
host, err := syntax.Quote(sp.Host, syntax.LangPOSIX)
if err != nil {
return nil, fmt.Errorf("invalid host: %w", err)
}
return append(args, "--", host), nil
}
// Command returns the ssh flags and arguments to execute a command
// (remoteCommandAndArgs) on the remote host. Where needed, it quotes
// values passed in remoteCommandAndArgs to account for ssh executing
// the remote command in a shell. It returns an error if no remote command
// is passed, or when unable to quote the remote command.
//
// Important: to preserve backward-compatibility, Command does not currently
// perform sanitization or quoting on the sshFlags and callers are expected
// to sanitize this argument.
func (sp *Spec) Command(sshFlags []string, remoteCommandAndArgs ...string) ([]string, error) {
if len(remoteCommandAndArgs) == 0 {
return nil, errors.New("no remote command specified")
}
sshArgs, err := sp.args(sshFlags...)
if err != nil {
return nil, err
}
remoteCommand, err := quoteCommand(remoteCommandAndArgs...)
if err != nil {
return nil, err
}
if remoteCommand != "" {
sshArgs = append(sshArgs, remoteCommand)
}
return sshArgs, nil
}
// quoteCommand returns the remote command to run using the ssh connection
// as a single string, quoting values where needed because ssh executes
// these in a POSIX shell.
func quoteCommand(commandAndArgs ...string) (string, error) {
var quotedCmd string
for i, arg := range commandAndArgs {
a, err := syntax.Quote(arg, syntax.LangPOSIX)
if err != nil {
return "", fmt.Errorf("invalid argument: %w", err)
}
if i == 0 {
quotedCmd = a
continue
}
quotedCmd += " " + a
}
// each part is quoted appropriately, so now we'll have a full
// shell command to pass off to "ssh"
return quotedCmd, nil
} }

View File

@@ -101,7 +101,22 @@ func (ep *Endpoint) ClientOpts() ([]client.Opt, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
result = append(result, withHTTPClient(tlsConfig))
// If there's no tlsConfig available, we use the default HTTPClient.
if tlsConfig != nil {
result = append(result,
client.WithHTTPClient(&http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
DialContext: (&net.Dialer{
KeepAlive: 30 * time.Second,
Timeout: 30 * time.Second,
}).DialContext,
},
CheckRedirect: client.CheckRedirect,
}),
)
}
} }
result = append(result, client.WithHost(ep.Host)) result = append(result, client.WithHost(ep.Host))
} else { } else {
@@ -133,25 +148,6 @@ func isSocket(addr string) bool {
} }
} }
func withHTTPClient(tlsConfig *tls.Config) func(*client.Client) error {
return func(c *client.Client) error {
if tlsConfig == nil {
// Use the default HTTPClient
return nil
}
return client.WithHTTPClient(&http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
DialContext: (&net.Dialer{
KeepAlive: 30 * time.Second,
Timeout: 30 * time.Second,
}).DialContext,
},
CheckRedirect: client.CheckRedirect,
})(c)
}
}
// EndpointFromContext parses a context docker endpoint metadata into a typed EndpointMeta structure // EndpointFromContext parses a context docker endpoint metadata into a typed EndpointMeta structure
func EndpointFromContext(metadata store.Metadata) (EndpointMeta, error) { func EndpointFromContext(metadata store.Metadata) (EndpointMeta, error) {
ep, ok := metadata.Endpoints[DockerEndpoint] ep, ok := metadata.Endpoints[DockerEndpoint]

View File

@@ -1,9 +1,9 @@
package store package store
import cerrdefs "github.com/containerd/errdefs" import "github.com/containerd/errdefs"
func invalidParameter(err error) error { func invalidParameter(err error) error {
if err == nil || cerrdefs.IsInvalidArgument(err) { if err == nil || errdefs.IsInvalidArgument(err) {
return err return err
} }
return invalidParameterErr{err} return invalidParameterErr{err}
@@ -14,7 +14,7 @@ type invalidParameterErr struct{ error }
func (invalidParameterErr) InvalidParameter() {} func (invalidParameterErr) InvalidParameter() {}
func notFound(err error) error { func notFound(err error) error {
if err == nil || cerrdefs.IsNotFound(err) { if err == nil || errdefs.IsNotFound(err) {
return err return err
} }
return notFoundErr{err} return notFoundErr{err}

View File

@@ -33,5 +33,8 @@ func IsEnabled() bool {
// The default is to log to the debug level which is only // The default is to log to the debug level which is only
// enabled when debugging is enabled. // enabled when debugging is enabled.
var OTELErrorHandler otel.ErrorHandler = otel.ErrorHandlerFunc(func(err error) { var OTELErrorHandler otel.ErrorHandler = otel.ErrorHandlerFunc(func(err error) {
if err == nil {
return
}
logrus.WithError(err).Debug("otel error") logrus.WithError(err).Debug("otel error")
}) })

View File

@@ -1,12 +1,12 @@
package flags package flags
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/opts"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/go-connections/tlsconfig" "github.com/docker/go-connections/tlsconfig"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@@ -54,6 +54,39 @@ var (
dockerTLS = os.Getenv(EnvEnableTLS) != "" dockerTLS = os.Getenv(EnvEnableTLS) != ""
) )
// hostVar is used for the '--host' / '-H' flag to set [ClientOptions.Hosts].
// The [ClientOptions.Hosts] field is a slice because it was originally shared
// with the daemon config. However, the CLI only allows for a single host to
// be specified.
//
// hostVar presents itself as a "string", but stores the value in a string
// slice. It produces an error when trying to set multiple values, matching
// the check in [getServerHost].
//
// [getServerHost]: https://github.com/docker/cli/blob/7eab668982645def1cd46fe1b60894cba6fd17a4/cli/command/cli.go#L542-L551
type hostVar struct {
dst *[]string
set bool
}
func (h *hostVar) String() string {
if h.dst == nil || len(*h.dst) == 0 {
return ""
}
return (*h.dst)[0]
}
func (h *hostVar) Set(s string) error {
if h.set {
return errors.New("specify only one -H")
}
*h.dst = []string{s}
h.set = true
return nil
}
func (*hostVar) Type() string { return "string" }
// ClientOptions are the options used to configure the client cli. // ClientOptions are the options used to configure the client cli.
type ClientOptions struct { type ClientOptions struct {
Debug bool Debug bool
@@ -90,13 +123,13 @@ func (o *ClientOptions) InstallFlags(flags *pflag.FlagSet) {
KeyFile: filepath.Join(dockerCertPath, DefaultKeyFile), KeyFile: filepath.Join(dockerCertPath, DefaultKeyFile),
} }
tlsOptions := o.TLSOptions tlsOptions := o.TLSOptions
flags.Var(opts.NewQuotedString(&tlsOptions.CAFile), "tlscacert", "Trust certs signed only by this CA") flags.Var(&quotedString{&tlsOptions.CAFile}, "tlscacert", "Trust certs signed only by this CA")
flags.Var(opts.NewQuotedString(&tlsOptions.CertFile), "tlscert", "Path to TLS certificate file") flags.Var(&quotedString{&tlsOptions.CertFile}, "tlscert", "Path to TLS certificate file")
flags.Var(opts.NewQuotedString(&tlsOptions.KeyFile), "tlskey", "Path to TLS key file") flags.Var(&quotedString{&tlsOptions.KeyFile}, "tlskey", "Path to TLS key file")
// opts.ValidateHost is not used here, so as to allow connection helpers // TODO(thaJeztah): show the default host.
hostOpt := opts.NewNamedListOptsRef("hosts", &o.Hosts, nil) // TODO(thaJeztah): this should be a string, not an "array" as we only allow a single host.
flags.VarP(hostOpt, "host", "H", "Daemon socket to connect to") flags.VarP(&hostVar{dst: &o.Hosts}, "host", "H", "Daemon socket to connect to")
flags.StringVarP(&o.Context, "context", "c", "", flags.StringVarP(&o.Context, "context", "c", "",
`Name of the context to use to connect to the daemon (overrides `+client.EnvOverrideHost+` env var and default context set with "docker context use")`) `Name of the context to use to connect to the daemon (overrides `+client.EnvOverrideHost+` env var and default context set with "docker context use")`)
} }
@@ -146,3 +179,33 @@ func SetLogLevel(logLevel string) {
logrus.SetLevel(logrus.InfoLevel) logrus.SetLevel(logrus.InfoLevel)
} }
} }
type quotedString struct {
value *string
}
func (s *quotedString) Set(val string) error {
*s.value = trimQuotes(val)
return nil
}
func (*quotedString) Type() string {
return "string"
}
func (s *quotedString) String() string {
return *s.value
}
func trimQuotes(value string) string {
if len(value) < 2 {
return value
}
lastIndex := len(value) - 1
for _, char := range []byte{'\'', '"'} {
if value[0] == char && value[lastIndex] == char {
return value[1:lastIndex]
}
}
return value
}

View File

@@ -15,19 +15,39 @@ var InfoHeader = Str{
Fancy: aec.Bold.Apply(aec.LightCyanB.Apply(aec.BlackF.Apply("i")) + " " + aec.LightCyanF.Apply("Info → ")), Fancy: aec.Bold.Apply(aec.LightCyanB.Apply(aec.BlackF.Apply("i")) + " " + aec.LightCyanF.Apply("Info → ")),
} }
func (o Output) PrintNote(format string, args ...any) { type options struct {
header Str
}
type noteOptions func(o *options)
func withHeader(header Str) noteOptions {
return func(o *options) {
o.header = header
}
}
func (o Output) printNoteWithOptions(format string, args []any, opts ...noteOptions) {
if o.isTerminal { if o.isTerminal {
// TODO: Handle all flags // TODO: Handle all flags
format = strings.ReplaceAll(format, "--platform", ColorFlag.Apply("--platform")) format = strings.ReplaceAll(format, "--platform", ColorFlag.Apply("--platform"))
} }
header := o.Sprint(InfoHeader) opt := &options{
header: InfoHeader,
}
_, _ = fmt.Fprint(o, "\n", header) for _, override := range opts {
override(opt)
}
h := o.Sprint(opt.header)
_, _ = fmt.Fprint(o, "\n", h)
s := fmt.Sprintf(format, args...) s := fmt.Sprintf(format, args...)
for idx, line := range strings.Split(s, "\n") { for idx, line := range strings.Split(s, "\n") {
if idx > 0 { if idx > 0 {
_, _ = fmt.Fprint(o, strings.Repeat(" ", Width(header))) _, _ = fmt.Fprint(o, strings.Repeat(" ", Width(h)))
} }
l := line l := line
@@ -37,3 +57,16 @@ func (o Output) PrintNote(format string, args ...any) {
_, _ = fmt.Fprintln(o, l) _, _ = fmt.Fprintln(o, l)
} }
} }
func (o Output) PrintNote(format string, args ...any) {
o.printNoteWithOptions(format, args, withHeader(InfoHeader))
}
var warningHeader = Str{
Plain: " Warn -> ",
Fancy: aec.Bold.Apply(aec.LightYellowB.Apply(aec.BlackF.Apply("w")) + " " + ColorWarning.Apply("Warn → ")),
}
func (o Output) PrintWarning(format string, args ...any) {
o.printNoteWithOptions(format, args, withHeader(warningHeader))
}

View File

@@ -7,13 +7,14 @@ import (
) )
// ValidateEnv validates an environment variable and returns it. // ValidateEnv validates an environment variable and returns it.
// If no value is specified, it obtains its value from the current environment // If no value is specified, it obtains its value from the current environment.
// //
// As on ParseEnvFile and related to #16585, environment variable names // Environment variable names are not validated, and it's up to the application
// are not validated, and it's up to the application inside the container // inside the container to validate them (see [moby-16585]). The only validation
// to validate them or not. // here is to check if name is empty, per [moby-25099].
// //
// The only validation here is to check if name is empty, per #25099 // [moby-16585]: https://github.com/moby/moby/issues/16585
// [moby-25099]: https://github.com/moby/moby/issues/25099
func ValidateEnv(val string) (string, error) { func ValidateEnv(val string) (string, error) {
k, _, hasValue := strings.Cut(val, "=") k, _, hasValue := strings.Cut(val, "=")
if k == "" { if k == "" {

View File

@@ -1,24 +0,0 @@
package opts
import (
"os"
"github.com/docker/cli/pkg/kvfile"
)
// ParseEnvFile reads a file with environment variables enumerated by lines
//
// “Environment variable names used by the utilities in the Shell and
// Utilities volume of IEEE Std 1003.1-2001 consist solely of uppercase
// letters, digits, and the '_' (underscore) from the characters defined in
// Portable Character Set and do not begin with a digit. *But*, other
// characters may be permitted by an implementation; applications shall
// tolerate the presence of such names.”
// -- http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap08.html
//
// As of #16585, it's up to application inside docker to validate or not
// environment variables, that's why we just strip leading whitespace and
// nothing more.
func ParseEnvFile(filename string) ([]string, error) {
return kvfile.Parse(filename, os.LookupEnv)
}

View File

@@ -0,0 +1,14 @@
package opts
import (
"os"
"github.com/docker/cli/pkg/kvfile"
)
// ParseEnvFile reads a file with environment variables enumerated by lines
//
// Deprecated: use [kvfile.Parse] and pass [os.LookupEnv] to lookup env-vars from the current environment.
func ParseEnvFile(filename string) ([]string, error) {
return kvfile.Parse(filename, os.LookupEnv)
}

View File

@@ -34,7 +34,7 @@ const (
// ValidateHost validates that the specified string is a valid host and returns it. // ValidateHost validates that the specified string is a valid host and returns it.
// //
// TODO(thaJeztah): ValidateHost appears to be unused; deprecate it. // Deprecated: this function is no longer used, and will be removed in the next release.
func ValidateHost(val string) (string, error) { func ValidateHost(val string) (string, error) {
host := strings.TrimSpace(val) host := strings.TrimSpace(val)
// The empty string means default and is not handled by parseDockerDaemonHost // The empty string means default and is not handled by parseDockerDaemonHost

View File

@@ -134,6 +134,8 @@ func (opts *ListOpts) WithValidator(validator ValidatorFctType) *ListOpts {
// NamedOption is an interface that list and map options // NamedOption is an interface that list and map options
// with names implement. // with names implement.
//
// Deprecated: NamedOption is no longer used and will be removed in the next release.
type NamedOption interface { type NamedOption interface {
Name() string Name() string
} }
@@ -141,6 +143,8 @@ type NamedOption interface {
// NamedListOpts is a ListOpts with a configuration name. // NamedListOpts is a ListOpts with a configuration name.
// This struct is useful to keep reference to the assigned // This struct is useful to keep reference to the assigned
// field name in the internal configuration struct. // field name in the internal configuration struct.
//
// Deprecated: NamedListOpts is no longer used and will be removed in the next release.
type NamedListOpts struct { type NamedListOpts struct {
name string name string
ListOpts ListOpts
@@ -149,6 +153,8 @@ type NamedListOpts struct {
var _ NamedOption = &NamedListOpts{} var _ NamedOption = &NamedListOpts{}
// NewNamedListOptsRef creates a reference to a new NamedListOpts struct. // NewNamedListOptsRef creates a reference to a new NamedListOpts struct.
//
// Deprecated: NewNamedListOptsRef is no longer used and will be removed in the next release.
func NewNamedListOptsRef(name string, values *[]string, validator ValidatorFctType) *NamedListOpts { func NewNamedListOptsRef(name string, values *[]string, validator ValidatorFctType) *NamedListOpts {
return &NamedListOpts{ return &NamedListOpts{
name: name, name: name,
@@ -157,6 +163,8 @@ func NewNamedListOptsRef(name string, values *[]string, validator ValidatorFctTy
} }
// Name returns the name of the NamedListOpts in the configuration. // Name returns the name of the NamedListOpts in the configuration.
//
// Deprecated: NamedListOpts is no longer used and will be removed in the next release.
func (o *NamedListOpts) Name() string { func (o *NamedListOpts) Name() string {
return o.name return o.name
} }
@@ -210,6 +218,8 @@ func NewMapOpts(values map[string]string, validator ValidatorFctType) *MapOpts {
// NamedMapOpts is a MapOpts struct with a configuration name. // NamedMapOpts is a MapOpts struct with a configuration name.
// This struct is useful to keep reference to the assigned // This struct is useful to keep reference to the assigned
// field name in the internal configuration struct. // field name in the internal configuration struct.
//
// Deprecated: NamedMapOpts is no longer used and will be removed in the next release.
type NamedMapOpts struct { type NamedMapOpts struct {
name string name string
MapOpts MapOpts
@@ -218,6 +228,8 @@ type NamedMapOpts struct {
var _ NamedOption = &NamedMapOpts{} var _ NamedOption = &NamedMapOpts{}
// NewNamedMapOpts creates a reference to a new NamedMapOpts struct. // NewNamedMapOpts creates a reference to a new NamedMapOpts struct.
//
// Deprecated: NamedMapOpts is no longer used and will be removed in the next release.
func NewNamedMapOpts(name string, values map[string]string, validator ValidatorFctType) *NamedMapOpts { func NewNamedMapOpts(name string, values map[string]string, validator ValidatorFctType) *NamedMapOpts {
return &NamedMapOpts{ return &NamedMapOpts{
name: name, name: name,
@@ -226,6 +238,8 @@ func NewNamedMapOpts(name string, values map[string]string, validator ValidatorF
} }
// Name returns the name of the NamedMapOpts in the configuration. // Name returns the name of the NamedMapOpts in the configuration.
//
// Deprecated: NamedMapOpts is no longer used and will be removed in the next release.
func (o *NamedMapOpts) Name() string { func (o *NamedMapOpts) Name() string {
return o.name return o.name
} }

View File

@@ -2,6 +2,8 @@ package opts
// QuotedString is a string that may have extra quotes around the value. The // QuotedString is a string that may have extra quotes around the value. The
// quotes are stripped from the value. // quotes are stripped from the value.
//
// Deprecated: This option type is no longer used and will be removed in the next release.
type QuotedString struct { type QuotedString struct {
value *string value *string
} }
@@ -35,6 +37,8 @@ func trimQuotes(value string) string {
} }
// NewQuotedString returns a new quoted string option // NewQuotedString returns a new quoted string option
//
// Deprecated: This option type is no longer used and will be removed in the next release.
func NewQuotedString(value *string) *QuotedString { func NewQuotedString(value *string) *QuotedString {
return &QuotedString{value: value} return &QuotedString{value: value}
} }

View File

@@ -1,26 +1,28 @@
version: "2"
run: run:
timeout: 1m timeout: 1m
tests: true tests: true
linters: linters:
disable-all: true default: none
enable: enable: # please keep this alphabetized
- asasalint
- asciicheck - asciicheck
- copyloopvar
- dupl
- errcheck - errcheck
- forcetypeassert - forcetypeassert
- goconst
- gocritic - gocritic
- gofmt
- goimports
- gosimple
- govet - govet
- ineffassign - ineffassign
- misspell - misspell
- musttag
- revive - revive
- staticcheck - staticcheck
- typecheck
- unused - unused
issues: issues:
exclude-use-default: false
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 10 max-same-issues: 10

View File

@@ -77,7 +77,7 @@ func newSink(fn func(prefix, args string), formatter Formatter) logr.LogSink {
write: fn, write: fn,
} }
// For skipping fnlogger.Info and fnlogger.Error. // For skipping fnlogger.Info and fnlogger.Error.
l.Formatter.AddCallDepth(1) l.AddCallDepth(1) // via Formatter
return l return l
} }
@@ -164,17 +164,17 @@ type fnlogger struct {
} }
func (l fnlogger) WithName(name string) logr.LogSink { func (l fnlogger) WithName(name string) logr.LogSink {
l.Formatter.AddName(name) l.AddName(name) // via Formatter
return &l return &l
} }
func (l fnlogger) WithValues(kvList ...any) logr.LogSink { func (l fnlogger) WithValues(kvList ...any) logr.LogSink {
l.Formatter.AddValues(kvList) l.AddValues(kvList) // via Formatter
return &l return &l
} }
func (l fnlogger) WithCallDepth(depth int) logr.LogSink { func (l fnlogger) WithCallDepth(depth int) logr.LogSink {
l.Formatter.AddCallDepth(depth) l.AddCallDepth(depth) // via Formatter
return &l return &l
} }

View File

@@ -1,5 +1,12 @@
run: version: "2"
timeout: 10m
formatters:
enable:
- gofumpt
- goimports
settings:
gofumpt:
extra-rules: true
linters: linters:
enable: enable:
@@ -18,9 +25,7 @@ linters:
- gocritic - gocritic
- godot - godot
- godox - godox
- gofumpt
- goheader - goheader
- goimports
- gomoddirectives - gomoddirectives
- goprintffuncname - goprintffuncname
- gosec - gosec
@@ -31,84 +36,81 @@ linters:
- misspell - misspell
- nolintlint - nolintlint
- revive - revive
- stylecheck - staticcheck
- tenv
- testifylint - testifylint
- thelper - thelper
- unconvert - unconvert
- unparam - unparam
- usestdlibvars - usestdlibvars
- whitespace - whitespace
- wsl_v5
linters-settings: settings:
misspell: gocritic:
locale: US disabled-checks:
godox: - paramTypeCombine # already handle by gofumpt.extra-rules
keywords: - whyNoLint # already handle by nonolint
- FIXME - unnamedResult
goheader: - hugeParam
template: |- - sloppyReassign
Copyright 2015 Tim Heckman. All rights reserved. - rangeValCopy
Copyright 2018-{{ YEAR }} The Gofrs. All rights reserved. - octalLiteral
Use of this source code is governed by the BSD 3-Clause - ptrToRefParam
license that can be found in the LICENSE file. - appendAssign
gofumpt: - ruleguard
extra-rules: true - httpNoBody
gocritic: - exposedSyncMutex
enabled-tags: enabled-tags:
- diagnostic - diagnostic
- style - style
- performance - performance
disabled-checks: godox:
- paramTypeCombine # already handle by gofumpt.extra-rules keywords:
- whyNoLint # already handle by nonolint - FIXME
- unnamedResult goheader:
- hugeParam template: |-
- sloppyReassign Copyright 2015 Tim Heckman. All rights reserved.
- rangeValCopy Copyright 2018-{{ YEAR }} The Gofrs. All rights reserved.
- octalLiteral Use of this source code is governed by the BSD 3-Clause
- ptrToRefParam license that can be found in the LICENSE file.
- appendAssign gosec:
- ruleguard excludes:
- httpNoBody - G115
- exposedSyncMutex misspell:
locale: US
revive: revive:
rules: rules:
- name: struct-tag - name: struct-tag
- name: blank-imports - name: blank-imports
- name: context-as-argument - name: context-as-argument
- name: context-keys-type - name: context-keys-type
- name: dot-imports - name: dot-imports
- name: error-return - name: error-return
- name: error-strings - name: error-strings
- name: error-naming - name: error-naming
- name: exported - name: exported
- name: if-return - name: if-return
- name: increment-decrement - name: increment-decrement
- name: var-naming - name: var-naming
- name: var-declaration - name: var-declaration
- name: package-comments - name: package-comments
- name: range - name: range
- name: receiver-naming - name: receiver-naming
- name: time-naming - name: time-naming
- name: unexported-return - name: unexported-return
- name: indent-error-flow - name: indent-error-flow
- name: errorf - name: errorf
- name: empty-block - name: empty-block
- name: superfluous-else - name: superfluous-else
- name: unused-parameter - name: unused-parameter
- name: unreachable-code - name: unreachable-code
- name: redefines-builtin-id - name: redefines-builtin-id
exclusions:
presets:
- comments
- common-false-positives
- std-error-handling
issues: issues:
exclude-use-default: true
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
output:
show-stats: true
sort-results: true
sort-order:
- linter
- file

View File

@@ -1,4 +1,4 @@
Copyright (c) 2018-2024, The Gofrs Copyright (c) 2018-2025, The Gofrs
Copyright (c) 2015-2020, Tim Heckman Copyright (c) 2015-2020, Tim Heckman
All rights reserved. All rights reserved.

View File

@@ -1,5 +1,5 @@
// Copyright 2015 Tim Heckman. All rights reserved. // Copyright 2015 Tim Heckman. All rights reserved.
// Copyright 2018-2024 The Gofrs. All rights reserved. // Copyright 2018-2025 The Gofrs. All rights reserved.
// Use of this source code is governed by the BSD 3-Clause // Use of this source code is governed by the BSD 3-Clause
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@@ -62,6 +62,7 @@ type Flock struct {
func New(path string, opts ...Option) *Flock { func New(path string, opts ...Option) *Flock {
// create it if it doesn't exist, and open the file read-only. // create it if it doesn't exist, and open the file read-only.
flags := os.O_CREATE flags := os.O_CREATE
switch runtime.GOOS { switch runtime.GOOS {
case "aix", "solaris", "illumos": case "aix", "solaris", "illumos":
// AIX cannot preform write-lock (i.e. exclusive) on a read-only file. // AIX cannot preform write-lock (i.e. exclusive) on a read-only file.
@@ -124,6 +125,22 @@ func (f *Flock) RLocked() bool {
return f.r return f.r
} }
// Stat returns the FileInfo structure describing the lock file.
// If the lock file does not exist or cannot be accessed, an error is returned.
//
// This can be used to check the modification time of the lock file,
// which is useful for detecting stale locks.
func (f *Flock) Stat() (fs.FileInfo, error) {
f.m.RLock()
defer f.m.RUnlock()
if f.fh != nil {
return f.fh.Stat()
}
return os.Stat(f.path)
}
func (f *Flock) String() string { func (f *Flock) String() string {
return f.path return f.path
} }
@@ -158,7 +175,6 @@ func tryCtx(ctx context.Context, fn func() (bool, error), retryDelay time.Durati
case <-ctx.Done(): case <-ctx.Done():
return false, ctx.Err() return false, ctx.Err()
case <-time.After(retryDelay): case <-time.After(retryDelay):
// try again
} }
} }
} }

View File

@@ -1,3 +1,8 @@
// Copyright 2015 Tim Heckman. All rights reserved.
// Copyright 2018-2025 The Gofrs. All rights reserved.
// Use of this source code is governed by the BSD 3-Clause
// license that can be found in the LICENSE file.
//go:build (!unix && !windows) || plan9 //go:build (!unix && !windows) || plan9
package flock package flock

View File

@@ -1,5 +1,5 @@
// Copyright 2015 Tim Heckman. All rights reserved. // Copyright 2015 Tim Heckman. All rights reserved.
// Copyright 2018-2024 The Gofrs. All rights reserved. // Copyright 2018-2025 The Gofrs. All rights reserved.
// Use of this source code is governed by the BSD 3-Clause // Use of this source code is governed by the BSD 3-Clause
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@@ -155,6 +155,7 @@ func (f *Flock) try(locked *bool, flag int) (bool, error) {
} }
var retried bool var retried bool
retry: retry:
err := unix.Flock(int(f.fh.Fd()), flag|unix.LOCK_NB) err := unix.Flock(int(f.fh.Fd()), flag|unix.LOCK_NB)

View File

@@ -1,5 +1,5 @@
// Copyright 2015 Tim Heckman. All rights reserved. // Copyright 2015 Tim Heckman. All rights reserved.
// Copyright 2018-2024 The Gofrs. All rights reserved. // Copyright 2018-2025 The Gofrs. All rights reserved.
// Use of this source code is governed by the BSD 3-Clause // Use of this source code is governed by the BSD 3-Clause
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.

View File

@@ -1,5 +1,5 @@
// Copyright 2015 Tim Heckman. All rights reserved. // Copyright 2015 Tim Heckman. All rights reserved.
// Copyright 2018-2024 The Gofrs. All rights reserved. // Copyright 2018-2025 The Gofrs. All rights reserved.
// Use of this source code is governed by the BSD 3-Clause // Use of this source code is governed by the BSD 3-Clause
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
@@ -23,6 +23,8 @@ const winLockfileSharedLock = 0x00000000
// ErrorLockViolation is the error code returned from the Windows syscall when a lock would block, // ErrorLockViolation is the error code returned from the Windows syscall when a lock would block,
// and you ask to fail immediately. // and you ask to fail immediately.
//
//nolint:errname // It should be renamed to `ErrLockViolation`.
const ErrorLockViolation windows.Errno = 0x21 // 33 const ErrorLockViolation windows.Errno = 0x21 // 33
// Lock is a blocking call to try and take an exclusive file lock. // Lock is a blocking call to try and take an exclusive file lock.

View File

@@ -7,19 +7,13 @@ Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
---
⚠️ **[The Gorilla WebSocket Package is looking for a new maintainer](https://github.com/gorilla/websocket/issues/370)**
---
### Documentation ### Documentation
* [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc) * [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc)
* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat) * [Chat example](https://github.com/gorilla/websocket/tree/main/examples/chat)
* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command) * [Command example](https://github.com/gorilla/websocket/tree/main/examples/command)
* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo) * [Client and server example](https://github.com/gorilla/websocket/tree/main/examples/echo)
* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch) * [File watch example](https://github.com/gorilla/websocket/tree/main/examples/filewatch)
### Status ### Status
@@ -35,5 +29,4 @@ package API is stable.
The Gorilla WebSocket package passes the server tests in the [Autobahn Test The Gorilla WebSocket package passes the server tests in the [Autobahn Test
Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn
subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn). subdirectory](https://github.com/gorilla/websocket/tree/main/examples/autobahn).

View File

@@ -9,8 +9,8 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"net/http/httptrace" "net/http/httptrace"
@@ -51,18 +51,34 @@ func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufS
// //
// It is safe to call Dialer's methods concurrently. // It is safe to call Dialer's methods concurrently.
type Dialer struct { type Dialer struct {
// The following custom dial functions can be set to establish
// connections to either the backend server or the proxy (if it
// exists). The scheme of the dialed entity (either backend or
// proxy) determines which custom dial function is selected:
// either NetDialTLSContext for HTTPS or NetDialContext/NetDial
// for HTTP. Since the "Proxy" function can determine the scheme
// dynamically, it can make sense to set multiple custom dial
// functions simultaneously.
//
// NetDial specifies the dial function for creating TCP connections. If // NetDial specifies the dial function for creating TCP connections. If
// NetDial is nil, net.Dial is used. // NetDial is nil, net.Dialer DialContext is used.
// If "Proxy" field is also set, this function dials the proxy--not
// the backend server.
NetDial func(network, addr string) (net.Conn, error) NetDial func(network, addr string) (net.Conn, error)
// NetDialContext specifies the dial function for creating TCP connections. If // NetDialContext specifies the dial function for creating TCP connections. If
// NetDialContext is nil, NetDial is used. // NetDialContext is nil, NetDial is used.
// If "Proxy" field is also set, this function dials the proxy--not
// the backend server.
NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error) NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error)
// NetDialTLSContext specifies the dial function for creating TLS/TCP connections. If // NetDialTLSContext specifies the dial function for creating TLS/TCP connections. If
// NetDialTLSContext is nil, NetDialContext is used. // NetDialTLSContext is nil, NetDialContext is used.
// If NetDialTLSContext is set, Dial assumes the TLS handshake is done there and // If NetDialTLSContext is set, Dial assumes the TLS handshake is done there and
// TLSClientConfig is ignored. // TLSClientConfig is ignored.
// If "Proxy" field is also set, this function dials the proxy (and performs
// the TLS handshake with the proxy, ignoring TLSClientConfig). In this TLS proxy
// dialing case the TLSClientConfig could still be necessary for TLS to the backend server.
NetDialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error) NetDialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)
// Proxy specifies a function to return a proxy for a given // Proxy specifies a function to return a proxy for a given
@@ -73,7 +89,7 @@ type Dialer struct {
// TLSClientConfig specifies the TLS configuration to use with tls.Client. // TLSClientConfig specifies the TLS configuration to use with tls.Client.
// If nil, the default configuration is used. // If nil, the default configuration is used.
// If either NetDialTLS or NetDialTLSContext are set, Dial assumes the TLS handshake // If NetDialTLSContext is set, Dial assumes the TLS handshake
// is done there and TLSClientConfig is ignored. // is done there and TLSClientConfig is ignored.
TLSClientConfig *tls.Config TLSClientConfig *tls.Config
@@ -244,71 +260,16 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
defer cancel() defer cancel()
} }
// Get network dial function. var proxyURL *url.URL
var netDial func(network, add string) (net.Conn, error)
switch u.Scheme {
case "http":
if d.NetDialContext != nil {
netDial = func(network, addr string) (net.Conn, error) {
return d.NetDialContext(ctx, network, addr)
}
} else if d.NetDial != nil {
netDial = d.NetDial
}
case "https":
if d.NetDialTLSContext != nil {
netDial = func(network, addr string) (net.Conn, error) {
return d.NetDialTLSContext(ctx, network, addr)
}
} else if d.NetDialContext != nil {
netDial = func(network, addr string) (net.Conn, error) {
return d.NetDialContext(ctx, network, addr)
}
} else if d.NetDial != nil {
netDial = d.NetDial
}
default:
return nil, nil, errMalformedURL
}
if netDial == nil {
netDialer := &net.Dialer{}
netDial = func(network, addr string) (net.Conn, error) {
return netDialer.DialContext(ctx, network, addr)
}
}
// If needed, wrap the dial function to set the connection deadline.
if deadline, ok := ctx.Deadline(); ok {
forwardDial := netDial
netDial = func(network, addr string) (net.Conn, error) {
c, err := forwardDial(network, addr)
if err != nil {
return nil, err
}
err = c.SetDeadline(deadline)
if err != nil {
c.Close()
return nil, err
}
return c, nil
}
}
// If needed, wrap the dial function to connect through a proxy.
if d.Proxy != nil { if d.Proxy != nil {
proxyURL, err := d.Proxy(req) proxyURL, err = d.Proxy(req)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if proxyURL != nil { }
dialer, err := proxy_FromURL(proxyURL, netDialerFunc(netDial)) netDial, err := d.netDialFn(ctx, proxyURL, u)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
}
netDial = dialer.Dial
}
} }
hostPort, hostNoPort := hostPortNoPort(u) hostPort, hostNoPort := hostPortNoPort(u)
@@ -317,24 +278,30 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
trace.GetConn(hostPort) trace.GetConn(hostPort)
} }
netConn, err := netDial("tcp", hostPort) netConn, err := netDial(ctx, "tcp", hostPort)
if err != nil {
return nil, nil, err
}
if trace != nil && trace.GotConn != nil { if trace != nil && trace.GotConn != nil {
trace.GotConn(httptrace.GotConnInfo{ trace.GotConn(httptrace.GotConnInfo{
Conn: netConn, Conn: netConn,
}) })
} }
if err != nil {
return nil, nil, err
}
// Close the network connection when returning an error. The variable
// netConn is set to nil before the success return at the end of the
// function.
defer func() { defer func() {
if netConn != nil { if netConn != nil {
netConn.Close() // It's safe to ignore the error from Close() because this code is
// only executed when returning a more important error to the
// application.
_ = netConn.Close()
} }
}() }()
if u.Scheme == "https" && d.NetDialTLSContext == nil { // Do TLS handshake over established connection if a proxy exists.
// If NetDialTLSContext is set, assume that the TLS handshake has already been done if proxyURL != nil && u.Scheme == "https" {
cfg := cloneTLSConfig(d.TLSClientConfig) cfg := cloneTLSConfig(d.TLSClientConfig)
if cfg.ServerName == "" { if cfg.ServerName == "" {
@@ -370,6 +337,17 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
resp, err := http.ReadResponse(conn.br, req) resp, err := http.ReadResponse(conn.br, req)
if err != nil { if err != nil {
if d.TLSClientConfig != nil {
for _, proto := range d.TLSClientConfig.NextProtos {
if proto != "http/1.1" {
return nil, nil, fmt.Errorf(
"websocket: protocol %q was given but is not supported;"+
"sharing tls.Config with net/http Transport can cause this error: %w",
proto, err,
)
}
}
}
return nil, nil, err return nil, nil, err
} }
@@ -388,7 +366,7 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
// debugging. // debugging.
buf := make([]byte, 1024) buf := make([]byte, 1024)
n, _ := io.ReadFull(resp.Body, buf) n, _ := io.ReadFull(resp.Body, buf)
resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n])) resp.Body = io.NopCloser(bytes.NewReader(buf[:n]))
return nil, resp, ErrBadHandshake return nil, resp, ErrBadHandshake
} }
@@ -406,17 +384,134 @@ func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader h
break break
} }
resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) resp.Body = io.NopCloser(bytes.NewReader([]byte{}))
conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol") conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")
netConn.SetDeadline(time.Time{}) if err := netConn.SetDeadline(time.Time{}); err != nil {
netConn = nil // to avoid close in defer. return nil, resp, err
}
// Success! Set netConn to nil to stop the deferred function above from
// closing the network connection.
netConn = nil
return conn, resp, nil return conn, resp, nil
} }
// Returns the dial function to establish the connection to either the backend
// server or the proxy (if it exists). If the dialed entity is HTTPS, then the
// returned dial function *also* performs the TLS handshake to the dialed entity.
// NOTE: If a proxy exists, it is possible for a second TLS handshake to be
// necessary over the established connection.
func (d *Dialer) netDialFn(ctx context.Context, proxyURL *url.URL, backendURL *url.URL) (netDialerFunc, error) {
var netDial netDialerFunc
if proxyURL != nil {
netDial = d.netDialFromURL(proxyURL)
} else {
netDial = d.netDialFromURL(backendURL)
}
// If needed, wrap the dial function to set the connection deadline.
if deadline, ok := ctx.Deadline(); ok {
netDial = netDialWithDeadline(netDial, deadline)
}
// Proxy dialing is wrapped to implement CONNECT method and possibly proxy auth.
if proxyURL != nil {
return proxyFromURL(proxyURL, netDial)
}
return netDial, nil
}
// Returns function to create the connection depending on the Dialer's
// custom dialing functions and the passed URL of entity connecting to.
func (d *Dialer) netDialFromURL(u *url.URL) netDialerFunc {
var netDial netDialerFunc
switch {
case d.NetDialContext != nil:
netDial = d.NetDialContext
case d.NetDial != nil:
netDial = func(ctx context.Context, net, addr string) (net.Conn, error) {
return d.NetDial(net, addr)
}
default:
netDial = (&net.Dialer{}).DialContext
}
// If dialed entity is HTTPS, then either use custom TLS dialing function (if exists)
// or wrap the previously computed "netDial" to use TLS config for handshake.
if u.Scheme == "https" {
if d.NetDialTLSContext != nil {
netDial = d.NetDialTLSContext
} else {
netDial = netDialWithTLSHandshake(netDial, d.TLSClientConfig, u)
}
}
return netDial
}
// Returns wrapped "netDial" function, performing TLS handshake after connecting.
func netDialWithTLSHandshake(netDial netDialerFunc, tlsConfig *tls.Config, u *url.URL) netDialerFunc {
return func(ctx context.Context, unused, addr string) (net.Conn, error) {
hostPort, hostNoPort := hostPortNoPort(u)
trace := httptrace.ContextClientTrace(ctx)
if trace != nil && trace.GetConn != nil {
trace.GetConn(hostPort)
}
// Creates TCP connection to addr using passed "netDial" function.
conn, err := netDial(ctx, "tcp", addr)
if err != nil {
return nil, err
}
cfg := cloneTLSConfig(tlsConfig)
if cfg.ServerName == "" {
cfg.ServerName = hostNoPort
}
tlsConn := tls.Client(conn, cfg)
// Do the TLS handshake using TLSConfig over the wrapped connection.
if trace != nil && trace.TLSHandshakeStart != nil {
trace.TLSHandshakeStart()
}
err = doHandshake(ctx, tlsConn, cfg)
if trace != nil && trace.TLSHandshakeDone != nil {
trace.TLSHandshakeDone(tlsConn.ConnectionState(), err)
}
if err != nil {
tlsConn.Close()
return nil, err
}
return tlsConn, nil
}
}
// Returns wrapped "netDial" function, setting passed deadline.
func netDialWithDeadline(netDial netDialerFunc, deadline time.Time) netDialerFunc {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
c, err := netDial(ctx, network, addr)
if err != nil {
return nil, err
}
err = c.SetDeadline(deadline)
if err != nil {
c.Close()
return nil, err
}
return c, nil
}
}
func cloneTLSConfig(cfg *tls.Config) *tls.Config { func cloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil { if cfg == nil {
return &tls.Config{} return &tls.Config{}
} }
return cfg.Clone() return cfg.Clone()
} }
func doHandshake(ctx context.Context, tlsConn *tls.Conn, cfg *tls.Config) error {
if err := tlsConn.HandshakeContext(ctx); err != nil {
return err
}
if !cfg.InsecureSkipVerify {
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
return err
}
}
return nil
}

View File

@@ -33,7 +33,11 @@ func decompressNoContextTakeover(r io.Reader) io.ReadCloser {
"\x01\x00\x00\xff\xff" "\x01\x00\x00\xff\xff"
fr, _ := flateReaderPool.Get().(io.ReadCloser) fr, _ := flateReaderPool.Get().(io.ReadCloser)
fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil) mr := io.MultiReader(r, strings.NewReader(tail))
if err := fr.(flate.Resetter).Reset(mr, nil); err != nil {
// Reset never fails, but handle error in case that changes.
fr = flate.NewReader(mr)
}
return &flateReadWrapper{fr} return &flateReadWrapper{fr}
} }

View File

@@ -6,11 +6,10 @@ package websocket
import ( import (
"bufio" "bufio"
"crypto/rand"
"encoding/binary" "encoding/binary"
"errors" "errors"
"io" "io"
"io/ioutil"
"math/rand"
"net" "net"
"strconv" "strconv"
"strings" "strings"
@@ -181,16 +180,16 @@ var (
errInvalidControlFrame = errors.New("websocket: invalid control frame") errInvalidControlFrame = errors.New("websocket: invalid control frame")
) )
func newMaskKey() [4]byte { // maskRand is an io.Reader for generating mask bytes. The reader is initialized
n := rand.Uint32() // to crypto/rand Reader. Tests swap the reader to a math/rand reader for
return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)} // reproducible results.
} var maskRand = rand.Reader
func hideTempErr(err error) error { // newMaskKey returns a new 32 bit value for masking client frames.
if e, ok := err.(net.Error); ok && e.Temporary() { func newMaskKey() [4]byte {
err = &netError{msg: e.Error(), timeout: e.Timeout()} var k [4]byte
} _, _ = io.ReadFull(maskRand, k[:])
return err return k
} }
func isControl(frameType int) bool { func isControl(frameType int) bool {
@@ -358,7 +357,6 @@ func (c *Conn) RemoteAddr() net.Addr {
// Write methods // Write methods
func (c *Conn) writeFatal(err error) error { func (c *Conn) writeFatal(err error) error {
err = hideTempErr(err)
c.writeErrMu.Lock() c.writeErrMu.Lock()
if c.writeErr == nil { if c.writeErr == nil {
c.writeErr = err c.writeErr = err
@@ -372,7 +370,9 @@ func (c *Conn) read(n int) ([]byte, error) {
if err == io.EOF { if err == io.EOF {
err = errUnexpectedEOF err = errUnexpectedEOF
} }
c.br.Discard(len(p)) // Discard is guaranteed to succeed because the number of bytes to discard
// is less than or equal to the number of bytes buffered.
_, _ = c.br.Discard(len(p))
return p, err return p, err
} }
@@ -387,7 +387,9 @@ func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error
return err return err
} }
c.conn.SetWriteDeadline(deadline) if err := c.conn.SetWriteDeadline(deadline); err != nil {
return c.writeFatal(err)
}
if len(buf1) == 0 { if len(buf1) == 0 {
_, err = c.conn.Write(buf0) _, err = c.conn.Write(buf0)
} else { } else {
@@ -397,7 +399,7 @@ func (c *Conn) write(frameType int, deadline time.Time, buf0, buf1 []byte) error
return c.writeFatal(err) return c.writeFatal(err)
} }
if frameType == CloseMessage { if frameType == CloseMessage {
c.writeFatal(ErrCloseSent) _ = c.writeFatal(ErrCloseSent)
} }
return nil return nil
} }
@@ -436,21 +438,27 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
maskBytes(key, 0, buf[6:]) maskBytes(key, 0, buf[6:])
} }
d := 1000 * time.Hour if deadline.IsZero() {
if !deadline.IsZero() { // No timeout for zero time.
d = deadline.Sub(time.Now()) <-c.mu
} else {
d := time.Until(deadline)
if d < 0 { if d < 0 {
return errWriteTimeout return errWriteTimeout
} }
select {
case <-c.mu:
default:
timer := time.NewTimer(d)
select {
case <-c.mu:
timer.Stop()
case <-timer.C:
return errWriteTimeout
}
}
} }
timer := time.NewTimer(d)
select {
case <-c.mu:
timer.Stop()
case <-timer.C:
return errWriteTimeout
}
defer func() { c.mu <- struct{}{} }() defer func() { c.mu <- struct{}{} }()
c.writeErrMu.Lock() c.writeErrMu.Lock()
@@ -460,13 +468,14 @@ func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) er
return err return err
} }
c.conn.SetWriteDeadline(deadline) if err := c.conn.SetWriteDeadline(deadline); err != nil {
_, err = c.conn.Write(buf) return c.writeFatal(err)
if err != nil { }
if _, err = c.conn.Write(buf); err != nil {
return c.writeFatal(err) return c.writeFatal(err)
} }
if messageType == CloseMessage { if messageType == CloseMessage {
c.writeFatal(ErrCloseSent) _ = c.writeFatal(ErrCloseSent)
} }
return err return err
} }
@@ -630,7 +639,7 @@ func (w *messageWriter) flushFrame(final bool, extra []byte) error {
} }
if final { if final {
w.endMessage(errWriteClosed) _ = w.endMessage(errWriteClosed)
return nil return nil
} }
@@ -795,7 +804,7 @@ func (c *Conn) advanceFrame() (int, error) {
// 1. Skip remainder of previous frame. // 1. Skip remainder of previous frame.
if c.readRemaining > 0 { if c.readRemaining > 0 {
if _, err := io.CopyN(ioutil.Discard, c.br, c.readRemaining); err != nil { if _, err := io.CopyN(io.Discard, c.br, c.readRemaining); err != nil {
return noFrame, err return noFrame, err
} }
} }
@@ -817,7 +826,7 @@ func (c *Conn) advanceFrame() (int, error) {
rsv2 := p[0]&rsv2Bit != 0 rsv2 := p[0]&rsv2Bit != 0
rsv3 := p[0]&rsv3Bit != 0 rsv3 := p[0]&rsv3Bit != 0
mask := p[1]&maskBit != 0 mask := p[1]&maskBit != 0
c.setReadRemaining(int64(p[1] & 0x7f)) _ = c.setReadRemaining(int64(p[1] & 0x7f)) // will not fail because argument is >= 0
c.readDecompress = false c.readDecompress = false
if rsv1 { if rsv1 {
@@ -922,7 +931,8 @@ func (c *Conn) advanceFrame() (int, error) {
} }
if c.readLimit > 0 && c.readLength > c.readLimit { if c.readLimit > 0 && c.readLength > c.readLimit {
c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait)) // Make a best effort to send a close message describing the problem.
_ = c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait))
return noFrame, ErrReadLimit return noFrame, ErrReadLimit
} }
@@ -934,7 +944,7 @@ func (c *Conn) advanceFrame() (int, error) {
var payload []byte var payload []byte
if c.readRemaining > 0 { if c.readRemaining > 0 {
payload, err = c.read(int(c.readRemaining)) payload, err = c.read(int(c.readRemaining))
c.setReadRemaining(0) _ = c.setReadRemaining(0) // will not fail because argument is >= 0
if err != nil { if err != nil {
return noFrame, err return noFrame, err
} }
@@ -981,7 +991,8 @@ func (c *Conn) handleProtocolError(message string) error {
if len(data) > maxControlFramePayloadSize { if len(data) > maxControlFramePayloadSize {
data = data[:maxControlFramePayloadSize] data = data[:maxControlFramePayloadSize]
} }
c.WriteControl(CloseMessage, data, time.Now().Add(writeWait)) // Make a best effor to send a close message describing the problem.
_ = c.WriteControl(CloseMessage, data, time.Now().Add(writeWait))
return errors.New("websocket: " + message) return errors.New("websocket: " + message)
} }
@@ -1008,7 +1019,7 @@ func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
for c.readErr == nil { for c.readErr == nil {
frameType, err := c.advanceFrame() frameType, err := c.advanceFrame()
if err != nil { if err != nil {
c.readErr = hideTempErr(err) c.readErr = err
break break
} }
@@ -1048,13 +1059,13 @@ func (r *messageReader) Read(b []byte) (int, error) {
b = b[:c.readRemaining] b = b[:c.readRemaining]
} }
n, err := c.br.Read(b) n, err := c.br.Read(b)
c.readErr = hideTempErr(err) c.readErr = err
if c.isServer { if c.isServer {
c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n]) c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n])
} }
rem := c.readRemaining rem := c.readRemaining
rem -= int64(n) rem -= int64(n)
c.setReadRemaining(rem) _ = c.setReadRemaining(rem) // rem is guaranteed to be >= 0
if c.readRemaining > 0 && c.readErr == io.EOF { if c.readRemaining > 0 && c.readErr == io.EOF {
c.readErr = errUnexpectedEOF c.readErr = errUnexpectedEOF
} }
@@ -1069,7 +1080,7 @@ func (r *messageReader) Read(b []byte) (int, error) {
frameType, err := c.advanceFrame() frameType, err := c.advanceFrame()
switch { switch {
case err != nil: case err != nil:
c.readErr = hideTempErr(err) c.readErr = err
case frameType == TextMessage || frameType == BinaryMessage: case frameType == TextMessage || frameType == BinaryMessage:
c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader") c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader")
} }
@@ -1094,7 +1105,7 @@ func (c *Conn) ReadMessage() (messageType int, p []byte, err error) {
if err != nil { if err != nil {
return messageType, nil, err return messageType, nil, err
} }
p, err = ioutil.ReadAll(r) p, err = io.ReadAll(r)
return messageType, p, err return messageType, p, err
} }
@@ -1136,7 +1147,8 @@ func (c *Conn) SetCloseHandler(h func(code int, text string) error) {
if h == nil { if h == nil {
h = func(code int, text string) error { h = func(code int, text string) error {
message := FormatCloseMessage(code, "") message := FormatCloseMessage(code, "")
c.WriteControl(CloseMessage, message, time.Now().Add(writeWait)) // Make a best effor to send the close message.
_ = c.WriteControl(CloseMessage, message, time.Now().Add(writeWait))
return nil return nil
} }
} }
@@ -1158,13 +1170,9 @@ func (c *Conn) PingHandler() func(appData string) error {
func (c *Conn) SetPingHandler(h func(appData string) error) { func (c *Conn) SetPingHandler(h func(appData string) error) {
if h == nil { if h == nil {
h = func(message string) error { h = func(message string) error {
err := c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait)) // Make a best effort to send the pong message.
if err == ErrCloseSent { _ = c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait))
return nil return nil
} else if e, ok := err.(net.Error); ok && e.Temporary() {
return nil
}
return err
} }
} }
c.handlePing = h c.handlePing = h
@@ -1189,8 +1197,16 @@ func (c *Conn) SetPongHandler(h func(appData string) error) {
c.handlePong = h c.handlePong = h
} }
// NetConn returns the underlying connection that is wrapped by c.
// Note that writing to or reading from this connection directly will corrupt the
// WebSocket connection.
func (c *Conn) NetConn() net.Conn {
return c.conn
}
// UnderlyingConn returns the internal net.Conn. This can be used to further // UnderlyingConn returns the internal net.Conn. This can be used to further
// modifications to connection specific flags. // modifications to connection specific flags.
// Deprecated: Use the NetConn method.
func (c *Conn) UnderlyingConn() net.Conn { func (c *Conn) UnderlyingConn() net.Conn {
return c.conn return c.conn
} }

View File

@@ -6,34 +6,52 @@ package websocket
import ( import (
"bufio" "bufio"
"bytes"
"context"
"encoding/base64" "encoding/base64"
"errors" "errors"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"golang.org/x/net/proxy"
) )
type netDialerFunc func(network, addr string) (net.Conn, error) type netDialerFunc func(ctx context.Context, network, addr string) (net.Conn, error)
func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) { func (fn netDialerFunc) Dial(network, addr string) (net.Conn, error) {
return fn(network, addr) return fn(context.Background(), network, addr)
} }
func init() { func (fn netDialerFunc) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
proxy_RegisterDialerType("http", func(proxyURL *url.URL, forwardDialer proxy_Dialer) (proxy_Dialer, error) { return fn(ctx, network, addr)
return &httpProxyDialer{proxyURL: proxyURL, forwardDial: forwardDialer.Dial}, nil }
})
func proxyFromURL(proxyURL *url.URL, forwardDial netDialerFunc) (netDialerFunc, error) {
if proxyURL.Scheme == "http" || proxyURL.Scheme == "https" {
return (&httpProxyDialer{proxyURL: proxyURL, forwardDial: forwardDial}).DialContext, nil
}
dialer, err := proxy.FromURL(proxyURL, forwardDial)
if err != nil {
return nil, err
}
if d, ok := dialer.(proxy.ContextDialer); ok {
return d.DialContext, nil
}
return func(ctx context.Context, net, addr string) (net.Conn, error) {
return dialer.Dial(net, addr)
}, nil
} }
type httpProxyDialer struct { type httpProxyDialer struct {
proxyURL *url.URL proxyURL *url.URL
forwardDial func(network, addr string) (net.Conn, error) forwardDial netDialerFunc
} }
func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error) { func (hpd *httpProxyDialer) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) {
hostPort, _ := hostPortNoPort(hpd.proxyURL) hostPort, _ := hostPortNoPort(hpd.proxyURL)
conn, err := hpd.forwardDial(network, hostPort) conn, err := hpd.forwardDial(ctx, network, hostPort)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -46,7 +64,6 @@ func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error)
connectHeader.Set("Proxy-Authorization", "Basic "+credential) connectHeader.Set("Proxy-Authorization", "Basic "+credential)
} }
} }
connectReq := &http.Request{ connectReq := &http.Request{
Method: http.MethodConnect, Method: http.MethodConnect,
URL: &url.URL{Opaque: addr}, URL: &url.URL{Opaque: addr},
@@ -59,7 +76,7 @@ func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error)
return nil, err return nil, err
} }
// Read response. It's OK to use and discard buffered reader here becaue // Read response. It's OK to use and discard buffered reader here because
// the remote server does not speak until spoken to. // the remote server does not speak until spoken to.
br := bufio.NewReader(conn) br := bufio.NewReader(conn)
resp, err := http.ReadResponse(br, connectReq) resp, err := http.ReadResponse(br, connectReq)
@@ -68,8 +85,18 @@ func (hpd *httpProxyDialer) Dial(network string, addr string) (net.Conn, error)
return nil, err return nil, err
} }
if resp.StatusCode != 200 { // Close the response body to silence false positives from linters. Reset
conn.Close() // the buffered reader first to ensure that Close() does not read from
// conn.
// Note: Applications must call resp.Body.Close() on a response returned
// http.ReadResponse to inspect trailers or read another response from the
// buffered reader. The call to resp.Body.Close() does not release
// resources.
br.Reset(bytes.NewReader(nil))
_ = resp.Body.Close()
if resp.StatusCode != http.StatusOK {
_ = conn.Close()
f := strings.SplitN(resp.Status, " ", 2) f := strings.SplitN(resp.Status, " ", 2)
return nil, errors.New(f[1]) return nil, errors.New(f[1])
} }

View File

@@ -6,8 +6,7 @@ package websocket
import ( import (
"bufio" "bufio"
"errors" "net"
"io"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@@ -101,8 +100,8 @@ func checkSameOrigin(r *http.Request) bool {
func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string { func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string {
if u.Subprotocols != nil { if u.Subprotocols != nil {
clientProtocols := Subprotocols(r) clientProtocols := Subprotocols(r)
for _, serverProtocol := range u.Subprotocols { for _, clientProtocol := range clientProtocols {
for _, clientProtocol := range clientProtocols { for _, serverProtocol := range u.Subprotocols {
if clientProtocol == serverProtocol { if clientProtocol == serverProtocol {
return clientProtocol return clientProtocol
} }
@@ -130,7 +129,8 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
} }
if !tokenListContainsValue(r.Header, "Upgrade", "websocket") { if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header") w.Header().Set("Upgrade", "websocket")
return u.returnError(w, r, http.StatusUpgradeRequired, badHandshake+"'websocket' token not found in 'Upgrade' header")
} }
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
@@ -154,8 +154,8 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
} }
challengeKey := r.Header.Get("Sec-Websocket-Key") challengeKey := r.Header.Get("Sec-Websocket-Key")
if challengeKey == "" { if !isValidChallengeKey(challengeKey) {
return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header is missing or blank") return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'Sec-WebSocket-Key' header must be Base64 encoded value of 16-byte in length")
} }
subprotocol := u.selectSubprotocol(r, responseHeader) subprotocol := u.selectSubprotocol(r, responseHeader)
@@ -172,28 +172,37 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
} }
} }
h, ok := w.(http.Hijacker) netConn, brw, err := http.NewResponseController(w).Hijack()
if !ok {
return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
}
var brw *bufio.ReadWriter
netConn, brw, err := h.Hijack()
if err != nil { if err != nil {
return u.returnError(w, r, http.StatusInternalServerError, err.Error()) return u.returnError(w, r, http.StatusInternalServerError,
"websocket: hijack: "+err.Error())
} }
if brw.Reader.Buffered() > 0 { // Close the network connection when returning an error. The variable
netConn.Close() // netConn is set to nil before the success return at the end of the
return nil, errors.New("websocket: client sent data before handshake is complete") // function.
} defer func() {
if netConn != nil {
// It's safe to ignore the error from Close() because this code is
// only executed when returning a more important error to the
// application.
_ = netConn.Close()
}
}()
var br *bufio.Reader var br *bufio.Reader
if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 { if u.ReadBufferSize == 0 && brw.Reader.Size() > 256 {
// Reuse hijacked buffered reader as connection reader. // Use hijacked buffered reader as the connection reader.
br = brw.Reader br = brw.Reader
} else if brw.Reader.Buffered() > 0 {
// Wrap the network connection to read buffered data in brw.Reader
// before reading from the network connection. This should be rare
// because a client must not send message data before receiving the
// handshake response.
netConn = &brNetConn{br: brw.Reader, Conn: netConn}
} }
buf := bufioWriterBuffer(netConn, brw.Writer) buf := brw.Writer.AvailableBuffer()
var writeBuf []byte var writeBuf []byte
if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 { if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {
@@ -247,20 +256,30 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
} }
p = append(p, "\r\n"...) p = append(p, "\r\n"...)
// Clear deadlines set by HTTP server.
netConn.SetDeadline(time.Time{})
if u.HandshakeTimeout > 0 { if u.HandshakeTimeout > 0 {
netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout)) if err := netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout)); err != nil {
return nil, err
}
} else {
// Clear deadlines set by HTTP server.
if err := netConn.SetDeadline(time.Time{}); err != nil {
return nil, err
}
} }
if _, err = netConn.Write(p); err != nil { if _, err = netConn.Write(p); err != nil {
netConn.Close()
return nil, err return nil, err
} }
if u.HandshakeTimeout > 0 { if u.HandshakeTimeout > 0 {
netConn.SetWriteDeadline(time.Time{}) if err := netConn.SetWriteDeadline(time.Time{}); err != nil {
return nil, err
}
} }
// Success! Set netConn to nil to stop the deferred function above from
// closing the network connection.
netConn = nil
return c, nil return c, nil
} }
@@ -327,39 +346,28 @@ func IsWebSocketUpgrade(r *http.Request) bool {
tokenListContainsValue(r.Header, "Upgrade", "websocket") tokenListContainsValue(r.Header, "Upgrade", "websocket")
} }
// bufioReaderSize size returns the size of a bufio.Reader. type brNetConn struct {
func bufioReaderSize(originalReader io.Reader, br *bufio.Reader) int { br *bufio.Reader
// This code assumes that peek on a reset reader returns net.Conn
// bufio.Reader.buf[:0]. }
// TODO: Use bufio.Reader.Size() after Go 1.10
br.Reset(originalReader) func (b *brNetConn) Read(p []byte) (n int, err error) {
if p, err := br.Peek(0); err == nil { if b.br != nil {
return cap(p) // Limit read to buferred data.
if n := b.br.Buffered(); len(p) > n {
p = p[:n]
}
n, err = b.br.Read(p)
if b.br.Buffered() == 0 {
b.br = nil
}
return n, err
} }
return 0 return b.Conn.Read(p)
} }
// writeHook is an io.Writer that records the last slice passed to it vio // NetConn returns the underlying connection that is wrapped by b.
// io.Writer.Write. func (b *brNetConn) NetConn() net.Conn {
type writeHook struct { return b.Conn
p []byte
} }
func (wh *writeHook) Write(p []byte) (int, error) {
wh.p = p
return len(p), nil
}
// bufioWriterBuffer grabs the buffer from a bufio.Writer.
func bufioWriterBuffer(originalWriter io.Writer, bw *bufio.Writer) []byte {
// This code assumes that bufio.Writer.buf[:1] is passed to the
// bufio.Writer's underlying writer.
var wh writeHook
bw.Reset(&wh)
bw.WriteByte(0)
bw.Flush()
bw.Reset(originalWriter)
return wh.p[:cap(wh.p)]
}

View File

@@ -1,21 +0,0 @@
//go:build go1.17
// +build go1.17
package websocket
import (
"context"
"crypto/tls"
)
func doHandshake(ctx context.Context, tlsConn *tls.Conn, cfg *tls.Config) error {
if err := tlsConn.HandshakeContext(ctx); err != nil {
return err
}
if !cfg.InsecureSkipVerify {
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
return err
}
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More