├── scripts ├── test ├── entry ├── build ├── ci ├── checksum ├── version.sh ├── package-image └── build-plugins ├── .gitignore ├── Makefile ├── pkg ├── util │ └── strings.go ├── tarfile │ ├── readcloser.go │ └── tarfile.go ├── registries │ ├── types.go │ ├── endpoint.go │ ├── registries.go │ ├── registries_test.go │ └── endpoint_test.go ├── credentialprovider │ └── plugin │ │ └── plugin.go └── extract │ ├── extract_test.go │ └── extract.go ├── package ├── Dockerfile └── config.yaml ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── Dockerfile.dapper ├── README.md ├── main.go ├── go.mod ├── LICENSE └── go.sum /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -x 3 | 4 | cd $(dirname $0)/.. 5 | 6 | go test -v ./... 7 | -------------------------------------------------------------------------------- /scripts/entry: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | mkdir -p bin dist 5 | if [ -e ./scripts/$1 ]; then 6 | ./scripts/"$@" 7 | else 8 | exec "$@" 9 | fi 10 | 11 | chown -R $DAPPER_UID:$DAPPER_GID . 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | /.dapper 3 | /.tags 4 | /.idea 5 | /.trash-cache 6 | /.vagrant 7 | /.kube 8 | /.cache 9 | /.docker 10 | /.*_history 11 | /.viminfo 12 | /.lesshst 13 | /*.log 14 | /.test 15 | /bin 16 | /dist 17 | /wharfie 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGETS := $(shell ls scripts | grep -vF .sh) 2 | 3 | .dapper: 4 | @echo Downloading dapper 5 | @curl -sL https://releases.rancher.com/dapper/v0.5.1/dapper-$$(uname -s)-$$(uname -m) > .dapper.tmp 6 | @@chmod +x .dapper.tmp 7 | @./.dapper.tmp -v 8 | @mv .dapper.tmp .dapper 9 | 10 | $(TARGETS): .dapper 11 | ./.dapper $@ 12 | 13 | .DEFAULT_GOAL := ci 14 | -------------------------------------------------------------------------------- /pkg/util/strings.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strings" 4 | 5 | // HasSuffixI returns true if string s has any of the given suffixes, ignoring case. 6 | func HasSuffixI(s string, suffixes ...string) bool { 7 | s = strings.ToLower(s) 8 | for _, suffix := range suffixes { 9 | if strings.HasSuffix(s, strings.ToLower(suffix)) { 10 | return true 11 | } 12 | } 13 | return false 14 | } 15 | -------------------------------------------------------------------------------- /package/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.20 AS base 2 | RUN apk add -U ca-certificates 3 | RUN mkdir -p /image/etc/ssl/certs /image/bin && \ 4 | cp /etc/ssl/certs/ca-certificates.crt /image/etc/ssl/certs/ca-certificates.crt 5 | COPY bin/wharfie /image/bin 6 | COPY bin/plugins /image/bin/plugins 7 | COPY package/config.yaml /image/etc/ 8 | 9 | FROM scratch 10 | COPY --from=base /image / 11 | ENTRYPOINT ["/bin/wharfie"] 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | 7 | name: Build 8 | permissions: 9 | contents: read 10 | jobs: 11 | build-cross-arch: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Cross Arch Build 18 | run: | 19 | make ci 20 | env: 21 | CROSS: "true" 22 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -x 3 | 4 | cd $(dirname $0)/.. 5 | . scripts/version.sh 6 | 7 | TAGS="netcgo osusergo static_build" 8 | LDFLAGS="-w -s -X main.version=$VERSION" 9 | CGO_ENABLED=0 go build -v -tags "$TAGS" -ldflags "$LDFLAGS" -o bin/wharfie-amd64 10 | 11 | if [ "$CROSS" = "true" ] && [ "$ARCH" = "amd64" ]; then 12 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -tags "$TAGS" -ldflags "$LDFLAGS" -o bin/wharfie-arm64 13 | fi 14 | -------------------------------------------------------------------------------- /scripts/ci: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -x 3 | 4 | cd $(dirname $0)/.. 5 | . scripts/version.sh 6 | 7 | ./scripts/test 8 | ./scripts/build 9 | ./scripts/build-plugins 10 | 11 | mkdir -p dist/artifacts 12 | 13 | for FILE in bin/wharfie* bin/plugins/*; do 14 | cp ${FILE} dist/artifacts/$(basename $FILE) 15 | done 16 | 17 | sed "s/ - name: .*/&-amd64/" package/config.yaml > dist/artifacts/config-amd64.yaml 18 | sed "s/ - name: .*/&-arm64/" package/config.yaml > dist/artifacts/config-arm64.yaml 19 | 20 | source scripts/checksum 21 | 22 | ARCH=amd64 checksum 23 | ARCH=arm64 checksum -------------------------------------------------------------------------------- /scripts/checksum: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | cd $(dirname $0)/.. 5 | 6 | source ./scripts/version.sh 7 | 8 | CHECKSUM_DIR=${CHECKSUM_DIR:-./dist/artifacts} 9 | 10 | function checksum(){ 11 | 12 | sumfile="${CHECKSUM_DIR}/sha256sum-${ARCH}.txt" 13 | echo -n "" > "${sumfile}" 14 | 15 | files=$(ls ${CHECKSUM_DIR} | grep "${ARCH}" | grep -v "sha256sum-${ARCH}.txt") 16 | for file in ${files}; do 17 | sha256sum "${CHECKSUM_DIR}/${file}" | sed "s;$(dirname ${CHECKSUM_DIR}/${file})/;;g" >> "${sumfile}" 18 | done 19 | 20 | cat "${sumfile}" 21 | } -------------------------------------------------------------------------------- /scripts/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -n "$(git status --porcelain --untracked-files=no)" ]; then 4 | DIRTY="-dirty" 5 | fi 6 | 7 | COMMIT=$(git rev-parse --short HEAD) 8 | GIT_TAG=${GITHUB_ACTION_TAG:-$(git tag -l --contains HEAD | head -n 1)} 9 | 10 | if [[ -z "$DIRTY" && -n "$GIT_TAG" ]]; then 11 | VERSION=$GIT_TAG 12 | else 13 | VERSION="${COMMIT}${DIRTY}" 14 | fi 15 | 16 | if [ -z "$ARCH" ]; then 17 | ARCH=$(go env GOARCH) 18 | fi 19 | 20 | if [ ${ARCH} = armv7l ] || [ ${ARCH} = arm ]; then 21 | export GOARCH="arm" 22 | export GOARM="7" 23 | fi 24 | -------------------------------------------------------------------------------- /Dockerfile.dapper: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine 2 | 3 | ARG ARCH=amd64 4 | 5 | RUN apk -U --no-cache add bash git gcc musl-dev docker-cli zlib-dev zlib-static zstd gzip alpine-sdk binutils-gold 6 | 7 | ENV DAPPER_RUN_ARGS --privileged -v wharfie-pkg:/go/pkg -v wharfie-cache:/go/src/github.com/rancher/wharfie/.cache 8 | ENV DAPPER_ENV ARCH REPO TAG DRONE_TAG IMAGE_NAME CROSS SKIP_VALIDATE 9 | ENV DAPPER_SOURCE /go/src/github.com/rancher/wharfie/ 10 | ENV DAPPER_OUTPUT ./bin ./dist 11 | ENV DAPPER_DOCKER_SOCKET true 12 | ENV HOME ${DAPPER_SOURCE} 13 | WORKDIR ${DAPPER_SOURCE} 14 | ENTRYPOINT ["./scripts/entry"] 15 | CMD ["ci"] 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*" 5 | 6 | env: 7 | GH_TOKEN: ${{ github.token }} 8 | GITHUB_ACTION_TAG: ${{ github.ref_name }} 9 | 10 | name: Release 11 | permissions: 12 | contents: write 13 | id-token: write 14 | jobs: 15 | release-cross-arch: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Cross Arch Build 22 | run: | 23 | make ci 24 | env: 25 | CROSS: "true" 26 | 27 | - name: release binaries 28 | run: | 29 | gh release upload ${{ github.ref_name }} dist/artifacts/* 30 | -------------------------------------------------------------------------------- /scripts/package-image: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -x 3 | 4 | cd $(dirname $0)/.. 5 | . scripts/version.sh 6 | 7 | ARCH=${ARCH:-amd64} 8 | VERSION_TAG="$(sed -e 's/+/-/g' <<< "$VERSION")" 9 | TAG=${TAG:-${VERSION_TAG}-${ARCH}} 10 | REPO=${REPO:-rancher} 11 | IMAGE_NAME=${IMAGE_NAME:-wharfie} 12 | 13 | IMAGE=${REPO}/${IMAGE_NAME}:${TAG} 14 | PROXY_OPTS= 15 | [ -z "$http_proxy" ] || PROXY_OPTS="$PROXY_OPTS --build-arg http_proxy=$http_proxy" 16 | [ -z "$https_proxy" ] || PROXY_OPTS="$PROXY_OPTS --build-arg https_proxy=$https_proxy" 17 | [ -z "$no_proxy" ] || PROXY_OPTS="$PROXY_OPTS --build-arg no_proxy=$no_proxy" 18 | docker build ${PROXY_OPTS} -t ${IMAGE} -f package/Dockerfile . 19 | echo Built ${IMAGE} 20 | -------------------------------------------------------------------------------- /scripts/build-plugins: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -x 3 | 4 | cd $(dirname $0)/.. 5 | . scripts/version.sh 6 | 7 | BASE=$(pwd) 8 | LDFLAGS="-w -s" 9 | TAGS="netcgo osusergo static_build" 10 | 11 | mkdir -p ${BASE}/build/plugins ${BASE}/bin/plugins 12 | 13 | cd ${BASE}/build/plugins 14 | git clone -b v1.26.0 --depth 1 https://github.com/kubernetes/cloud-provider-aws.git 15 | cd cloud-provider-aws 16 | CGO_ENABLED=0 go build -v -tags="$TAGS" -ldflags="$LDFLAGS" -o=${BASE}/bin/plugins/ecr-credential-provider-amd64 cmd/ecr-credential-provider/*.go 17 | if [ "$CROSS" = "true" ] && [ "$ARCH" = "amd64" ]; then 18 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -v -tags="$TAGS" -ldflags="$LDFLAGS" -o=${BASE}/bin/plugins/ecr-credential-provider-arm64 cmd/ecr-credential-provider/*.go 19 | fi 20 | 21 | cd ${BASE}/build/plugins 22 | git clone -b providers/v0.25.5 --depth 1 https://github.com/kubernetes/cloud-provider-gcp.git 23 | cd cloud-provider-gcp 24 | CGO_ENABLED=0 go build -v -tags "$TAGS" -ldflags "$LDFLAGS" -o=${BASE}/bin/plugins/auth-provider-gcp-amd64 cmd/auth-provider-gcp/*.go 25 | if [ "$CROSS" = "true" ] && [ "$ARCH" = "amd64" ]; then 26 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -v -tags "$TAGS" -ldflags "$LDFLAGS" -o=${BASE}/bin/plugins/auth-provider-gcp-arm64 cmd/auth-provider-gcp/*.go 27 | fi 28 | 29 | cd ${BASE}/build/plugins 30 | git clone -b v1.26.0 --depth 1 https://github.com/kubernetes-sigs/cloud-provider-azure.git 31 | cd cloud-provider-azure 32 | CGO_ENABLED=0 go build -v -tags "$TAGS" -ldflags "$LDFLAGS" -o=${BASE}/bin/plugins/acr-credential-provider-amd64 cmd/acr-credential-provider/*.go 33 | if [ "$CROSS" = "true" ] && [ "$ARCH" = "amd64" ]; then 34 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -v -tags "$TAGS" -ldflags "$LDFLAGS" -o=${BASE}/bin/plugins/acr-credential-provider-arm64 cmd/acr-credential-provider/*.go 35 | fi -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wharfie 2 | Utility libraries to provide additional functionality for users of go-containerregistry. Also includes a basic command-line app demonstrating use of the library code. 3 | 4 | ### cli 5 | 6 | ```console 7 | NAME: 8 | wharfie - pulls and unpacks a container image to the local filesystem 9 | 10 | USAGE: 11 | wharfie [global options] command [command options] 12 | 13 | VERSION: 14 | v0.3.1 15 | 16 | DESCRIPTION: 17 | Supports K3s/RKE2 style repository rewrites, endpoint overrides, and auth configuration. 18 | Supports optional loading from local image tarballs or layer cache. 19 | Supports Kubelet credential provider plugins. 20 | 21 | COMMANDS: 22 | help, h Shows a list of commands or help for one command 23 | 24 | GLOBAL OPTIONS: 25 | --private-registry value Private registry configuration file (default: "/etc/rancher/common/registries.yaml") 26 | --images-dir value Images tarball directory 27 | --cache Enable layer cache when image is not available locally 28 | --cache-dir value Layer cache directory (default: "$XDG_CACHE_HOME/rancher/wharfie") 29 | --image-credential-provider-config value Image credential provider configuration file 30 | --image-credential-provider-bin-dir value Image credential provider binary directory 31 | --debug Enable debug logging 32 | --help, -h show help 33 | --version, -v print the version 34 | ``` 35 | 36 | ### image credential providers 37 | 38 | ([KEP-2133](https://github.com/kubernetes/enhancements/issues/2133)) [kubelet image credential providers](https://kubernetes.io/docs/tasks/kubelet-credential-provider/kubelet-credential-provider/) are supported. 39 | At the time of this writing, none of the out-of-tree cloud providers offer standalone binaries. The wharfie docker image (available by running `make package-image`) bundles provider plugins at `/bin/plugins`, 40 | with a sample config file at `/etc/config.yaml`. 41 | 42 | More information is available at: 43 | * https://github.com/kubernetes/cloud-provider-aws/tree/master/cmd/ecr-credential-provider 44 | * https://github.com/kubernetes/cloud-provider-gcp/tree/master/cmd/auth-provider-gcp 45 | * https://github.com/kubernetes-sigs/cloud-provider-azure/tree/master/cmd/acr-credential-provider 46 | -------------------------------------------------------------------------------- /pkg/tarfile/readcloser.go: -------------------------------------------------------------------------------- 1 | package tarfile 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/klauspost/compress/zstd" 8 | ) 9 | 10 | // Explicit interface checks 11 | var _ io.ReadCloser = &zstdReadCloser{} 12 | var _ io.ReadCloser = &multiReadCloser{} 13 | var _ io.ReadCloser = &splitReadCloser{} 14 | 15 | // ZstdReadCloser implements the ReadCloser interface for zstd. The zstd decompressor's Close() 16 | // method doesn't have a return value and therefore doesn't match the ReadCloser interface, so we 17 | // have to wrap it in our own ReadCloser that doesn't expect a return value. We also need to close 18 | // the underlying filehandle. 19 | func ZstdReadCloser(r *zstd.Decoder, c io.Closer) io.ReadCloser { 20 | return zstdReadCloser{r, c} 21 | } 22 | 23 | type zstdReadCloser struct { 24 | r *zstd.Decoder 25 | c io.Closer 26 | } 27 | 28 | func (z zstdReadCloser) Read(p []byte) (int, error) { 29 | return z.r.Read(p) 30 | } 31 | 32 | func (z zstdReadCloser) Close() error { 33 | z.r.Close() 34 | return z.c.Close() 35 | } 36 | 37 | // MultiReadCloser implements the ReadCloser interface for decompressors that need to be closed. 38 | // Some decompressors implement a Close function that needs to be called to clean up resources or 39 | // verify checksums, but we also need to ensure that the underlying file gets closed as well. 40 | func MultiReadCloser(r io.ReadCloser, c io.Closer) io.ReadCloser { 41 | return multiReadCloser{r, c} 42 | } 43 | 44 | type multiReadCloser struct { 45 | r io.ReadCloser 46 | c io.Closer 47 | } 48 | 49 | func (m multiReadCloser) Read(p []byte) (int, error) { 50 | return m.r.Read(p) 51 | } 52 | 53 | func (m multiReadCloser) Close() error { 54 | var errs []error 55 | if err := m.r.Close(); err != nil { 56 | errs = append(errs, err) 57 | } 58 | if err := m.c.Close(); err != nil { 59 | errs = append(errs, err) 60 | } 61 | return fmt.Errorf("errors closing: %v", errs) 62 | } 63 | 64 | // SplitReadCloser implements the ReadCloser interface for decompressors that don't need to be 65 | // closed. Some decompressors don't implement a Close function, so we just need to ensure that the 66 | // underlying file gets closed. 67 | func SplitReadCloser(r io.Reader, c io.Closer) io.ReadCloser { 68 | return splitReadCloser{r, c} 69 | } 70 | 71 | type splitReadCloser struct { 72 | r io.Reader 73 | c io.Closer 74 | } 75 | 76 | func (s splitReadCloser) Read(p []byte) (int, error) { 77 | return s.r.Read(p) 78 | } 79 | 80 | func (s splitReadCloser) Close() error { 81 | return s.c.Close() 82 | } 83 | -------------------------------------------------------------------------------- /pkg/registries/types.go: -------------------------------------------------------------------------------- 1 | package registries 2 | 3 | // Mirror contains the config related to the registry mirror 4 | type Mirror struct { 5 | // Endpoints are endpoints for a namespace. CRI plugin will try the endpoints 6 | // one by one until a working one is found. The endpoint must be a valid url 7 | // with host specified. 8 | // The scheme, host and path from the endpoint URL will be used. 9 | Endpoints []string `toml:"endpoint" yaml:"endpoint" json:"endpoint"` 10 | 11 | // Rewrites are repository rewrite rules for a namespace. When fetching image resources 12 | // from an endpoint and a key matches the repository via regular expression matching 13 | // it will be replaced with the corresponding value from the map in the resource request. 14 | Rewrites map[string]string `toml:"rewrite" yaml:"rewrite" json:"rewrite"` 15 | } 16 | 17 | // AuthConfig contains the config related to authentication to a specific registry 18 | type AuthConfig struct { 19 | // Username is the username to login the registry. 20 | Username string `toml:"username" yaml:"username" json:"username"` 21 | // Password is the password to login the registry. 22 | Password string `toml:"password" yaml:"password" json:"password"` 23 | // Auth is a base64 encoded string from the concatenation of the username, 24 | // a colon, and the password. 25 | Auth string `toml:"auth" yaml:"auth" json:"auth"` 26 | // IdentityToken is used to authenticate the user and get 27 | // an access token for the registry. 28 | IdentityToken string `toml:"identitytoken" yaml:"identity_token" json:"identitytoken"` 29 | } 30 | 31 | // TLSConfig contains the CA/Cert/Key used for a registry 32 | type TLSConfig struct { 33 | CAFile string `toml:"ca_file" yaml:"ca_file" json:"ca_file"` 34 | CertFile string `toml:"cert_file" yaml:"cert_file" json:"cert_file"` 35 | KeyFile string `toml:"key_file" yaml:"key_file" json:"key_file"` 36 | InsecureSkipVerify bool `toml:"insecure_skip_verify" yaml:"insecure_skip_verify" json:"insecure_skip_verify"` 37 | } 38 | 39 | // Registry is registry settings including mirrors, TLS, and credentials 40 | type Registry struct { 41 | // Mirrors are namespace to mirror mapping for all namespaces. 42 | Mirrors map[string]Mirror `toml:"mirrors" yaml:"mirrors" json:"mirrors"` 43 | // Configs are configs for each registry. 44 | // The key is the FDQN or IP of the registry. 45 | Configs map[string]RegistryConfig `toml:"configs" yaml:"configs" json:"configs"` 46 | 47 | // Auths are registry endpoint to auth config mapping. The registry endpoint must 48 | // be a valid url with host specified. 49 | // DEPRECATED: Use Configs instead. Remove in containerd 1.4. 50 | Auths map[string]AuthConfig `toml:"auths" yaml:"auths" json:"auths"` 51 | } 52 | 53 | // RegistryConfig contains configuration used to communicate with the registry. 54 | type RegistryConfig struct { 55 | // Auth contains information to authenticate to the registry. 56 | Auth *AuthConfig `toml:"auth" yaml:"auth" json:"auth"` 57 | // TLS is a pair of CA/Cert/Key which then are used when creating the transport 58 | // that communicates with the registry. 59 | TLS *TLSConfig `toml:"tls" yaml:"tls" json:"tls"` 60 | } 61 | -------------------------------------------------------------------------------- /pkg/credentialprovider/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/sirupsen/logrus" 9 | 10 | "github.com/google/go-containerregistry/pkg/authn" 11 | authenticationv1 "k8s.io/api/authentication/v1" 12 | corev1 "k8s.io/api/core/v1" 13 | "k8s.io/klog/v2" 14 | kubecredentialprovider "k8s.io/kubernetes/pkg/credentialprovider" 15 | kubeplugin "k8s.io/kubernetes/pkg/credentialprovider/plugin" 16 | ) 17 | 18 | type pluginWrapper struct { 19 | k kubecredentialprovider.DockerKeyring 20 | } 21 | 22 | // Explicit interface checks 23 | var _ authn.Keychain = &pluginWrapper{} 24 | 25 | // RegisterCredentialProviderPlugins loads the provided configuration into the credentialprovider plugin registry 26 | // If the configuration is not valid or any configured plugins are missing, an error will be raised. 27 | func RegisterCredentialProviderPlugins(imageCredentialProviderConfigFile, imageCredentialProviderBinDir string) (*pluginWrapper, error) { 28 | klogSetup() 29 | // Upstream code does not check if the functions are ever nil before calling them, so stubs are required. 30 | blankTokenFunc := func(_, _ string, _ *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { 31 | return nil, fmt.Errorf("get service account token is not implemented") 32 | } 33 | blankSAFunc := func(_, _ string) (*corev1.ServiceAccount, error) { 34 | return nil, fmt.Errorf("get service account is not implemented") 35 | } 36 | if err := kubeplugin.RegisterCredentialProviderPlugins(imageCredentialProviderConfigFile, imageCredentialProviderBinDir, blankTokenFunc, blankSAFunc); err != nil { 37 | return nil, errors.Wrap(err, "failed to register CRI auth plugins") 38 | } 39 | return &pluginWrapper{k: kubecredentialprovider.NewDefaultDockerKeyring()}, nil 40 | } 41 | 42 | // Resolve returns an authenticator for the authn.Keychain interface. The authenticator provides 43 | // credentials to a registry by calling the credentialprovider plugin registry's Lookup method, 44 | // which in turn consults the configuration and executes plugins to obtain credentials. 45 | func (p *pluginWrapper) Resolve(target authn.Resource) (authn.Authenticator, error) { 46 | // Lookup may provide multiple AuthConfigs (for credential rotation support) but the Keychain interface only allows us to return one. 47 | if configs, ok := p.k.Lookup(target.String()); ok { 48 | return authn.FromConfig(authn.AuthConfig{ 49 | Username: configs[0].Username, 50 | Password: configs[0].Password, 51 | Auth: configs[0].Auth, 52 | IdentityToken: configs[0].IdentityToken, 53 | RegistryToken: configs[0].RegistryToken, 54 | }), nil 55 | } 56 | 57 | return authn.Anonymous, nil 58 | } 59 | 60 | // klogSetup syncs the klog verbosity to the current Logrus log level. This is necessary because the 61 | // auth plugin stuff all uses klog/v2 and there's no good translation layer between logrus and klog. 62 | func klogSetup() { 63 | klogFlags := flag.NewFlagSet("klog", flag.ContinueOnError) 64 | klog.InitFlags(klogFlags) 65 | if logrus.IsLevelEnabled(logrus.DebugLevel) { 66 | _ = klogFlags.Set("v", "9") 67 | } 68 | _ = klogFlags.Parse(nil) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/registries/endpoint.go: -------------------------------------------------------------------------------- 1 | package registries 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/google/go-containerregistry/pkg/authn" 9 | "github.com/google/go-containerregistry/pkg/name" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | var _ authn.Keychain = &endpoint{} 14 | var _ http.RoundTripper = &endpoint{} 15 | 16 | const ( 17 | defaultRegistry = "docker.io" 18 | defaultRegistryHost = "index.docker.io" 19 | ) 20 | 21 | type endpoint struct { 22 | auth authn.Authenticator 23 | keychain authn.Keychain 24 | ref name.Reference 25 | registry *registry 26 | url *url.URL 27 | } 28 | 29 | // Resolve returns an authenticator for the authn.Keychain interface. The authenticator 30 | // provides credentials to a registry by returning the credentials from mirror endpoints. 31 | // If there were no credentials provided for this endpoint, the default keychain is used 32 | // as a fallback, followed by simply anonymous access. 33 | func (e endpoint) Resolve(target authn.Resource) (authn.Authenticator, error) { 34 | if e.auth != nil && e.auth != authn.Anonymous { 35 | return e.auth, nil 36 | } 37 | if e.keychain != nil { 38 | return e.keychain.Resolve(target) 39 | } 40 | return authn.Anonymous, nil 41 | } 42 | 43 | // RoundTrip handles making a request to an endpoint. It is responsible for rewriting the request 44 | // URL to reflect the scheme, host, and path specified in the endpoint config. The transport itself 45 | // will be retrieved from the registry config, potentially using a cached entry. 46 | func (e endpoint) RoundTrip(req *http.Request) (*http.Response, error) { 47 | endpointURL := e.url 48 | originalURL := req.URL.String() 49 | 50 | // Only rewrite the URL if the request is being made against the original registry host. 51 | // We might have been redirected to a different URL as part of the auth 52 | // workflow, and must not rewrite URLs if that's the case. 53 | if e.ref.Context().RegistryStr() == req.URL.Host { 54 | if strings.HasPrefix(req.URL.Path, "/v2") { 55 | // The default base path is /v2; if a path is included in the endpoint, 56 | // replace the /v2 prefix from the request path with the endpoint path. 57 | // This behavior is cribbed from containerd. 58 | if endpointURL.Path != "" { 59 | req.URL.Path = endpointURL.Path + strings.TrimPrefix(req.URL.Path, "/v2") 60 | 61 | // If either URL has RawPath set (due to the path including urlencoded 62 | // characters), it also needs to be used to set the combined URL 63 | if endpointURL.RawPath != "" || req.URL.RawPath != "" { 64 | endpointPath := endpointURL.Path 65 | if endpointURL.RawPath != "" { 66 | endpointPath = endpointURL.RawPath 67 | } 68 | reqPath := req.URL.Path 69 | if req.URL.RawPath != "" { 70 | reqPath = req.URL.RawPath 71 | } 72 | req.URL.RawPath = endpointPath + strings.TrimPrefix(reqPath, "/v2") 73 | } 74 | } 75 | } 76 | 77 | // set ns from original host if the request is being proxied 78 | if ns := getNamespace(req.Host); isProxy(endpointURL.Host, ns) { 79 | q := req.URL.Query() 80 | q.Set("ns", ns) 81 | req.URL.RawQuery = q.Encode() 82 | } 83 | 84 | // override request host and scheme 85 | req.Host = endpointURL.Host 86 | req.URL.Host = endpointURL.Host 87 | req.URL.Scheme = endpointURL.Scheme 88 | } 89 | 90 | if newURL := req.URL.String(); originalURL != newURL { 91 | logrus.Debugf("Registry endpoint URL modified: %s => %s", originalURL, newURL) 92 | } 93 | return e.registry.getTransport(req.URL).RoundTrip(req) 94 | } 95 | 96 | // isDefault returns true if this endpoint is the default endpoint for the image - 97 | // does the registry namespace match the mirror endpoint namespace? 98 | func (e endpoint) isDefault() bool { 99 | return getNamespace(e.ref.Context().RegistryStr()) == getNamespace(e.url.Host) 100 | } 101 | 102 | func getNamespace(host string) string { 103 | if host == defaultRegistryHost { 104 | return defaultRegistry 105 | } 106 | return host 107 | } 108 | 109 | func isProxy(host, ns string) bool { 110 | if ns != "" && ns != host { 111 | if ns != defaultRegistry || host != defaultRegistryHost { 112 | return true 113 | } 114 | } 115 | return false 116 | } 117 | -------------------------------------------------------------------------------- /package/config.yaml: -------------------------------------------------------------------------------- 1 | kind: CredentialProviderConfig 2 | apiVersion: kubelet.config.k8s.io/v1alpha1 3 | # providers is a list of credential provider plugins that will be enabled by the kubelet. 4 | # Multiple providers may match against a single image, in which case credentials 5 | # from all providers will be returned to the kubelet. If multiple providers are called 6 | # for a single image, the results are combined. If providers return overlapping 7 | # auth keys, the value from the provider earlier in this list is used. 8 | providers: 9 | # name is the required name of the credential provider. It must match the name of the 10 | # provider executable as seen by the kubelet. The executable must be in the kubelet's 11 | # bin directory (set by the --image-credential-provider-bin-dir flag). 12 | - name: ecr-credential-provider 13 | # matchImages is a required list of strings used to match against images in order to 14 | # determine if this provider should be invoked. If one of the strings matches the 15 | # requested image from the kubelet, the plugin will be invoked and given a chance 16 | # to provide credentials. Images are expected to contain the registry domain 17 | # and URL path. 18 | # 19 | # Each entry in matchImages is a pattern which can optionally contain a port and a path. 20 | # Globs can be used in the domain, but not in the port or the path. Globs are supported 21 | # as subdomains like '*.k8s.io' or 'k8s.*.io', and top-level-domains such as 'k8s.*'. 22 | # Matching partial subdomains like 'app*.k8s.io' is also supported. Each glob can only match 23 | # a single subdomain segment, so *.io does not match *.k8s.io. 24 | # 25 | # A match exists between an image and a matchImage when all of the below are true: 26 | # - Both contain the same number of domain parts and each part matches. 27 | # - The URL path of an imageMatch must be a prefix of the target image URL path. 28 | # - If the imageMatch contains a port, then the port must match in the image as well. 29 | # 30 | # Example values of matchImages: 31 | # - 123456789.dkr.ecr.us-east-1.amazonaws.com 32 | # - *.azurecr.io 33 | # - gcr.io 34 | # - *.*.registry.io 35 | # - registry.io:8080/path 36 | matchImages: 37 | - "*.dkr.ecr.*.amazonaws.com" 38 | - "*.dkr.ecr.*.amazonaws.cn" 39 | - "*.dkr.ecr-fips.*.amazonaws.com" 40 | - "*.dkr.ecr.us-iso-east-1.c2s.ic.gov" 41 | - "*.dkr.ecr.us-isob-east-1.sc2s.sgov.gov" 42 | # defaultCacheDuration is the default duration the plugin will cache credentials in-memory 43 | # if a cache duration is not provided in the plugin response. This field is required. 44 | defaultCacheDuration: "12h" 45 | # Required input version of the exec CredentialProviderRequest. The returned CredentialProviderResponse 46 | # MUST use the same encoding version as the input. Current supported values are: 47 | # - credentialprovider.kubelet.k8s.io/v1alpha1 48 | apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1 49 | # Arguments to pass to the command when executing it. 50 | # +optional 51 | args: 52 | - get-credentials 53 | # Env defines additional environment variables to expose to the process. These 54 | # are unioned with the host's environment, as well as variables client-go uses 55 | # to pass argument to the plugin. 56 | # +optional 57 | #env: 58 | #- name: AWS_PROFILE 59 | # value: example_profile 60 | - name: auth-provider-gcp 61 | # matchImages is a required list of strings used to match against images in order to 62 | # determine if this provider should be invoked. If one of the strings matches the 63 | # requested image from the kubelet, the plugin will be invoked and given a chance 64 | # to provide credentials. Images are expected to contain the registry domain 65 | # and URL path. 66 | matchImages: 67 | - "*.gcr.io" 68 | # defaultCacheDuration is the default duration the plugin will cache credentials in-memory 69 | # if a cache duration is not provided in the plugin response. This field is required. 70 | defaultCacheDuration: "12h" 71 | # Required input version of the exec CredentialProviderRequest. The returned CredentialProviderResponse 72 | # MUST use the same encoding version as the input. Current supported values are: 73 | # - credentialprovider.kubelet.k8s.io/v1alpha1 74 | apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1 75 | # Arguments to pass to the command when executing it. 76 | # +optional 77 | args: 78 | - get-credentials 79 | # Env defines additional environment variables to expose to the process. These 80 | # are unioned with the host's environment, as well as variables client-go uses 81 | # to pass argument to the plugin. 82 | # +optional 83 | #env: 84 | -------------------------------------------------------------------------------- /pkg/tarfile/tarfile.go: -------------------------------------------------------------------------------- 1 | package tarfile 2 | 3 | import ( 4 | "compress/bzip2" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/google/go-containerregistry/pkg/name" 13 | v1 "github.com/google/go-containerregistry/pkg/v1" 14 | "github.com/google/go-containerregistry/pkg/v1/tarball" 15 | "github.com/klauspost/compress/zstd" 16 | "github.com/pierrec/lz4" 17 | "github.com/pkg/errors" 18 | "github.com/rancher/wharfie/pkg/util" 19 | "github.com/sirupsen/logrus" 20 | ) 21 | 22 | var ( 23 | ErrNotFound = errors.New("image not found") 24 | // This needs to be kept in sync with the decompressor list 25 | SupportedExtensions = []string{".tar", ".tar.lz4", ".tar.bz2", ".tbz", ".tar.gz", ".tgz", ".tar.zst", ".tzst"} 26 | // The zstd decoder will attempt to use up to 1GB memory for streaming operations by default, 27 | // which is excessive and will OOM low-memory devices. 28 | // NOTE: This must be at least as large as the window size used when compressing tarballs, or you 29 | // will see a "window size exceeded" error when decompressing. The zstd CLI tool uses 4MB by 30 | // default; the --long option defaults to 27 or 128M, which is still too much for a Pi3. 32MB 31 | // (--long=25) has been tested to work acceptably while still compressing by an additional 3-6% on 32 | // our datasets. 33 | MaxDecoderMemory = uint64(1 << 25) 34 | ) 35 | 36 | // FindImage checks tarball files in a given directory for a copy of the referenced image. The image reference must be a Tag, not a Digest. 37 | // The image is retrieved from the first file (ordered by name) that it is found in; there is no preference in terms of compression format. 38 | // If the image is not found in any file in the given directory, a NotFoundError is returned. 39 | func FindImage(imagesDir string, imageRef name.Reference) (v1.Image, error) { 40 | imageTag, ok := imageRef.(name.Tag) 41 | if !ok { 42 | return nil, fmt.Errorf("no local image available for %s: reference is not a tag", imageRef.Name()) 43 | } 44 | 45 | if _, err := os.Stat(imagesDir); err != nil { 46 | if os.IsNotExist(err) { 47 | return nil, errors.Wrapf(ErrNotFound, "no local image available for %s: directory %s does not exist", imageTag.Name(), imagesDir) 48 | } 49 | return nil, err 50 | } 51 | 52 | logrus.Infof("Checking local image archives in %s for %s", imagesDir, imageTag.Name()) 53 | 54 | // Walk the images dir to get a list of tar files. 55 | // dotfiles and files with unsupported extensions are ignored. 56 | files := map[string]os.FileInfo{} 57 | if err := filepath.Walk(imagesDir, func(path string, info os.FileInfo, err error) error { 58 | if err != nil { 59 | return err 60 | } 61 | base := filepath.Base(info.Name()) 62 | if !info.IsDir() && !strings.HasPrefix(base, ".") && util.HasSuffixI(base, SupportedExtensions...) { 63 | files[path] = info 64 | } 65 | return nil 66 | }); err != nil { 67 | return nil, err 68 | } 69 | 70 | // Try to find the requested tag in each file, moving on to the next if there's an error 71 | for fileName := range files { 72 | img, err := findImage(fileName, imageTag) 73 | if err != nil { 74 | logrus.Infof("Failed to find %s in %s: %v", imageTag.Name(), fileName, err) 75 | } 76 | if img != nil { 77 | logrus.Debugf("Found %s in %s", imageTag.Name(), fileName) 78 | return img, nil 79 | } 80 | } 81 | return nil, errors.Wrapf(ErrNotFound, "no local image available for %s: not found in any file in %s", imageTag.Name(), imagesDir) 82 | } 83 | 84 | // findImage returns a handle to an image in a tarfile on disk. 85 | // If the image is not found in the file, an error is returned. 86 | func findImage(fileName string, imageTag name.Tag) (v1.Image, error) { 87 | opener, err := GetOpener(fileName) 88 | if err != nil { 89 | return nil, err 90 | } 91 | return tarball.Image(opener, &imageTag) 92 | } 93 | 94 | // GetOpener returns a function implementing the tarball.Opener interface. 95 | // This is required because compressed tarballs are not seekable, and the image 96 | // reader may need to seek backwards in the file to find a required layer. 97 | // Instead of seeking backwards, it just closes and reopens the file. 98 | // If the file format is not supported, an error is returned. 99 | func GetOpener(fileName string) (tarball.Opener, error) { 100 | var opener tarball.Opener 101 | switch { 102 | case util.HasSuffixI(fileName, ".tar"): 103 | opener = func() (io.ReadCloser, error) { 104 | return os.Open(fileName) 105 | } 106 | case util.HasSuffixI(fileName, ".tar.lz4"): 107 | opener = func() (io.ReadCloser, error) { 108 | file, err := os.Open(fileName) 109 | if err != nil { 110 | return nil, err 111 | } 112 | zr := lz4.NewReader(file) 113 | return SplitReadCloser(zr, file), nil 114 | } 115 | case util.HasSuffixI(fileName, ".tar.bz2", ".tbz"): 116 | opener = func() (io.ReadCloser, error) { 117 | file, err := os.Open(fileName) 118 | if err != nil { 119 | return nil, err 120 | } 121 | zr := bzip2.NewReader(file) 122 | return SplitReadCloser(zr, file), nil 123 | } 124 | case util.HasSuffixI(fileName, ".tar.gz", ".tgz"): 125 | opener = func() (io.ReadCloser, error) { 126 | file, err := os.Open(fileName) 127 | if err != nil { 128 | return nil, err 129 | } 130 | zr, err := gzip.NewReader(file) 131 | if err != nil { 132 | return nil, err 133 | } 134 | return MultiReadCloser(zr, file), nil 135 | } 136 | case util.HasSuffixI(fileName, "tar.zst", ".tzst"): 137 | opener = func() (io.ReadCloser, error) { 138 | file, err := os.Open(fileName) 139 | if err != nil { 140 | return nil, err 141 | } 142 | zr, err := zstd.NewReader(file, zstd.WithDecoderMaxMemory(MaxDecoderMemory)) 143 | if err != nil { 144 | return nil, err 145 | } 146 | return ZstdReadCloser(zr, file), nil 147 | } 148 | default: 149 | return nil, fmt.Errorf("unhandled file type; supported extensions: %s", strings.Join(SupportedExtensions, " ")) 150 | } 151 | return opener, nil 152 | } 153 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/pkg/errors" 12 | 13 | "github.com/google/go-containerregistry/pkg/authn" 14 | "github.com/google/go-containerregistry/pkg/name" 15 | v1 "github.com/google/go-containerregistry/pkg/v1" 16 | "github.com/google/go-containerregistry/pkg/v1/cache" 17 | "github.com/google/go-containerregistry/pkg/v1/remote" 18 | "github.com/rancher/wharfie/pkg/credentialprovider/plugin" 19 | "github.com/rancher/wharfie/pkg/extract" 20 | "github.com/rancher/wharfie/pkg/registries" 21 | "github.com/rancher/wharfie/pkg/tarfile" 22 | "github.com/sirupsen/logrus" 23 | "github.com/urfave/cli/v2" 24 | ) 25 | 26 | var ( 27 | version = "v0.0.0" 28 | ) 29 | 30 | func main() { 31 | app := cli.NewApp() 32 | app.Name = "wharfie" 33 | app.Usage = "pulls and unpacks a container image to the local filesystem" 34 | app.Description = "Supports K3s/RKE2 style repository rewrites, endpoint overrides, and auth configuration. Supports optional loading from local image tarballs or layer cache. Supports Kubelet credential provider plugins." 35 | app.ArgsUsage = " [|] []" 36 | app.Version = version 37 | app.Action = run 38 | app.Flags = []cli.Flag{ 39 | &cli.StringFlag{ 40 | Name: "private-registry", 41 | Usage: "Private registry configuration file", 42 | Value: "/etc/rancher/common/registries.yaml", 43 | }, 44 | &cli.StringFlag{ 45 | Name: "images-dir", 46 | Usage: "Images tarball directory", 47 | }, 48 | &cli.BoolFlag{ 49 | Name: "cache", 50 | Usage: "Enable layer cache when image is not available locally", 51 | }, 52 | &cli.StringFlag{ 53 | Name: "cache-dir", 54 | Usage: "Layer cache directory", 55 | Value: "$XDG_CACHE_HOME/rancher/wharfie", 56 | }, 57 | &cli.StringFlag{ 58 | Name: "image-credential-provider-config", 59 | Usage: "Image credential provider configuration file", 60 | }, 61 | &cli.StringFlag{ 62 | Name: "image-credential-provider-bin-dir", 63 | Usage: "Image credential provider binary directory", 64 | }, 65 | &cli.BoolFlag{ 66 | Name: "debug", 67 | Usage: "Enable debug logging", 68 | }, 69 | &cli.StringFlag{ 70 | Name: "arch", 71 | Usage: "Override the machine architecture", 72 | Value: runtime.GOARCH, 73 | }, 74 | &cli.StringFlag{ 75 | Name: "os", 76 | Usage: "Override the machine operating system", 77 | Value: runtime.GOOS, 78 | }, 79 | } 80 | 81 | if os.Getenv("XDG_CACHE_HOME") == "" && os.Getenv("HOME") != "" { 82 | os.Setenv("XDG_CACHE_HOME", os.ExpandEnv("$HOME/.cache")) 83 | } 84 | 85 | if err := app.Run(os.Args); err != nil { 86 | if !errors.Is(err, context.Canceled) { 87 | logrus.Fatalf("Error: %v", err) 88 | } 89 | } 90 | } 91 | 92 | func run(clx *cli.Context) error { 93 | var img v1.Image 94 | 95 | if clx.Args().Len() < 2 { 96 | fmt.Fprintf(clx.App.Writer, "Incorrect Usage. and are required arguments.\n\n") 97 | cli.ShowAppHelpAndExit(clx, 1) 98 | } 99 | 100 | if clx.Bool("debug") { 101 | logrus.SetLevel(logrus.TraceLevel) 102 | } 103 | 104 | ref, err := name.ParseReference(clx.Args().Get(0)) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | // destination is one or more bare local paths to extract to on the host, or 110 | // image-path:local-path pairs if the content should be extracted to specific 111 | // locations. 112 | dirs := map[string]string{} 113 | for i := 1; i < clx.NArg(); i++ { 114 | var source, destination string 115 | destination = clx.Args().Get(i) 116 | parts := strings.SplitN(destination, ":", 2) 117 | if len(parts) == 2 { 118 | source, destination = parts[0], parts[1] 119 | } else { 120 | source, destination = "/", parts[0] 121 | } 122 | destination, err := filepath.Abs(os.ExpandEnv(destination)) 123 | if err != nil { 124 | return err 125 | } 126 | logrus.Infof("Extract mapping %s => %s", source, destination) 127 | dirs[source] = destination 128 | } 129 | 130 | if clx.IsSet("images-dir") { 131 | imagesDir, err := filepath.Abs(os.ExpandEnv(clx.String("images-dir"))) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | i, err := tarfile.FindImage(imagesDir, ref) 137 | if err != nil && !errors.Is(err, tarfile.ErrNotFound) { 138 | return err 139 | } 140 | img = i 141 | } 142 | 143 | if img == nil { 144 | registry, err := registries.GetPrivateRegistries(clx.String("private-registry")) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | // Next check Kubelet image credential provider plugins, if configured 150 | if clx.IsSet("image-credential-provider-config") && clx.IsSet("image-credential-provider-bin-dir") { 151 | plugins, err := plugin.RegisterCredentialProviderPlugins(clx.String("image-credential-provider-config"), clx.String("image-credential-provider-bin-dir")) 152 | if err != nil { 153 | return err 154 | } 155 | registry.DefaultKeychain = plugins 156 | } else { 157 | // The kubelet image credential provider plugin also falls back to checking legacy Docker credentials, so only 158 | // explicitly set up the go-containerregistry DefaultKeychain if plugins are not configured. 159 | // DefaultKeychain tries to read config from the home dir, and will error if HOME isn't set, so also gate on that. 160 | if os.Getenv("HOME") != "" { 161 | registry.DefaultKeychain = authn.DefaultKeychain 162 | } 163 | } 164 | 165 | logrus.Infof("Pulling image reference %s", ref.Name()) 166 | img, err = registry.Image(ref, remote.WithPlatform(v1.Platform{Architecture: clx.String("arch"), OS: clx.String("os")})) 167 | if err != nil { 168 | return errors.Wrapf(err, "failed to get image reference %s", ref.Name()) 169 | } 170 | 171 | if clx.Bool("cache") { 172 | cacheDir, err := filepath.Abs(os.ExpandEnv(clx.String("cache-dir"))) 173 | if err != nil { 174 | return err 175 | } 176 | logrus.Infof("Using layer cache %s", cacheDir) 177 | imageCache := cache.NewFilesystemCache(cacheDir) 178 | img = cache.Image(img, imageCache) 179 | } 180 | } 181 | 182 | return extract.ExtractDirs(img, dirs) 183 | } 184 | -------------------------------------------------------------------------------- /pkg/extract/extract_test.go: -------------------------------------------------------------------------------- 1 | package extract 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/google/go-containerregistry/pkg/name" 9 | v1 "github.com/google/go-containerregistry/pkg/v1" 10 | "github.com/google/go-containerregistry/pkg/v1/remote" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func init() { 15 | logrus.SetLevel(logrus.DebugLevel) 16 | } 17 | 18 | func TestFindPathFromExtract(t *testing.T) { 19 | tempdir := t.TempDir() 20 | testImageRef := "docker.io/rancher/rke2-runtime:v1.22.4-rke2r1" 21 | ref, err := name.ParseReference(testImageRef) 22 | if err != nil { 23 | t.Fatalf("Failed to parse image reference: %v", err) 24 | } 25 | 26 | testOperatingSystems := map[string]string{ 27 | "linux": "containerd", 28 | "windows": "containerd.exe", 29 | } 30 | 31 | // https://github.com/google/go-containerregistry/commit/f9a1886f3df0e2b00d6c62715114fe1093ab1ad7 32 | // changed go-containerregistry behavior; tar paths are now platform-specific and will have forward 33 | // slashes on Linux and backslashes on Windows. 34 | for operatingSystem, pauseBin := range testOperatingSystems { 35 | image, err := remote.Image(ref, remote.WithPlatform(v1.Platform{Architecture: "amd64", OS: operatingSystem})) 36 | if err != nil { 37 | t.Fatalf("Failed to pull remote image: %v", err) 38 | } 39 | 40 | extractMap := map[string]string{ 41 | "/bin": filepath.Join(tempdir, "bin"), 42 | "/charts": filepath.Join(tempdir, "charts"), 43 | } 44 | 45 | t.Logf("Testing ExtractDirs with map %#v for %s", extractMap, operatingSystem) 46 | if err := ExtractDirs(image, extractMap); err != nil { 47 | t.Errorf("Failed to extract containerd binary for %s: %v", operatingSystem, err) 48 | continue 49 | } 50 | 51 | i, err := os.Stat(filepath.Join(tempdir, "bin", pauseBin)) 52 | if err != nil { 53 | t.Errorf("containerd binary for %s not found: %v", operatingSystem, err) 54 | continue 55 | } 56 | 57 | t.Logf("containerd binary for %s extracted successfully: %s", operatingSystem, i.Name()) 58 | } 59 | } 60 | 61 | func TestFindPath(t *testing.T) { 62 | type mss map[string]string 63 | type testPath struct { 64 | in string 65 | out string 66 | err error 67 | } 68 | temp := os.TempDir() 69 | findPathTests := []struct { 70 | dirs mss 71 | paths []testPath 72 | }{ 73 | { 74 | // test a simple root directory mapping with various valid and invalid paths 75 | dirs: mss{"/": temp}, 76 | paths: []testPath{ 77 | { 78 | in: "/test.txt", 79 | out: filepath.Join(temp, "test.txt"), 80 | err: nil, 81 | }, { 82 | in: "///test.txt", 83 | out: filepath.Join(temp, "test.txt"), 84 | err: nil, 85 | }, { 86 | in: "/etc/../test.txt", 87 | out: filepath.Join(temp, "test.txt"), 88 | err: nil, 89 | }, { 90 | in: "test.txt", 91 | out: filepath.Join(temp, "test.txt"), 92 | err: nil, 93 | }, { 94 | in: "/etc/hosts", 95 | out: filepath.Join(temp, "etc", "hosts"), 96 | err: nil, 97 | }, { 98 | in: "/var/lib/rancher", 99 | out: filepath.Join(temp, "var", "lib", "rancher"), 100 | err: nil, 101 | }, { 102 | in: "../../etc/passwd", 103 | out: "", 104 | err: ErrIllegalPath, 105 | }, 106 | }, 107 | }, { 108 | // test no mapping at all 109 | dirs: mss{}, 110 | paths: []testPath{ 111 | { 112 | in: "/text.txt", 113 | out: "", 114 | err: nil, 115 | }, 116 | }, 117 | }, { 118 | // test mapping various nested paths 119 | dirs: mss{ 120 | "/Files/bin": filepath.Join(temp, "Files-bin"), 121 | "/Files": filepath.Join(temp, "Files"), 122 | "/etc": filepath.Join(temp, "etc"), 123 | }, 124 | paths: []testPath{ 125 | { 126 | in: "Files/bin", 127 | out: filepath.Join(temp, "Files-bin"), 128 | err: nil, 129 | }, { 130 | in: "Files/bin/test.txt", 131 | out: filepath.Join(temp, "Files-bin", "test.txt"), 132 | err: nil, 133 | }, { 134 | in: "Files/bin/aux", 135 | out: filepath.Join(temp, "Files-bin", "aux"), 136 | err: nil, 137 | }, { 138 | in: "Files/bin/aux/mount", 139 | out: filepath.Join(temp, "Files-bin", "aux", "mount"), 140 | err: nil, 141 | }, { 142 | in: "Files", 143 | out: filepath.Join(temp, "Files"), 144 | err: nil, 145 | }, { 146 | in: "Files/test.txt", 147 | out: filepath.Join(temp, "Files", "test.txt"), 148 | err: nil, 149 | }, { 150 | in: "Files/opt", 151 | out: filepath.Join(temp, "Files", "opt"), 152 | err: nil, 153 | }, { 154 | in: "Files/opt/other.txt", 155 | out: filepath.Join(temp, "Files", "opt", "other.txt"), 156 | err: nil, 157 | }, { 158 | in: "etc", 159 | out: filepath.Join(temp, "etc"), 160 | err: nil, 161 | }, { 162 | in: "etc/hosts", 163 | out: filepath.Join(temp, "etc", "hosts"), 164 | err: nil, 165 | }, { 166 | in: "etc/shadow/passwd", 167 | out: filepath.Join(temp, "etc", "shadow", "passwd"), 168 | err: nil, 169 | }, { 170 | in: "sbin", 171 | out: "", 172 | err: nil, 173 | }, { 174 | in: "sbin/ip", 175 | out: "", 176 | err: nil, 177 | }, { 178 | in: "Files/bin/../../../../etc/passwd", 179 | out: "", 180 | err: ErrIllegalPath, 181 | }, 182 | }, 183 | }, 184 | } 185 | 186 | for _, test := range findPathTests { 187 | t.Logf("Testing paths with dirs %#v", test.dirs) 188 | for _, testPath := range test.paths { 189 | dirs, err := cleanExtractDirs(test.dirs) 190 | if err != nil { 191 | t.Errorf("Failed to clean extracted dirs: %v", err) 192 | continue 193 | } 194 | // as of recent go-containerruntime versions, tar file paths are pre-processed with filepath.Clean 195 | in := filepath.Clean(testPath.in) 196 | destination, err := findPath(dirs, in) 197 | t.Logf("Got mapped path %q, err %v for image path %q", destination, err, in) 198 | if destination != testPath.out { 199 | t.Errorf("Expected path %q but got path %q for image path %q", testPath.out, destination, in) 200 | } 201 | if err != testPath.err { 202 | t.Errorf("Expected error %v but got error %v for image path %q", testPath.err, err, in) 203 | } 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /pkg/extract/extract.go: -------------------------------------------------------------------------------- 1 | package extract 2 | 3 | import ( 4 | "archive/tar" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | v1 "github.com/google/go-containerregistry/pkg/v1" 11 | "github.com/google/go-containerregistry/pkg/v1/mutate" 12 | "github.com/pkg/errors" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | var ( 17 | ErrIllegalPath = errors.New("illegal path") 18 | ps = string(os.PathSeparator) 19 | ) 20 | 21 | // An Option modifies the default file extraction behavior 22 | type Option func(*options) error 23 | 24 | type options struct { 25 | mode os.FileMode 26 | } 27 | 28 | // Extract extracts all content from the image to the provided path. 29 | func Extract(img v1.Image, dir string, opts ...Option) error { 30 | dirs := map[string]string{"/": dir} 31 | return ExtractDirs(img, dirs, opts...) 32 | } 33 | 34 | // ExtractDirs extracts content from the image, honoring the directory map when 35 | // deciding where on the local filesystem to place the extracted files. For example: 36 | // {"/bin": "/usr/local/bin", "/etc": "/etc", "/etc/rancher": "/opt/rancher/etc"} 37 | func ExtractDirs(img v1.Image, dirs map[string]string, opts ...Option) error { 38 | opt, err := makeOptions(opts...) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | cleanDirs, err := cleanExtractDirs(dirs) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | reader := mutate.Extract(img) 49 | defer reader.Close() 50 | 51 | // Read from the tar until EOF 52 | t := tar.NewReader(reader) 53 | for { 54 | h, err := t.Next() 55 | if err == io.EOF { 56 | return nil 57 | } else if err != nil { 58 | return err 59 | } 60 | 61 | destination, err := findPath(cleanDirs, h.Name) 62 | parent := filepath.Dir(destination) 63 | if err != nil { 64 | return errors.Wrapf(err, "unable to extract file %s", h.Name) 65 | } 66 | if destination == "" { 67 | logrus.Debugf("Skipping file %s", h.Name) 68 | continue 69 | } 70 | 71 | switch h.Typeflag { 72 | case tar.TypeDir: 73 | logrus.Infof("Creating directory %s", destination) 74 | if err := os.MkdirAll(destination, opt.mode); err != nil { 75 | return err 76 | } 77 | case tar.TypeReg: 78 | logrus.Infof("Extracting file %s to %s", h.Name, destination) 79 | mode := h.FileInfo().Mode() & opt.mode 80 | if mode == 0 { 81 | // images tarfiles created on Windows have empty mode bits, which when round-tripped 82 | // results in creating files that are marked read-only. In this case, use the 83 | // requested mode instead of masking. 84 | mode = opt.mode 85 | } 86 | if err := os.MkdirAll(parent, opt.mode); err != nil { 87 | return err 88 | } 89 | f, err := os.OpenFile(destination, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | if _, err = io.Copy(f, t); err != nil { 95 | f.Close() 96 | return err 97 | } 98 | if err := f.Close(); err != nil { 99 | return err 100 | } 101 | case tar.TypeSymlink: 102 | logrus.Infof("Symlinking %s to %s", destination, h.Linkname) 103 | if err := os.MkdirAll(parent, opt.mode); err != nil { 104 | return err 105 | } 106 | _ = os.Remove(destination) // blind remove, if it fails the Symlink call will deal with it. 107 | err := os.Symlink(h.Linkname, destination) 108 | if err != nil { 109 | return err 110 | } 111 | case tar.TypeLink: 112 | linkname, err := findPath(cleanDirs, h.Linkname) 113 | if err != nil { 114 | return errors.Wrapf(err, "unable to find target for hardlink %s", destination) 115 | } 116 | if linkname == "" { 117 | logrus.Warnf("Skipping hardlink %s, target was skipped", destination) 118 | continue 119 | } 120 | logrus.Infof("Linking %s to %s", destination, linkname) 121 | if err := os.MkdirAll(parent, opt.mode); err != nil { 122 | return err 123 | } 124 | _ = os.Remove(destination) // blind remove, if it fails the Link call will deal with it. 125 | err = os.Link(linkname, destination) 126 | if err != nil { 127 | return err 128 | } 129 | default: 130 | logrus.Warnf("Unhandled Typeflag %d for %s", h.Typeflag, h.Name) 131 | } 132 | } 133 | } 134 | 135 | // WithMode overrides the default mode used when extracting files and directories. 136 | func WithMode(mode os.FileMode) Option { 137 | return func(o *options) error { 138 | o.mode = mode 139 | return nil 140 | } 141 | } 142 | 143 | // makeOptions applies Options, returning a modified option struct. 144 | func makeOptions(opts ...Option) (*options, error) { 145 | o := &options{ 146 | mode: 0755, 147 | } 148 | for _, option := range opts { 149 | if err := option(o); err != nil { 150 | return nil, err 151 | } 152 | } 153 | return o, nil 154 | } 155 | 156 | // cleanExtractDirs normalizes the directory map to ensure that source and destination 157 | // reliably do not have trailing slashes, unless the path is root. This is required to 158 | // make directory name matching reliable while walking up the source path. 159 | func cleanExtractDirs(dirs map[string]string) (map[string]string, error) { 160 | cleanDirs := make(map[string]string, len(dirs)) 161 | for s, d := range dirs { 162 | if s != ps { 163 | s = filepath.Clean(strings.TrimSuffix(s, ps)) 164 | } 165 | if d != ps { 166 | var err error 167 | d, err = filepath.Abs(strings.TrimSuffix(d, ps)) 168 | if err != nil { 169 | return nil, errors.Wrap(err, "invalid destination") 170 | } 171 | } 172 | cleanDirs[s] = d 173 | } 174 | return cleanDirs, nil 175 | } 176 | 177 | // findPath walks up the path, finding the longest match in the dirs map and returning the desired path. 178 | func findPath(dirs map[string]string, path string) (string, error) { 179 | if !strings.HasPrefix(path, ps) { 180 | path = ps + path 181 | } 182 | 183 | // Depth-first walk up the path to find a matching entry in the map, until we hit the root path separator. 184 | for source := path; ; source = filepath.Dir(source) { 185 | if destination, ok := dirs[source]; ok { 186 | // Trim the source path prefix, replace it with the destination, and normalize the joined result. 187 | joined := filepath.Clean(filepath.Join(destination, strings.TrimPrefix(path, source))) 188 | 189 | // Ensure that the path after cleaning does not escape the target prefix. 190 | if !strings.HasPrefix(joined, destination) { 191 | return "", ErrIllegalPath 192 | } 193 | 194 | return joined, nil 195 | } 196 | if source == ps { 197 | return "", nil 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rancher/wharfie 2 | 3 | go 1.24.6 4 | 5 | replace ( 6 | k8s.io/api => github.com/k3s-io/kubernetes/staging/src/k8s.io/api v1.34.1-k3s1 7 | k8s.io/apiextensions-apiserver => github.com/k3s-io/kubernetes/staging/src/k8s.io/apiextensions-apiserver v1.34.1-k3s1 8 | k8s.io/apimachinery => github.com/k3s-io/kubernetes/staging/src/k8s.io/apimachinery v1.34.1-k3s1 9 | k8s.io/apiserver => github.com/k3s-io/kubernetes/staging/src/k8s.io/apiserver v1.34.1-k3s1 10 | k8s.io/cli-runtime => github.com/k3s-io/kubernetes/staging/src/k8s.io/cli-runtime v1.34.1-k3s1 11 | k8s.io/client-go => github.com/k3s-io/kubernetes/staging/src/k8s.io/client-go v1.34.1-k3s1 12 | k8s.io/cloud-provider => github.com/k3s-io/kubernetes/staging/src/k8s.io/cloud-provider v1.34.1-k3s1 13 | k8s.io/cluster-bootstrap => github.com/k3s-io/kubernetes/staging/src/k8s.io/cluster-bootstrap v1.34.1-k3s1 14 | k8s.io/code-generator => github.com/k3s-io/kubernetes/staging/src/k8s.io/code-generator v1.34.1-k3s1 15 | k8s.io/component-base => github.com/k3s-io/kubernetes/staging/src/k8s.io/component-base v1.34.1-k3s1 16 | k8s.io/component-helpers => github.com/k3s-io/kubernetes/staging/src/k8s.io/component-helpers v1.34.1-k3s1 17 | k8s.io/controller-manager => github.com/k3s-io/kubernetes/staging/src/k8s.io/controller-manager v1.34.1-k3s1 18 | k8s.io/cri-api => github.com/k3s-io/kubernetes/staging/src/k8s.io/cri-api v1.34.1-k3s1 19 | k8s.io/csi-translation-lib => github.com/k3s-io/kubernetes/staging/src/k8s.io/csi-translation-lib v1.34.1-k3s1 20 | k8s.io/kms => github.com/k3s-io/kubernetes/staging/src/k8s.io/kms v1.34.1-k3s1 21 | k8s.io/kube-aggregator => github.com/k3s-io/kubernetes/staging/src/k8s.io/kube-aggregator v1.34.1-k3s1 22 | k8s.io/kube-controller-manager => github.com/k3s-io/kubernetes/staging/src/k8s.io/kube-controller-manager v1.34.1-k3s1 23 | k8s.io/kube-proxy => github.com/k3s-io/kubernetes/staging/src/k8s.io/kube-proxy v1.34.1-k3s1 24 | k8s.io/kube-scheduler => github.com/k3s-io/kubernetes/staging/src/k8s.io/kube-scheduler v1.34.1-k3s1 25 | k8s.io/kubectl => github.com/k3s-io/kubernetes/staging/src/k8s.io/kubectl v1.34.1-k3s1 26 | k8s.io/kubelet => github.com/k3s-io/kubernetes/staging/src/k8s.io/kubelet v1.34.1-k3s1 27 | k8s.io/kubernetes => github.com/k3s-io/kubernetes v1.34.1-k3s1 28 | k8s.io/legacy-cloud-providers => github.com/k3s-io/kubernetes/staging/src/k8s.io/legacy-cloud-providers v1.34.1-k3s1 29 | k8s.io/metrics => github.com/k3s-io/kubernetes/staging/src/k8s.io/metrics v1.34.1-k3s1 30 | k8s.io/mount-utils => github.com/k3s-io/kubernetes/staging/src/k8s.io/mount-utils v1.34.1-k3s1 31 | k8s.io/node-api => github.com/k3s-io/kubernetes/staging/src/k8s.io/node-api v1.34.1-k3s1 32 | k8s.io/sample-apiserver => github.com/k3s-io/kubernetes/staging/src/k8s.io/sample-apiserver v1.34.1-k3s1 33 | k8s.io/sample-cli-plugin => github.com/k3s-io/kubernetes/staging/src/k8s.io/sample-cli-plugin v1.34.1-k3s1 34 | k8s.io/sample-controller => github.com/k3s-io/kubernetes/staging/src/k8s.io/sample-controller v1.34.1-k3s1 35 | ) 36 | 37 | require ( 38 | github.com/google/go-containerregistry v0.20.2 39 | github.com/klauspost/compress v1.18.0 40 | github.com/pierrec/lz4 v2.6.0+incompatible 41 | github.com/pkg/errors v0.9.1 42 | github.com/rancher/dynamiclistener v0.7.2-rc2 43 | github.com/sirupsen/logrus v1.9.3 44 | github.com/stretchr/testify v1.11.1 45 | github.com/urfave/cli/v2 v2.27.5 46 | go.uber.org/multierr v1.11.0 47 | gopkg.in/yaml.v2 v2.4.0 48 | k8s.io/api v0.34.1 49 | k8s.io/klog/v2 v2.130.1 50 | k8s.io/kubernetes v1.34.1 51 | ) 52 | 53 | require ( 54 | cel.dev/expr v0.24.0 // indirect 55 | github.com/Microsoft/go-winio v0.6.2 // indirect 56 | github.com/NYTimes/gziphandler v1.1.1 // indirect 57 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 58 | github.com/beorn7/perks v1.0.1 // indirect 59 | github.com/blang/semver/v4 v4.0.0 // indirect 60 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 61 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 62 | github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect 63 | github.com/coreos/go-semver v0.3.1 // indirect 64 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 65 | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect 66 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 67 | github.com/distribution/reference v0.6.0 // indirect 68 | github.com/docker/cli v27.1.1+incompatible // indirect 69 | github.com/docker/distribution v2.8.2+incompatible // indirect 70 | github.com/docker/docker-credential-helpers v0.7.0 // indirect 71 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 72 | github.com/felixge/httpsnoop v1.0.4 // indirect 73 | github.com/frankban/quicktest v1.12.1 // indirect 74 | github.com/fsnotify/fsnotify v1.9.0 // indirect 75 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect 76 | github.com/go-logr/logr v1.4.2 // indirect 77 | github.com/go-logr/stdr v1.2.2 // indirect 78 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 79 | github.com/go-openapi/jsonreference v0.21.0 // indirect 80 | github.com/go-openapi/swag v0.23.0 // indirect 81 | github.com/gogo/protobuf v1.3.2 // indirect 82 | github.com/golang/protobuf v1.5.4 // indirect 83 | github.com/google/btree v1.1.3 // indirect 84 | github.com/google/cel-go v0.26.0 // indirect 85 | github.com/google/gnostic-models v0.7.0 // indirect 86 | github.com/google/go-cmp v0.7.0 // indirect 87 | github.com/google/uuid v1.6.0 // indirect 88 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 89 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect 90 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 91 | github.com/josharian/intern v1.0.0 // indirect 92 | github.com/json-iterator/go v1.1.12 // indirect 93 | github.com/kylelemons/godebug v1.1.0 // indirect 94 | github.com/mailru/easyjson v0.7.7 // indirect 95 | github.com/mitchellh/go-homedir v1.1.0 // indirect 96 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 97 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 98 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 99 | github.com/opencontainers/go-digest v1.0.0 // indirect 100 | github.com/opencontainers/image-spec v1.1.1 // indirect 101 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 102 | github.com/prometheus/client_golang v1.23.2 // indirect 103 | github.com/prometheus/client_model v0.6.2 // indirect 104 | github.com/prometheus/common v0.66.1 // indirect 105 | github.com/prometheus/procfs v0.16.1 // indirect 106 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 107 | github.com/spf13/cobra v1.9.1 // indirect 108 | github.com/spf13/pflag v1.0.6 // indirect 109 | github.com/stoewer/go-strcase v1.3.0 // indirect 110 | github.com/vbatts/tar-split v0.11.3 // indirect 111 | github.com/x448/float16 v0.8.4 // indirect 112 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 113 | go.etcd.io/etcd/api/v3 v3.6.4 // indirect 114 | go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect 115 | go.etcd.io/etcd/client/v3 v3.6.4 // indirect 116 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 117 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect 118 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect 119 | go.opentelemetry.io/otel v1.35.0 // indirect 120 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect 121 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect 122 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 123 | go.opentelemetry.io/otel/sdk v1.34.0 // indirect 124 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 125 | go.opentelemetry.io/proto/otlp v1.5.0 // indirect 126 | go.uber.org/zap v1.27.0 // indirect 127 | go.yaml.in/yaml/v2 v2.4.2 // indirect 128 | go.yaml.in/yaml/v3 v3.0.4 // indirect 129 | golang.org/x/crypto v0.42.0 // indirect 130 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 131 | golang.org/x/net v0.44.0 // indirect 132 | golang.org/x/oauth2 v0.30.0 // indirect 133 | golang.org/x/sync v0.17.0 // indirect 134 | golang.org/x/sys v0.36.0 // indirect 135 | golang.org/x/term v0.35.0 // indirect 136 | golang.org/x/text v0.29.0 // indirect 137 | golang.org/x/time v0.9.0 // indirect 138 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect 139 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect 140 | google.golang.org/grpc v1.72.1 // indirect 141 | google.golang.org/protobuf v1.36.8 // indirect 142 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 143 | gopkg.in/inf.v0 v0.9.1 // indirect 144 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 145 | gopkg.in/yaml.v3 v3.0.1 // indirect 146 | k8s.io/apiextensions-apiserver v0.34.0 // indirect 147 | k8s.io/apimachinery v0.34.1 // indirect 148 | k8s.io/apiserver v0.34.0 // indirect 149 | k8s.io/client-go v0.34.1 // indirect 150 | k8s.io/cloud-provider v0.34.0 // indirect 151 | k8s.io/component-base v0.34.0 // indirect 152 | k8s.io/component-helpers v0.0.0 // indirect 153 | k8s.io/controller-manager v0.0.0 // indirect 154 | k8s.io/cri-api v0.34.0 // indirect 155 | k8s.io/cri-client v0.34.0 // indirect 156 | k8s.io/kms v0.34.0 // indirect 157 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect 158 | k8s.io/kubelet v0.34.0 // indirect 159 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect 160 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect 161 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 162 | sigs.k8s.io/randfill v1.0.0 // indirect 163 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect 164 | sigs.k8s.io/yaml v1.6.0 // indirect 165 | ) 166 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /pkg/registries/registries.go: -------------------------------------------------------------------------------- 1 | package registries 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "fmt" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "path" 13 | "regexp" 14 | "strings" 15 | "time" 16 | 17 | "github.com/google/go-containerregistry/pkg/authn" 18 | "github.com/google/go-containerregistry/pkg/name" 19 | v1 "github.com/google/go-containerregistry/pkg/v1" 20 | "github.com/google/go-containerregistry/pkg/v1/remote" 21 | "github.com/pkg/errors" 22 | "github.com/sirupsen/logrus" 23 | "go.uber.org/multierr" 24 | "gopkg.in/yaml.v2" 25 | ) 26 | 27 | // registry stores information necessary to configure authentication and 28 | // connections to remote registries, including overriding registry endpoints 29 | type registry struct { 30 | DefaultKeychain authn.Keychain 31 | Registry *Registry 32 | 33 | transports map[string]*http.Transport 34 | } 35 | 36 | // getPrivateRegistries loads private registry configuration from a given file 37 | // If no file exists at the given path, default settings are returned. 38 | // Errors such as unreadable files or unparseable content are raised. 39 | func GetPrivateRegistries(path string) (*registry, error) { 40 | registry := ®istry{ 41 | DefaultKeychain: authn.DefaultKeychain, 42 | Registry: &Registry{}, 43 | transports: map[string]*http.Transport{}, 44 | } 45 | privRegistryFile, err := ioutil.ReadFile(path) 46 | if err != nil { 47 | if os.IsNotExist(err) { 48 | return registry, nil 49 | } 50 | return nil, err 51 | } 52 | logrus.Infof("Using private registry config file at %s", path) 53 | if err := yaml.Unmarshal(privRegistryFile, registry.Registry); err != nil { 54 | return nil, err 55 | } 56 | return registry, nil 57 | } 58 | 59 | func (r *registry) Image(ref name.Reference, options ...remote.Option) (v1.Image, error) { 60 | endpoints, err := r.getEndpoints(ref) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | errs := []error{} 66 | for _, endpoint := range endpoints { 67 | epRef := ref 68 | if !endpoint.isDefault() { 69 | epRef = r.rewrite(ref) 70 | } 71 | logrus.Debugf("Trying endpoint %s", endpoint.url) 72 | endpointOptions := append(options, remote.WithTransport(endpoint), remote.WithAuthFromKeychain(endpoint)) 73 | remoteImage, err := remote.Image(epRef, endpointOptions...) 74 | if err != nil { 75 | logrus.Warnf("Failed to get image from endpoint: %v", err) 76 | errs = append(errs, err) 77 | continue 78 | } 79 | return remoteImage, nil 80 | } 81 | return nil, errors.Wrap(multierr.Combine(errs...), "all endpoints failed") 82 | } 83 | 84 | // rewrite applies repository rewrites to the given image reference. 85 | func (r *registry) rewrite(ref name.Reference) name.Reference { 86 | registry := ref.Context().RegistryStr() 87 | rewrites := r.getRewrites(registry) 88 | repository := ref.Context().RepositoryStr() 89 | 90 | for pattern, replace := range rewrites { 91 | exp, err := regexp.Compile(pattern) 92 | if err != nil { 93 | logrus.Warnf("Failed to compile rewrite `%s` for %s", pattern, registry) 94 | continue 95 | } 96 | if rr := exp.ReplaceAllString(repository, replace); rr != repository { 97 | newRepo, err := name.NewRepository(registry + "/" + rr) 98 | if err != nil { 99 | logrus.Warnf("Invalid repository rewrite %s for %s", rr, registry) 100 | continue 101 | } 102 | if t, ok := ref.(name.Tag); ok { 103 | t.Repository = newRepo 104 | return t 105 | } else if d, ok := ref.(name.Digest); ok { 106 | d.Repository = newRepo 107 | return d 108 | } 109 | } 110 | } 111 | 112 | return ref 113 | } 114 | 115 | // getTransport returns a transport for a given endpoint URL. For HTTP endpoints, 116 | // the default transport is used. For HTTPS endpoints, a unique transport is created 117 | // with the endpoint's TLSConfig (if any), and cached for all connections to this host. 118 | func (r *registry) getTransport(endpointURL *url.URL) http.RoundTripper { 119 | if endpointURL.Scheme == "https" { 120 | // Create and cache transport if not found. 121 | if _, ok := r.transports[endpointURL.Host]; !ok { 122 | tlsConfig, err := r.getTLSConfig(endpointURL) 123 | if err != nil { 124 | logrus.Warnf("Failed to get TLS config for endpoint %v: %v", endpointURL, err) 125 | } 126 | 127 | r.transports[endpointURL.Host] = &http.Transport{ 128 | Proxy: http.ProxyFromEnvironment, 129 | DialContext: (&net.Dialer{ 130 | Timeout: 30 * time.Second, 131 | KeepAlive: 30 * time.Second, 132 | }).DialContext, 133 | TLSClientConfig: tlsConfig, 134 | ForceAttemptHTTP2: true, 135 | MaxIdleConns: 100, 136 | IdleConnTimeout: 90 * time.Second, 137 | TLSHandshakeTimeout: 10 * time.Second, 138 | ExpectContinueTimeout: 1 * time.Second, 139 | } 140 | } 141 | return r.transports[endpointURL.Host] 142 | } 143 | return remote.DefaultTransport 144 | } 145 | 146 | // getEndpoints gets endpoint configurations for an image reference. 147 | // The returned endpoint can be used as both a RoundTripper for requests, and a Keychain for authentication. 148 | // 149 | // Endpoint list generation is copied from containerd. For example, when pulling an image from gcr.io: 150 | // * `gcr.io` is configured: endpoints for `gcr.io` + default endpoint `https://gcr.io/v2`. 151 | // * `*` is configured, and `gcr.io` is not: endpoints for `*` + default endpoint `https://gcr.io/v2`. 152 | // * None of above is configured: default endpoint `https://gcr.io/v2`. 153 | func (r *registry) getEndpoints(ref name.Reference) ([]endpoint, error) { 154 | endpoints := []endpoint{} 155 | registry := ref.Context().RegistryStr() 156 | keys := []string{registry} 157 | if registry == name.DefaultRegistry { 158 | keys = append(keys, "docker.io") 159 | } else if _, _, err := net.SplitHostPort(registry); err != nil { 160 | keys = append(keys, registry+":443", registry+":80") 161 | } 162 | keys = append(keys, "*") 163 | 164 | for _, key := range keys { 165 | if mirror, ok := r.Registry.Mirrors[key]; ok { 166 | for _, endpointStr := range mirror.Endpoints { 167 | if endpointURL, err := normalizeEndpointAddress(endpointStr); err != nil { 168 | logrus.Warnf("Ignoring invalid endpoint %s for registry %s: %v", endpointStr, registry, err) 169 | } else { 170 | endpoints = append(endpoints, r.makeEndpoint(endpointURL, ref)) 171 | } 172 | } 173 | // found a mirror for this registry, don't check any further entries 174 | // even if we didn't add any valid endpoints. 175 | break 176 | } 177 | } 178 | 179 | // always add the default endpoint 180 | defaultURL, err := normalizeEndpointAddress(registry) 181 | if err != nil { 182 | return nil, errors.Wrapf(err, "failed to construct default endpoint for registry %s", registry) 183 | } 184 | endpoints = append(endpoints, r.makeEndpoint(defaultURL, ref)) 185 | return endpoints, nil 186 | } 187 | 188 | // makeEndpoint is a utility function to create an endpoint struct for a given endpoint URL 189 | // and registry name. 190 | func (r *registry) makeEndpoint(endpointURL *url.URL, ref name.Reference) endpoint { 191 | return endpoint{ 192 | auth: r.getAuthenticator(endpointURL), 193 | keychain: r.DefaultKeychain, 194 | ref: ref, 195 | registry: r, 196 | url: endpointURL, 197 | } 198 | } 199 | 200 | // normalizeEndpointAddress normalizes the endpoint address. 201 | // If successful, it returns the registry URL. 202 | // If unsuccessful, an error is returned. 203 | // Scheme and hostname logic should match containerd: 204 | // https://github.com/containerd/containerd/blob/v1.7.13/remotes/docker/config/hosts.go#L99-L131 205 | func normalizeEndpointAddress(endpoint string) (*url.URL, error) { 206 | // Ensure that the endpoint address has a scheme so that the URL is parsed properly 207 | if !strings.Contains(endpoint, "://") { 208 | endpoint = "//" + endpoint 209 | } 210 | endpointURL, err := url.Parse(endpoint) 211 | if err != nil { 212 | return nil, err 213 | } 214 | if endpointURL.Host == "" { 215 | return nil, fmt.Errorf("invalid URL without host: %s", endpoint) 216 | } 217 | if endpointURL.Scheme == "" { 218 | // localhost on odd ports defaults to http 219 | port := endpointURL.Port() 220 | if isLocalhost(endpointURL.Host) && port != "" && port != "443" { 221 | endpointURL.Scheme = "http" 222 | } else { 223 | endpointURL.Scheme = "https" 224 | } 225 | } 226 | switch endpointURL.Path { 227 | case "", "/", "/v2": 228 | endpointURL.Path = "/v2" 229 | default: 230 | endpointURL.Path = path.Clean(endpointURL.Path) 231 | } 232 | return endpointURL, nil 233 | } 234 | 235 | // getAuthenticatorForHost returns an Authenticator for an endpoint URL. If no 236 | // configuration is present, Anonymous authentication is used. 237 | func (r *registry) getAuthenticator(endpointURL *url.URL) authn.Authenticator { 238 | registry := endpointURL.Host 239 | keys := []string{registry} 240 | if registry == name.DefaultRegistry { 241 | keys = append(keys, "docker.io") 242 | } 243 | keys = append(keys, "*") 244 | 245 | for _, key := range keys { 246 | if config, ok := r.Registry.Configs[key]; ok { 247 | if config.Auth != nil { 248 | return authn.FromConfig(authn.AuthConfig{ 249 | Username: config.Auth.Username, 250 | Password: config.Auth.Password, 251 | Auth: config.Auth.Auth, 252 | IdentityToken: config.Auth.IdentityToken, 253 | }) 254 | } 255 | // found a config for this registry, don't check any further entries 256 | // even if we didn't add any valid auth. 257 | break 258 | } 259 | } 260 | return authn.Anonymous 261 | } 262 | 263 | // getTLSConfig returns TLS configuration for an endpoint URL. This is cribbed from 264 | // https://github.com/containerd/cri/blob/release/1.4/pkg/server/image_pull.go#L274 265 | func (r *registry) getTLSConfig(endpointURL *url.URL) (*tls.Config, error) { 266 | tlsConfig := &tls.Config{} 267 | registry := endpointURL.Host 268 | keys := []string{registry} 269 | if registry == name.DefaultRegistry { 270 | keys = append(keys, "docker.io") 271 | } 272 | keys = append(keys, "*") 273 | 274 | for _, key := range keys { 275 | if config, ok := r.Registry.Configs[key]; ok { 276 | if config.TLS != nil { 277 | if config.TLS.CertFile != "" && config.TLS.KeyFile == "" { 278 | return nil, errors.Errorf("cert file %q was specified, but no corresponding key file was specified", config.TLS.CertFile) 279 | } 280 | if config.TLS.CertFile == "" && config.TLS.KeyFile != "" { 281 | return nil, errors.Errorf("key file %q was specified, but no corresponding cert file was specified", config.TLS.KeyFile) 282 | } 283 | if config.TLS.CertFile != "" && config.TLS.KeyFile != "" { 284 | cert, err := tls.LoadX509KeyPair(config.TLS.CertFile, config.TLS.KeyFile) 285 | if err != nil { 286 | return nil, errors.Wrap(err, "failed to load cert file") 287 | } 288 | if len(cert.Certificate) != 0 { 289 | tlsConfig.Certificates = []tls.Certificate{cert} 290 | } 291 | tlsConfig.BuildNameToCertificate() // nolint:staticcheck 292 | } 293 | 294 | if config.TLS.CAFile != "" { 295 | caCertPool, err := x509.SystemCertPool() 296 | if err != nil { 297 | return nil, errors.Wrap(err, "failed to get system cert pool") 298 | } 299 | caCert, err := ioutil.ReadFile(config.TLS.CAFile) 300 | if err != nil { 301 | return nil, errors.Wrap(err, "failed to load CA file") 302 | } 303 | caCertPool.AppendCertsFromPEM(caCert) 304 | tlsConfig.RootCAs = caCertPool 305 | } 306 | 307 | tlsConfig.InsecureSkipVerify = config.TLS.InsecureSkipVerify 308 | } 309 | // found a config for this registry, don't check any further entries 310 | // even if we didn't add any valid tls config. 311 | break 312 | } 313 | } 314 | 315 | return tlsConfig, nil 316 | } 317 | 318 | // getRewritesForHost gets the map of rewrite patterns for a given registry. 319 | func (r *registry) getRewrites(registry string) map[string]string { 320 | keys := []string{registry} 321 | if registry == name.DefaultRegistry { 322 | keys = append(keys, "docker.io") 323 | } 324 | keys = append(keys, "*") 325 | 326 | for _, key := range keys { 327 | if mirror, ok := r.Registry.Mirrors[key]; ok { 328 | if len(mirror.Rewrites) > 0 { 329 | return mirror.Rewrites 330 | } 331 | // found a mirror for this registry, don't check any further entries 332 | // even if we didn't add any rewrites. 333 | break 334 | } 335 | } 336 | 337 | return nil 338 | } 339 | 340 | func isLocalhost(host string) bool { 341 | if h, _, err := net.SplitHostPort(host); err == nil { 342 | host = h 343 | } 344 | 345 | if host == "localhost" { 346 | return true 347 | } 348 | 349 | ip := net.ParseIP(host) 350 | return ip.IsLoopback() 351 | } 352 | -------------------------------------------------------------------------------- /pkg/registries/registries_test.go: -------------------------------------------------------------------------------- 1 | package registries 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/http" 6 | "net/url" 7 | "testing" 8 | 9 | "github.com/google/go-containerregistry/pkg/authn" 10 | "github.com/google/go-containerregistry/pkg/name" 11 | "github.com/sirupsen/logrus" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestRewrite(t *testing.T) { 16 | logrus.SetLevel(logrus.DebugLevel) 17 | type mss map[string]string 18 | 19 | rewriteTests := map[string]struct { 20 | registry string 21 | rewrites mss 22 | imageNames mss 23 | }{ 24 | "syntax error in rewrite, log a warning and fail to apply": { 25 | registry: "docker.io", 26 | rewrites: mss{ 27 | "(.*": "docker/$1", 28 | }, 29 | imageNames: mss{ 30 | "busybox": "index.docker.io/library/busybox:latest", 31 | }, 32 | }, 33 | "no rewrites, unmodified": { 34 | registry: "docker.io", 35 | rewrites: mss{}, 36 | imageNames: mss{ 37 | "busybox": "index.docker.io/library/busybox:latest", 38 | "registry.local/test": "registry.local/test:latest", 39 | }, 40 | }, 41 | "rewrite docker.io images to prefix \"docker/\"": { 42 | registry: "docker.io", 43 | rewrites: mss{ 44 | "(.*)": "docker/$1", 45 | }, 46 | imageNames: mss{ 47 | "busybox": "index.docker.io/docker/library/busybox:latest", 48 | "registry.local/test": "registry.local/test:latest", 49 | }, 50 | }, 51 | "ensure that rewrites work with digests": { 52 | registry: "docker.io", 53 | rewrites: mss{ 54 | "(.*)": "docker/$1", 55 | }, 56 | imageNames: mss{ 57 | "busybox@sha256:82becede498899ec668628e7cb0ad87b6e1c371cb8a1e597d83a47fac21d6af3": "index.docker.io/docker/library/busybox@sha256:82becede498899ec668628e7cb0ad87b6e1c371cb8a1e597d83a47fac21d6af3", 58 | }, 59 | }, 60 | "rewrite registry.local images to prefix \"localimages/\"": { 61 | registry: "registry.local", 62 | rewrites: mss{ 63 | "(.*)": "localimages/$1", 64 | }, 65 | imageNames: mss{ 66 | "busybox": "index.docker.io/library/busybox:latest", 67 | "registry.local/test": "registry.local/localimages/test:latest", 68 | }, 69 | }, 70 | "rewrite docker.io rancher and longhornio images to unique prefixes; others remain unchanged": { 71 | registry: "docker.io", 72 | rewrites: mss{ 73 | "rancher/(.*)": "rancher/prod/$1", 74 | "longhornio/(.*)": "longhornio/staging/$1", 75 | }, 76 | imageNames: mss{ 77 | "rancher/rancher:v2.5.9": "index.docker.io/rancher/prod/rancher:v2.5.9", 78 | "longhornio/longhorn-engine:v1.1.1": "index.docker.io/longhornio/staging/longhorn-engine:v1.1.1", 79 | "busybox": "index.docker.io/library/busybox:latest", 80 | }, 81 | }, 82 | "rewrite docker.io images to prefix \"docker.io/\"": { 83 | registry: "docker.io", 84 | rewrites: mss{ 85 | "(.*)": "docker.io/$1", 86 | }, 87 | imageNames: mss{ 88 | "busybox": "index.docker.io/docker.io/library/busybox:latest", 89 | "registry.local/test": "registry.local/test:latest", 90 | }, 91 | }, 92 | "rewrite registry.k8s.io to prefix \"registry.k8s.io/\"": { 93 | registry: "registry.k8s.io", 94 | rewrites: mss{ 95 | "(.*)": "registry.k8s.io/$1", 96 | }, 97 | imageNames: mss{ 98 | "busybox": "index.docker.io/library/busybox:latest", 99 | "registry.k8s.io/pause:3.2": "registry.k8s.io/registry.k8s.io/pause:3.2", 100 | }, 101 | }, 102 | "rewrite without a trailing slash": { 103 | registry: "docker.io", 104 | rewrites: mss{ 105 | "(.*)": "mirrored-$1", 106 | }, 107 | imageNames: mss{ 108 | "busybox": "index.docker.io/mirrored-library/busybox:latest", 109 | }, 110 | }, 111 | "rewrite with the match as a prefix instead of suffix": { 112 | // I can't think of why anyone would want to do this though. 113 | registry: "docker.io", 114 | rewrites: mss{ 115 | "(.*)": "$1/docker", 116 | }, 117 | imageNames: mss{ 118 | "busybox": "index.docker.io/library/busybox/docker:latest", 119 | }, 120 | }, 121 | "replace all namespace separators with dashes": { 122 | // note that this doesn't work for docker.io, as it has an implicit 'library/' namespace 123 | // that gets inserted if you don't have a namespace. 124 | registry: "registry.local", 125 | rewrites: mss{ 126 | "/": "-", 127 | }, 128 | imageNames: mss{ 129 | "registry.local/team1/images/test": "registry.local/team1-images-test:latest", 130 | }, 131 | }, 132 | } 133 | 134 | for testName, test := range rewriteTests { 135 | t.Run(testName, func(t *testing.T) { 136 | registry := registry{ 137 | Registry: &Registry{ 138 | Mirrors: map[string]Mirror{ 139 | test.registry: { 140 | Endpoints: []string{"https://registry.example.com/v2/"}, 141 | Rewrites: test.rewrites, 142 | }, 143 | }, 144 | Configs: map[string]RegistryConfig{}, 145 | }, 146 | transports: map[string]*http.Transport{}, 147 | } 148 | 149 | for source, dest := range test.imageNames { 150 | originalRef, err := name.ParseReference(source) 151 | assert.NoError(t, err, "Failed to parse source reference %s", source) 152 | 153 | rewriteRef := registry.rewrite(originalRef) 154 | assert.Equal(t, dest, rewriteRef.Name(), "Bad rewrite for %s as %s", source, originalRef.Name()) 155 | t.Logf("OK rewrite for %s as %s - got %s", source, originalRef.Name(), rewriteRef.Name()) 156 | } 157 | }) 158 | } 159 | } 160 | 161 | func TestEndpoints(t *testing.T) { 162 | type msr map[string]RegistryConfig 163 | type msm map[string]Mirror 164 | 165 | endpointTests := map[string]struct { 166 | imageName string 167 | configs msr 168 | mirrors msm 169 | endpoints []endpoint 170 | tlsconfigs []*tls.Config 171 | }{ 172 | "no config, default endpoint": { 173 | imageName: "busybox", 174 | endpoints: []endpoint{ 175 | {url: mustParseURL("https://index.docker.io/v2")}, 176 | }, 177 | }, 178 | "local registry with only the default endpoint": { 179 | imageName: "registry.example.com/busybox", 180 | endpoints: []endpoint{ 181 | {url: mustParseURL("https://registry.example.com/v2")}, 182 | }, 183 | }, 184 | "local registry with TLS verification disabled": { 185 | imageName: "registry.example.com/busybox", 186 | configs: msr{"registry.example.com": RegistryConfig{TLS: &TLSConfig{InsecureSkipVerify: true}}}, 187 | endpoints: []endpoint{ 188 | {url: mustParseURL("https://registry.example.com/v2")}, 189 | }, 190 | tlsconfigs: []*tls.Config{ 191 | {InsecureSkipVerify: true}, 192 | }, 193 | }, 194 | "local registry with TLS verification disabled in wildcard": { 195 | imageName: "registry.example.com/busybox", 196 | configs: msr{"*": RegistryConfig{TLS: &TLSConfig{InsecureSkipVerify: true}}}, 197 | endpoints: []endpoint{ 198 | {url: mustParseURL("https://registry.example.com/v2")}, 199 | }, 200 | tlsconfigs: []*tls.Config{ 201 | {InsecureSkipVerify: true}, 202 | }, 203 | }, 204 | "local registry with TLS verification disabled in wildcard but not for this registry": { 205 | imageName: "registry.example.com/busybox", 206 | configs: msr{ 207 | "*": RegistryConfig{TLS: &TLSConfig{InsecureSkipVerify: true}}, 208 | "registry.example.com": RegistryConfig{}}, 209 | endpoints: []endpoint{ 210 | {url: mustParseURL("https://registry.example.com/v2")}, 211 | }, 212 | tlsconfigs: []*tls.Config{ 213 | {InsecureSkipVerify: false}, 214 | }, 215 | }, 216 | "local registry with custom endpoint": { 217 | imageName: "registry.example.com/busybox", 218 | mirrors: msm{"registry.example.com": Mirror{Endpoints: []string{"http://registry.example.com:5000/v2"}}}, 219 | endpoints: []endpoint{ 220 | {url: mustParseURL("http://registry.example.com:5000/v2")}, 221 | {url: mustParseURL("https://registry.example.com/v2")}, 222 | }, 223 | }, 224 | "local registry with custom endpoint with trailing slash": { 225 | imageName: "registry.example.com/busybox", 226 | mirrors: msm{"registry.example.com": Mirror{Endpoints: []string{"http://registry.example.com:5000/v2/"}}}, 227 | endpoints: []endpoint{ 228 | {url: mustParseURL("http://registry.example.com:5000/v2")}, 229 | {url: mustParseURL("https://registry.example.com/v2")}, 230 | }, 231 | }, 232 | "config, but not for the registry we're pulling from": { 233 | imageName: "busybox", 234 | mirrors: msm{"registry.example.com": Mirror{Endpoints: []string{"https://registry.example.com/v2"}}}, 235 | endpoints: []endpoint{ 236 | {url: mustParseURL("https://index.docker.io/v2")}, 237 | }, 238 | }, 239 | "config for docker.io, plus default endpoint": { 240 | imageName: "busybox", 241 | mirrors: msm{"docker.io": Mirror{Endpoints: []string{"https://docker.example.com/v2"}}}, 242 | endpoints: []endpoint{ 243 | {url: mustParseURL("https://docker.example.com/v2")}, 244 | {url: mustParseURL("https://index.docker.io/v2")}, 245 | }, 246 | }, 247 | "multiple endpoints for docker.io, plus default endpoint": { 248 | imageName: "busybox", 249 | mirrors: msm{"docker.io": Mirror{Endpoints: []string{"https://docker1.example.com/v2", "https://docker2.example.com/v2"}}}, 250 | endpoints: []endpoint{ 251 | {url: mustParseURL("https://docker1.example.com/v2")}, 252 | {url: mustParseURL("https://docker2.example.com/v2")}, 253 | {url: mustParseURL("https://index.docker.io/v2")}, 254 | }, 255 | }, 256 | "wildcard registry plus default": { 257 | imageName: "busybox", 258 | mirrors: msm{"*": Mirror{Endpoints: []string{"https://registry.example.com/v2"}}}, 259 | endpoints: []endpoint{ 260 | {url: mustParseURL("https://registry.example.com/v2")}, 261 | {url: mustParseURL("https://index.docker.io/v2")}, 262 | }, 263 | }, 264 | "wildcard endpoint plus docker.io; only docker.io should be used": { 265 | imageName: "busybox", 266 | mirrors: msm{ 267 | "*": Mirror{Endpoints: []string{"https://registry.example.com/v2"}}, 268 | "docker.io": Mirror{Endpoints: []string{"https://docker.example.com/v2"}}, 269 | }, 270 | endpoints: []endpoint{ 271 | {url: mustParseURL("https://docker.example.com/v2")}, 272 | {url: mustParseURL("https://index.docker.io/v2")}, 273 | }, 274 | }, 275 | "confirm that bad URLs are skipped": { 276 | imageName: "busybox", 277 | mirrors: msm{"docker.io": Mirror{Endpoints: []string{"https://docker1.example.com/v2", "https://user:bad{@docker2.example.com"}}}, 278 | endpoints: []endpoint{ 279 | {url: mustParseURL("https://docker1.example.com/v2")}, 280 | {url: mustParseURL("https://index.docker.io/v2")}, 281 | }, 282 | }, 283 | "confirm that relative URLs are skipped": { 284 | imageName: "busybox", 285 | mirrors: msm{"docker.io": Mirror{Endpoints: []string{"https://docker1.example.com/v2", "docker2.example.com/v2", "/v2"}}}, 286 | endpoints: []endpoint{ 287 | {url: mustParseURL("https://docker1.example.com/v2")}, 288 | {url: mustParseURL("https://docker2.example.com/v2")}, 289 | {url: mustParseURL("https://index.docker.io/v2")}, 290 | }, 291 | }, 292 | "confirm that endpoints missing scheme are not skipped": { 293 | imageName: "registry.example.com/busybox", 294 | mirrors: msm{"registry.example.com": Mirror{Endpoints: []string{"registry.example.com:5000/v2"}}}, 295 | endpoints: []endpoint{ 296 | {url: mustParseURL("https://registry.example.com:5000/v2")}, 297 | {url: mustParseURL("https://registry.example.com/v2")}, 298 | }, 299 | }, 300 | "confirm that localhost with odd ports uses http": { 301 | imageName: "localhost:5000/busybox", 302 | endpoints: []endpoint{ 303 | {url: mustParseURL("http://localhost:5000/v2")}, 304 | }, 305 | }, 306 | "confirm that localhost with https port uses https": { 307 | imageName: "localhost:443/busybox", 308 | endpoints: []endpoint{ 309 | {url: mustParseURL("https://localhost:443/v2")}, 310 | }, 311 | }, 312 | "confirm that loopback with odd ports uses http": { 313 | imageName: "127.0.0.1:5000/busybox", 314 | endpoints: []endpoint{ 315 | {url: mustParseURL("http://127.0.0.1:5000/v2")}, 316 | }, 317 | }, 318 | "confirm that loopback with https port uses https": { 319 | imageName: "127.0.0.1:443/busybox", 320 | endpoints: []endpoint{ 321 | {url: mustParseURL("https://127.0.0.1:443/v2")}, 322 | }, 323 | }, 324 | "confirm that creds are used for the default endpoint": { 325 | imageName: "busybox", 326 | configs: msr{"docker.io": RegistryConfig{Auth: &AuthConfig{Username: "user", Password: "pass"}}}, 327 | endpoints: []endpoint{ 328 | { 329 | url: mustParseURL("https://index.docker.io/v2"), 330 | auth: &authn.Basic{Username: "user", Password: "pass"}, 331 | }, 332 | }, 333 | }, 334 | "confirm that creds are used for custom endpoints": { 335 | imageName: "busybox", 336 | mirrors: msm{"docker.io": Mirror{Endpoints: []string{"https://docker1.example.com/v2"}}}, 337 | configs: msr{"docker1.example.com": RegistryConfig{Auth: &AuthConfig{Username: "user", Password: "pass"}}}, 338 | endpoints: []endpoint{ 339 | { 340 | url: mustParseURL("https://docker1.example.com/v2"), 341 | auth: &authn.Basic{Username: "user", Password: "pass"}, 342 | }, 343 | { 344 | url: mustParseURL("https://index.docker.io/v2"), 345 | }, 346 | }, 347 | }, 348 | "confirm that creds are used from wildcard config": { 349 | imageName: "busybox", 350 | mirrors: msm{"*": Mirror{Endpoints: []string{"https://registry.example.com/v2"}}}, 351 | configs: msr{"*": RegistryConfig{Auth: &AuthConfig{Username: "user", Password: "pass"}}}, 352 | endpoints: []endpoint{ 353 | { 354 | url: mustParseURL("https://registry.example.com/v2"), 355 | auth: &authn.Basic{Username: "user", Password: "pass"}, 356 | }, 357 | { 358 | url: mustParseURL("https://index.docker.io/v2"), 359 | auth: &authn.Basic{Username: "user", Password: "pass"}, 360 | }, 361 | }, 362 | }, 363 | "confirm that non-default schemes and ports are honored for mirrors and configs": { 364 | imageName: "busybox", 365 | mirrors: msm{"docker.io": Mirror{Endpoints: []string{"http://docker1.example.com:5000/v2"}}}, 366 | configs: msr{"docker1.example.com:5000": RegistryConfig{Auth: &AuthConfig{Username: "user", Password: "pass"}}}, 367 | endpoints: []endpoint{ 368 | { 369 | url: mustParseURL("http://docker1.example.com:5000/v2"), 370 | auth: &authn.Basic{Username: "user", Password: "pass"}, 371 | }, 372 | { 373 | url: mustParseURL("https://index.docker.io/v2"), 374 | }, 375 | }, 376 | }, 377 | } 378 | 379 | for testName, test := range endpointTests { 380 | t.Run(testName, func(t *testing.T) { 381 | registry := registry{ 382 | Registry: &Registry{ 383 | Mirrors: test.mirrors, 384 | Configs: test.configs, 385 | }, 386 | transports: map[string]*http.Transport{}, 387 | } 388 | 389 | ref, err := name.ParseReference(test.imageName) 390 | assert.NoError(t, err, "Failed to parse test reference for %v", test.imageName) 391 | 392 | endpoints, err := registry.getEndpoints(ref) 393 | assert.NoError(t, err, "Failed to get endpoints for %s", ref) 394 | 395 | // Compare endpoint URLs 396 | var expectedURLs, endpointURLs []string 397 | for _, e := range test.endpoints { 398 | expectedURLs = append(expectedURLs, e.url.String()) 399 | } 400 | for _, e := range endpoints { 401 | endpointURLs = append(endpointURLs, e.url.String()) 402 | } 403 | assert.Equal(t, expectedURLs, endpointURLs, "Unexpected endpoints for %s", ref) 404 | 405 | for i, endpoint := range endpoints { 406 | // Compare endpoint auths 407 | if i < len(test.endpoints) { 408 | expectedAuth, err := getAuthConfig(test.endpoints[i], ref) 409 | assert.NoError(t, err, "Failed to get auth for expected endpoint %d for %s", i, ref) 410 | 411 | epAuth, err := getAuthConfig(endpoint, ref) 412 | assert.NoError(t, err, "Failed to get auth for test endpoint %d for %s", i, ref) 413 | assert.Equal(t, expectedAuth, epAuth, "Unexpected auth for endpoint %d for %s", i, ref) 414 | } 415 | 416 | // Compare endpoint TLS 417 | if i < len(test.tlsconfigs) { 418 | tlsConfig, err := registry.getTLSConfig(endpoint.url) 419 | assert.NoError(t, err, "Failed to get tlsconfig for test endpoint %d for %s", i, ref) 420 | assert.Equal(t, test.tlsconfigs[i], tlsConfig, "Unexpected tlsconfig for endpoint %d for %s", i, ref) 421 | } 422 | } 423 | }) 424 | } 425 | } 426 | 427 | func getAuthConfig(resolver authn.Keychain, ref name.Reference) (*authn.AuthConfig, error) { 428 | auth, err := resolver.Resolve(ref.Context()) 429 | if err != nil { 430 | return nil, err 431 | } 432 | return auth.Authorization() 433 | } 434 | 435 | func mustParseURL(s string) *url.URL { 436 | u, err := url.Parse(s) 437 | if err != nil { 438 | logrus.Fatalf("FATAL: Failed to parse url %s: %v", s, err) 439 | } 440 | return u 441 | } 442 | -------------------------------------------------------------------------------- /pkg/registries/endpoint_test.go: -------------------------------------------------------------------------------- 1 | package registries 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "encoding/base64" 7 | "fmt" 8 | "net" 9 | "net/http" 10 | "net/http/httptest" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/google/go-containerregistry/pkg/authn" 16 | "github.com/google/go-containerregistry/pkg/name" 17 | v1 "github.com/google/go-containerregistry/pkg/v1" 18 | "github.com/google/go-containerregistry/pkg/v1/remote" 19 | "github.com/rancher/dynamiclistener/cert" 20 | "github.com/rancher/dynamiclistener/factory" 21 | "github.com/sirupsen/logrus" 22 | ) 23 | 24 | const localhost = "127-0-0-1.sslip.io" 25 | 26 | func TestImage(t *testing.T) { 27 | logrus.SetLevel(logrus.DebugLevel) 28 | 29 | imageTests := map[string]struct { 30 | images []string 31 | rewrites map[string]string 32 | }{ 33 | "Pull busybox without rewrite on default endpoint": { 34 | images: []string{ 35 | "library/busybox:latest", 36 | }, 37 | rewrites: map[string]string{ 38 | "^library/(.*)": "bogus-image-prefix/$1", 39 | }, 40 | }, 41 | } 42 | 43 | for testName, test := range imageTests { 44 | t.Run(testName, func(t *testing.T) { 45 | rs, as, mux := newServers(t, "127.0.0.1:443", true, true, true) 46 | defer rs.Close() 47 | defer as.Close() 48 | 49 | regHost, regEndpoint := getHostEndpoint(rs.Listener.Addr().String(), true, false) 50 | authHost, authEndpoint := getHostEndpoint(as.Listener.Addr().String(), true, false) 51 | 52 | t.Logf("INFO: %s registry %s at %s, auth %s at %s, scheme %q", t.Name(), regHost, regEndpoint, authHost, authEndpoint, "Basic") 53 | 54 | mux.Handle("/v2/", serveRegistry(t, "Basic", authEndpoint+"/auth")) 55 | mux.Handle("/auth/", serveAuth(t)) 56 | 57 | r := ®istry{ 58 | DefaultKeychain: authn.DefaultKeychain, 59 | Registry: &Registry{ 60 | Mirrors: map[string]Mirror{ 61 | regHost: Mirror{ 62 | Endpoints: []string{regHost + ":443"}, 63 | Rewrites: test.rewrites, 64 | }, 65 | }, 66 | Configs: map[string]RegistryConfig{ 67 | regHost: RegistryConfig{ 68 | Auth: &AuthConfig{Username: "user", Password: "pass"}, 69 | TLS: &TLSConfig{InsecureSkipVerify: true}, 70 | }, 71 | regHost + ":443": RegistryConfig{ 72 | Auth: &AuthConfig{Username: "user", Password: "pass"}, 73 | TLS: &TLSConfig{InsecureSkipVerify: true}, 74 | }, 75 | }, 76 | }, 77 | transports: map[string]*http.Transport{}, 78 | } 79 | 80 | for _, refStr := range test.images { 81 | t.Run(refStr, func(t *testing.T) { 82 | ref, err := name.ParseReference(regHost + "/" + refStr) 83 | if err != nil { 84 | t.Fatalf("FATAL: Failed to parse reference: %v", err) 85 | } 86 | 87 | // Target the only supported dummy platform, regardless of what we're running on 88 | image, err := r.Image(ref, remote.WithPlatform(v1.Platform{Architecture: "amd64", OS: "linux"})) 89 | if err != nil { 90 | t.Fatalf("FATAL: Failed to get image: %v", err) 91 | } 92 | 93 | // confirm that we can get the manifest 94 | _, err = image.Manifest() 95 | if err != nil { 96 | t.Fatalf("FATAL: Failed to get manifest: %v", err) 97 | } 98 | 99 | // confirm that we can get the config file 100 | _, err = image.ConfigFile() 101 | if err != nil { 102 | t.Fatalf("FATAL: Failed to get config file: %v", err) 103 | } 104 | 105 | t.Logf("OK: %s", ref) 106 | }) 107 | } 108 | }) 109 | } 110 | } 111 | 112 | func TestEndpoint(t *testing.T) { 113 | logrus.SetLevel(logrus.DebugLevel) 114 | 115 | endpointTests := map[string]struct { 116 | address string // the address to bind to 117 | explicitPort bool // whether or not to include the port in the URL, even if it is default for the scheme 118 | registryTLS bool // enable TLS for registry endpoint 119 | authTLS bool // enable TLS for auth endpoint 120 | sameAddress bool // use the same endpoint for both registry and auth 121 | authScheme string // scheme to use for authentication (none/basic/bearer) 122 | }{ 123 | "http anonymous": {"127.0.0.1:80", false, false, false, true, ""}, 124 | "http basic+local": {"127.0.0.1:80", false, false, false, true, "Basic"}, 125 | "http basic+http": {"127.0.0.1:80", false, false, false, false, "Basic"}, 126 | "http basic+https": {"127.0.0.1:80", false, false, true, false, "Basic"}, 127 | "http bearer+local": {"127.0.0.1:80", false, false, false, true, "Bearer"}, 128 | "http bearer+http": {"127.0.0.1:80", false, false, false, false, "Bearer"}, 129 | "http bearer+https": {"127.0.0.1:80", false, false, true, false, "Bearer"}, 130 | "http:80 anonymous": {"127.0.0.1:80", true, false, false, true, ""}, 131 | "http:80 basic+local": {"127.0.0.1:80", true, false, false, true, "Basic"}, 132 | "http:80 basic+http": {"127.0.0.1:80", true, false, false, false, "Basic"}, 133 | "http:80 basic+https": {"127.0.0.1:80", true, false, true, false, "Basic"}, 134 | "http:80 bearer+local": {"127.0.0.1:80", true, false, false, true, "Bearer"}, 135 | "http:80 bearer+http": {"127.0.0.1:80", true, false, false, false, "Bearer"}, 136 | "http:80 bearer+https": {"127.0.0.1:80", true, false, true, false, "Bearer"}, 137 | "http:rand anonymous": {"127.0.0.1:0", true, false, false, true, ""}, 138 | "http:rand basic+local": {"127.0.0.1:0", true, false, false, true, "Basic"}, 139 | "http:rand basic+http": {"127.0.0.1:0", true, false, false, false, "Basic"}, 140 | "http:rand basic+https": {"127.0.0.1:0", true, false, true, false, "Basic"}, 141 | "http:rand bearer+local": {"127.0.0.1:0", true, false, false, true, "Bearer"}, 142 | "http:rand bearer+http": {"127.0.0.1:0", true, false, false, false, "Bearer"}, 143 | "http:rand bearer+https": {"127.0.0.1:0", true, false, true, false, "Bearer"}, 144 | "https anonymous": {"127.0.0.1:443", false, true, true, true, ""}, 145 | "https basic+local": {"127.0.0.1:443", false, true, true, true, "Basic"}, 146 | "https basic+http": {"127.0.0.1:443", false, true, false, false, "Basic"}, 147 | "https basic+https": {"127.0.0.1:443", false, true, true, false, "Basic"}, 148 | "https bearer+local": {"127.0.0.1:443", false, true, true, true, "Bearer"}, 149 | "https bearer+http": {"127.0.0.1:443", false, true, false, false, "Bearer"}, 150 | "https bearer+https": {"127.0.0.1:443", false, true, true, false, "Bearer"}, 151 | "https:443 anonymous": {"127.0.0.1:443", true, true, true, true, ""}, 152 | "https:443 basic+local": {"127.0.0.1:443", true, true, true, true, "Basic"}, 153 | "https:443 basic+http": {"127.0.0.1:443", true, true, false, false, "Basic"}, 154 | "https:443 basic+https": {"127.0.0.1:443", true, true, true, false, "Basic"}, 155 | "https:443 bearer+local": {"127.0.0.1:443", true, true, true, true, "Bearer"}, 156 | "https:443 bearer+http": {"127.0.0.1:443", true, true, false, false, "Bearer"}, 157 | "https:443 bearer+https": {"127.0.0.1:443", true, true, true, false, "Bearer"}, 158 | "https:rand anonymous": {"127.0.0.1:0", true, true, true, true, ""}, 159 | "https:rand basic+local": {"127.0.0.1:0", true, true, true, true, "Basic"}, 160 | "https:rand basic+http": {"127.0.0.1:0", true, true, false, false, "Basic"}, 161 | "https:rand basic+https": {"127.0.0.1:0", true, true, true, false, "Basic"}, 162 | "https:rand bearer+local": {"127.0.0.1:0", true, true, true, true, "Bearer"}, 163 | "https:rand bearer+http": {"127.0.0.1:0", true, true, false, false, "Bearer"}, 164 | "https:rand bearer+https": {"127.0.0.1:0", true, true, true, false, "Bearer"}, 165 | } 166 | 167 | for testName, test := range endpointTests { 168 | t.Run(testName, func(t *testing.T) { 169 | if test.sameAddress && test.registryTLS != test.authTLS { 170 | t.Fatal("FATAL: Invalid test case: sameAddress is true, but registryTLS != authTLS") 171 | } 172 | 173 | rs, as, mux := newServers(t, test.address, test.registryTLS, test.authTLS, test.sameAddress) 174 | defer rs.Close() 175 | defer as.Close() 176 | 177 | regHost, regEndpoint := getHostEndpoint(rs.Listener.Addr().String(), test.registryTLS, test.explicitPort) 178 | authHost, authEndpoint := getHostEndpoint(as.Listener.Addr().String(), test.authTLS, test.explicitPort) 179 | 180 | t.Logf("INFO: %s registry %s at %s, auth %s at %s, scheme %q", t.Name(), regHost, regEndpoint, authHost, authEndpoint, test.authScheme) 181 | 182 | mux.Handle("/v2/", serveRegistry(t, test.authScheme, authEndpoint+"/auth")) 183 | mux.Handle("/auth/", serveAuth(t)) 184 | 185 | r := ®istry{ 186 | DefaultKeychain: authn.DefaultKeychain, 187 | Registry: &Registry{ 188 | Mirrors: map[string]Mirror{ 189 | defaultRegistry: Mirror{ 190 | Endpoints: []string{regEndpoint}, 191 | }, 192 | regHost: Mirror{ 193 | Endpoints: []string{regEndpoint}, 194 | }, 195 | }, 196 | Configs: map[string]RegistryConfig{ 197 | regHost: RegistryConfig{ 198 | Auth: &AuthConfig{Username: "user", Password: "pass"}, 199 | TLS: &TLSConfig{InsecureSkipVerify: true}, 200 | }, 201 | }, 202 | }, 203 | transports: map[string]*http.Transport{}, 204 | } 205 | 206 | // disable TLS verification for the auth endpoint too, if it's separate 207 | if !test.sameAddress { 208 | r.Registry.Configs[authHost] = RegistryConfig{TLS: &TLSConfig{InsecureSkipVerify: true}} 209 | } 210 | 211 | imageRefs := []string{ 212 | defaultRegistry + "/library/busybox:latest", 213 | regHost + "/library/busybox:latest", 214 | } 215 | 216 | // When using the default port for a scheme, confirm that the image can be pulled from the bare hostname, 217 | // even if the port was explicitly included in the registry config. 218 | if _, port, _ := net.SplitHostPort(test.address); port != "0" && test.explicitPort { 219 | imageRefs = append(imageRefs, localhost+"/library/busybox:latest") 220 | r.Registry.Configs[localhost] = RegistryConfig{TLS: &TLSConfig{InsecureSkipVerify: true}} 221 | } 222 | 223 | for _, refStr := range imageRefs { 224 | t.Run(refStr, func(t *testing.T) { 225 | ref, err := name.ParseReference(refStr) 226 | if err != nil { 227 | t.Fatalf("FATAL: Failed to parse reference: %v", err) 228 | } 229 | 230 | // Target the only supported dummy platform, regardless of what we're running on 231 | image, err := r.Image(ref, remote.WithPlatform(v1.Platform{Architecture: "amd64", OS: "linux"})) 232 | if err != nil { 233 | t.Fatalf("FATAL: Failed to get image: %v", err) 234 | } 235 | 236 | // confirm that we can get the manifest 237 | _, err = image.Manifest() 238 | if err != nil { 239 | t.Fatalf("FATAL: Failed to get manifest: %v", err) 240 | } 241 | 242 | // confirm that we can get the config file 243 | _, err = image.ConfigFile() 244 | if err != nil { 245 | t.Fatalf("FATAL: Failed to get config file: %v", err) 246 | } 247 | 248 | t.Logf("OK: %s", ref) 249 | }) 250 | } 251 | }) 252 | } 253 | } 254 | 255 | // newServers creates and starts httptest.Server instances for the registry and auth endpoints, 256 | // returning both the servers, and a http.ServeMux used by both servers. If sameAddress is true, 257 | // the authentication server is local to the registry - the same server instance is returned for 258 | // both registry and auth, and the authTLS settings are ignored. If sameAddress is false, a second 259 | // server is started on a random port. 260 | func newServers(t *testing.T, registryAddress string, registryTLS bool, authTLS bool, sameAddress bool) (*httptest.Server, *httptest.Server, *http.ServeMux) { 261 | l, err := net.Listen("tcp", registryAddress) 262 | if err != nil { 263 | t.Fatalf("FATAL: Failed to listen on %s: %v", registryAddress, err) 264 | } 265 | 266 | // Create a unique dummy CA and cert for this test's servers, if necessary 267 | var tlsConfig *tls.Config 268 | if registryTLS || authTLS { 269 | caCert, caKey, err := factory.GenCA() 270 | if err != nil { 271 | t.Fatalf("FATAL: Failed to generate CA: %v", err) 272 | } 273 | 274 | cfg := cert.Config{ 275 | CommonName: localhost, 276 | Organization: []string{t.Name()}, 277 | Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 278 | AltNames: cert.AltNames{ 279 | DNSNames: []string{localhost}, 280 | IPs: []net.IP{net.IPv4(127, 0, 0, 1)}, 281 | }, 282 | } 283 | serverCert, err := cert.NewSignedCert(cfg, caKey, caCert, caKey) 284 | if err != nil { 285 | t.Fatalf("FATAL: Failed to generate certificate: %v", err) 286 | } 287 | 288 | tlsConfig = &tls.Config{ 289 | Certificates: []tls.Certificate{ 290 | {Certificate: [][]byte{serverCert.Raw}, Leaf: serverCert, PrivateKey: caKey}, 291 | {Certificate: [][]byte{caCert.Raw}, Leaf: caCert}, 292 | }, 293 | } 294 | } 295 | 296 | mux := http.NewServeMux() 297 | rs := &httptest.Server{ 298 | Listener: l, 299 | Config: &http.Server{Handler: mux}, 300 | EnableHTTP2: true, 301 | TLS: tlsConfig, 302 | } 303 | if registryTLS { 304 | rs.StartTLS() 305 | } else { 306 | rs.Start() 307 | } 308 | if sameAddress { 309 | return rs, rs, mux 310 | } 311 | 312 | as := httptest.NewUnstartedServer(mux) 313 | as.EnableHTTP2 = true 314 | as.TLS = tlsConfig 315 | if authTLS { 316 | as.StartTLS() 317 | } else { 318 | as.Start() 319 | } 320 | return rs, as, mux 321 | } 322 | 323 | // getHostEndpoint returns both the bare request host value, and the endpoint URL, for the given address. 324 | // If tls is true, the scheme is https, otherwise http. 325 | // If explicitPort is true, the port will be included in the host value, even if it would not 326 | // normally be included due to being the default port for the scheme. The port is always included 327 | // if it is not the default port for the scheme. 328 | func getHostEndpoint(addr string, tls, explicitPort bool) (string, string) { 329 | _, port, _ := net.SplitHostPort(addr) 330 | host := localhost 331 | scheme := "http" 332 | 333 | if tls { 334 | scheme = "https" 335 | if port != "443" || explicitPort { 336 | host = host + ":" + port 337 | } 338 | } else { 339 | if port != "80" || explicitPort { 340 | host = host + ":" + port 341 | } 342 | } 343 | 344 | return host, scheme + "://" + host 345 | } 346 | 347 | // serveRegistry serves requests to the registry endpoint 348 | // If authScheme is set and the request does not have an authorization header, the request will 349 | // be responded to with a requst for authentication. 350 | // Otherwise, a few canned registry API responses will be served; just enough to satisfy the tests. 351 | func serveRegistry(t *testing.T, authScheme, realm string) http.Handler { 352 | return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 353 | resp.Header().Add("Docker-Distribution-Api-Version", "registry/2") 354 | 355 | if authScheme != "" && req.Header.Get("Authorization") == "" { 356 | var scope string 357 | if req.URL.Path == "/v2/" { 358 | scope = "registry:catalog" 359 | } else if strings.HasPrefix(req.URL.Path, "/v2/library/busybox") { 360 | scope = "repository:library/busybox" 361 | switch req.Method { 362 | case http.MethodGet, http.MethodHead: 363 | scope += ":pull" 364 | case http.MethodPost, http.MethodPut, http.MethodPatch: 365 | scope += ":push,pull" 366 | case http.MethodDelete: 367 | scope += ":delete" 368 | } 369 | } else { 370 | resp.WriteHeader(http.StatusForbidden) 371 | return 372 | } 373 | resp.Header().Add("WWW-Authenticate", fmt.Sprintf(`%s realm="%s",service="registry",scope="%s"`, authScheme, realm, scope)) 374 | resp.Header().Add("Content-Type", "application/json") 375 | resp.WriteHeader(http.StatusUnauthorized) 376 | resp.Write([]byte(`{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":null}]}`)) 377 | return 378 | } 379 | 380 | switch req.URL.Path { 381 | case "/v2/": 382 | resp.Header().Add("Content-Type", "application/json") 383 | resp.Write([]byte(`{}`)) 384 | case "/v2/library/busybox/manifests/latest": 385 | resp.Header().Add("Content-Type", "application/vnd.docker.distribution.manifest.list.v2+json") 386 | resp.Write([]byte(manifestList)) 387 | case "/v2/library/busybox/manifests/sha256:5cd3db04b8be5773388576a83177aff4f40a03457a63855f4b9cbe30542b9a43": 388 | resp.Header().Add("Content-Type", "application/vnd.docker.distribution.manifest.v2+json") 389 | resp.Write([]byte(manifest)) 390 | case "/v2/library/busybox/blobs/sha256:8135583d97feb82398909c9c97607159e6db2c4ca2c885c0b8f590ee0f9fe90d": 391 | resp.Header().Add("Content-Type", "application/octet-stream") 392 | resp.Write([]byte(config)) 393 | default: 394 | resp.WriteHeader(http.StatusNotFound) 395 | } 396 | }) 397 | } 398 | 399 | // serveAuth serves requests to the authorization service endpoint. 400 | // It does not actually validate any credentials; any request with an Authorization header will be granted a dummy token. 401 | func serveAuth(t *testing.T) http.Handler { 402 | return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 403 | if req.Method != http.MethodGet { 404 | resp.WriteHeader(http.StatusMethodNotAllowed) 405 | } else if auth := req.Header.Get("Authorization"); auth != "" { 406 | if b64, ok := strings.CutPrefix(auth, "Basic "); ok { 407 | if b, err := base64.StdEncoding.DecodeString(b64); err == nil { 408 | auth = string(b) 409 | } 410 | } 411 | params := req.URL.Query() 412 | t.Logf("Got auth request: %+v for %+v", params, auth) 413 | if service := params["service"]; len(service) != 1 || service[0] != "registry" { 414 | resp.WriteHeader(http.StatusNotFound) 415 | } else if scope := params["scope"]; len(scope) != 1 || !(scope[0] == "registry:catalog" || strings.HasPrefix(scope[0], "repository:library/busybox:")) { 416 | resp.WriteHeader(http.StatusNotFound) 417 | } else { 418 | resp.Header().Add("Content-Type", "application/json") 419 | resp.Write([]byte(fmt.Sprintf(`{"token": "abc", "access_token": "123", "expires_in": 300, "issued_at": "%s"}`, time.Now().Format(time.RFC3339)))) 420 | } 421 | } else { 422 | resp.WriteHeader(http.StatusForbidden) 423 | } 424 | }) 425 | } 426 | 427 | // a canned single-arch manifest list for the busybox image's latest tag 428 | var manifestList = `{ 429 | "manifests": [ 430 | { 431 | "digest": "sha256:5cd3db04b8be5773388576a83177aff4f40a03457a63855f4b9cbe30542b9a43", 432 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 433 | "platform": { 434 | "architecture": "amd64", 435 | "os": "linux" 436 | }, 437 | "size": 528 438 | } 439 | ], 440 | "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", 441 | "schemaVersion": 2 442 | }` 443 | 444 | // a canned manifest for the busybox image's latest tag 445 | var manifest = `{ 446 | "schemaVersion": 2, 447 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 448 | "config": { 449 | "mediaType": "application/vnd.docker.container.image.v1+json", 450 | "size": 1457, 451 | "digest": "sha256:8135583d97feb82398909c9c97607159e6db2c4ca2c885c0b8f590ee0f9fe90d" 452 | }, 453 | "layers": [ 454 | { 455 | "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", 456 | "size": 2591575, 457 | "digest": "sha256:325d69979d33f72bfd1d30d420b8ec7f130919916fd02238ba23e4a22d753ed8" 458 | } 459 | ] 460 | }` 461 | 462 | // a canned config blob for the busybox image's latest tag 463 | var config = `{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["sh"],"Image":"sha256:505de91dcca928e5436702f887bbd8b81be91e719b552fb5c64e34234d22ac86","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"ffeefc40361ae173c8c4a1c2bad0f899f4de97601938eab16b5d019bdf2fa5f3","container_config":{"Hostname":"ffeefc40361a","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"sh\"]"],"Image":"sha256:505de91dcca928e5436702f887bbd8b81be91e719b552fb5c64e34234d22ac86","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2023-05-19T20:19:22.751398522Z","docker_version":"20.10.23","history":[{"created":"2023-05-19T20:19:22.642507645Z","created_by":"/bin/sh -c #(nop) ADD file:cfd4bc7e9470d1298c9d4143538a77aa9aedd74f96aa5a3262cf8714c6fc3ec6 in / "},{"created":"2023-05-19T20:19:22.751398522Z","created_by":"/bin/sh -c #(nop) CMD [\"sh\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:9547b4c33213e630a0ca602a989ecc094e042146ae8afa502e1e65af6473db03"]}}` 464 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= 2 | cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= 3 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 4 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 5 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 6 | github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= 7 | github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= 8 | github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= 9 | github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= 10 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 11 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 12 | github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 13 | github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 14 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 15 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 16 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 17 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 18 | github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= 19 | github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= 20 | github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= 21 | github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= 22 | github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= 23 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 24 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 25 | github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= 26 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 27 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 30 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 32 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 33 | github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= 34 | github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 35 | github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= 36 | github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 37 | github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= 38 | github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= 39 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 40 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 41 | github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 42 | github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 43 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 44 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 45 | github.com/frankban/quicktest v1.12.1 h1:P6vQcHwZYgVGIpUzKB5DXzkEeYJppJOStPLuh9aB89c= 46 | github.com/frankban/quicktest v1.12.1/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= 47 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 48 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 49 | github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= 50 | github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 51 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 52 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 53 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 54 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 55 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 56 | github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 57 | github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 58 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 59 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 60 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 61 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 62 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 63 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 64 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 65 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 66 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 67 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 68 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 69 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 70 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 71 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 72 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 73 | github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= 74 | github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 75 | github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= 76 | github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= 77 | github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= 78 | github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 79 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 80 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 81 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 82 | github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo= 83 | github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= 84 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 85 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 86 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 87 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 88 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 89 | github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= 90 | github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= 91 | github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= 92 | github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= 93 | github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs= 94 | github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= 95 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= 96 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 97 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= 98 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= 99 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 100 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 101 | github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= 102 | github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= 103 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 104 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 105 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 106 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 107 | github.com/k3s-io/kubernetes v1.34.1-k3s1 h1:9/NMITLXw8yZC/lPaftDetx3PXxwPEgMVdI5kQrmTew= 108 | github.com/k3s-io/kubernetes v1.34.1-k3s1/go.mod h1:iu+FhII+Oc/1gGWLJcer6wpyih441aNFHl7Pvm8yPto= 109 | github.com/k3s-io/kubernetes/staging/src/k8s.io/api v1.34.1-k3s1 h1:YTJRyNTpBHRMZRf3+xMBlb+SPNA1cqIkWTxhiG1FGv4= 110 | github.com/k3s-io/kubernetes/staging/src/k8s.io/api v1.34.1-k3s1/go.mod h1:3Y5V97lz2MrKYzHlUaXejkj+coCmqde9E9WwVFuWRXE= 111 | github.com/k3s-io/kubernetes/staging/src/k8s.io/apiextensions-apiserver v1.34.1-k3s1 h1:3nSmnFuYQdi93sHP/G2JycWut51JjNnpF8iZ5rzpk5o= 112 | github.com/k3s-io/kubernetes/staging/src/k8s.io/apiextensions-apiserver v1.34.1-k3s1/go.mod h1:NL2CyapDmJ+5XVVY8qr6niVA3UHVF17kPl0zh6ohkVM= 113 | github.com/k3s-io/kubernetes/staging/src/k8s.io/apimachinery v1.34.1-k3s1 h1:gbOHARcGXTIfDyNxGhIXkyWBsGXNTjaMyrF3XKJl3hg= 114 | github.com/k3s-io/kubernetes/staging/src/k8s.io/apimachinery v1.34.1-k3s1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= 115 | github.com/k3s-io/kubernetes/staging/src/k8s.io/apiserver v1.34.1-k3s1 h1:AwT2arH3eHp4jsU41URP6yUKGKW9yWD0hVbVtyKSlGQ= 116 | github.com/k3s-io/kubernetes/staging/src/k8s.io/apiserver v1.34.1-k3s1/go.mod h1:msyjTyI8TyfhYybEkao5LA8bUrVqz1xhic5zxsfejoM= 117 | github.com/k3s-io/kubernetes/staging/src/k8s.io/client-go v1.34.1-k3s1 h1:plj9Nk51Avz8v0kjAZQaSAH2+jzTW7SWGoO83P8MWkg= 118 | github.com/k3s-io/kubernetes/staging/src/k8s.io/client-go v1.34.1-k3s1/go.mod h1:EA6EHLX97x5H59hA02pKPLlZBMQEYnYMsIMglrufpFo= 119 | github.com/k3s-io/kubernetes/staging/src/k8s.io/cloud-provider v1.34.1-k3s1 h1:AknnbuC+KCNITI5OWuaQ2lYGMaDyQ4mKe2MQN1clnJ8= 120 | github.com/k3s-io/kubernetes/staging/src/k8s.io/cloud-provider v1.34.1-k3s1/go.mod h1:ZZzwSqYu465kx/03+L6Axo9WQxQxiJJuR7kx8i+km6o= 121 | github.com/k3s-io/kubernetes/staging/src/k8s.io/component-base v1.34.1-k3s1 h1:5q3N96zdUQldZIBCbxzBT7dpLUS9XaXx8Kcq/03QPnk= 122 | github.com/k3s-io/kubernetes/staging/src/k8s.io/component-base v1.34.1-k3s1/go.mod h1:ji6LERznCQ/l4DKb74Vd/5W8Q89b7SvBen7B4AMY0Oo= 123 | github.com/k3s-io/kubernetes/staging/src/k8s.io/component-helpers v1.34.1-k3s1 h1:jijAGPnOR+NRZqQJte/2jnrfOYpxY68cgZoz3uj9X68= 124 | github.com/k3s-io/kubernetes/staging/src/k8s.io/component-helpers v1.34.1-k3s1/go.mod h1:l7hECO13A52pGPFD+SlZJ+EGCuRoVOm0wtdDCMQQkf4= 125 | github.com/k3s-io/kubernetes/staging/src/k8s.io/controller-manager v1.34.1-k3s1 h1:1JaKjGXzKItB/v0LMWCJ8fYRpsh/+NMp3qpx2mVkF7s= 126 | github.com/k3s-io/kubernetes/staging/src/k8s.io/controller-manager v1.34.1-k3s1/go.mod h1:r87/fGxIfARYiYYbMjR8AIOqf25GMlSSqL/F05+OIzI= 127 | github.com/k3s-io/kubernetes/staging/src/k8s.io/cri-api v1.34.1-k3s1 h1:aP7KoqOHixfUugXhXKfh5DesL9y3f6K/eWep6veS/Hg= 128 | github.com/k3s-io/kubernetes/staging/src/k8s.io/cri-api v1.34.1-k3s1/go.mod h1:4qVUjidMg7/Z9YGZpqIDygbkPWkg3mkS1PvOx/kpHTE= 129 | github.com/k3s-io/kubernetes/staging/src/k8s.io/kms v1.34.1-k3s1 h1:YRS1i2brvu5XNenn6wpIHo/u4gtsINNtyMOx9hzUolo= 130 | github.com/k3s-io/kubernetes/staging/src/k8s.io/kms v1.34.1-k3s1/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM= 131 | github.com/k3s-io/kubernetes/staging/src/k8s.io/kubelet v1.34.1-k3s1 h1:f2SdXOxtxFpx5fGicteBmtYbTfKA8VUcXpEgySI9Jco= 132 | github.com/k3s-io/kubernetes/staging/src/k8s.io/kubelet v1.34.1-k3s1/go.mod h1:5xnzJEi0iAetJLsqhsO5yMAnW3yPZ+zs32oh4VAKgc0= 133 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 134 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 135 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 136 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 137 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 138 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 139 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 140 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 141 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 142 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 143 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 144 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 145 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 146 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 147 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 148 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 149 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 150 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 151 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 152 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 153 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 154 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= 155 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 156 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 157 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 158 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 159 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 160 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 161 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 162 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 163 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 164 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 165 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 166 | github.com/pierrec/lz4 v2.6.0+incompatible h1:Ix9yFKn1nSPBLFl/yZknTp8TU5G4Ps0JDmguYK6iH1A= 167 | github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 168 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 169 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 170 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 171 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 172 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 173 | github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 174 | github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 175 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 176 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 177 | github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= 178 | github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= 179 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 180 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 181 | github.com/rancher/dynamiclistener v0.7.2-rc2 h1:7eS/+mO1JWF9jahWVOGDP6xjMe+S10P9FXgjwZzRCJY= 182 | github.com/rancher/dynamiclistener v0.7.2-rc2/go.mod h1:w2sZQ9x2a/ZPIKcUb495xZYVgtZTKviViG5Rn0gXvq8= 183 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 184 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 185 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 186 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 187 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 188 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 189 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 190 | github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= 191 | github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= 192 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 193 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 194 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 195 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 196 | github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= 197 | github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= 198 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 199 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 200 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 201 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 202 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 203 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 204 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 205 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 206 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 207 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 208 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 209 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 210 | github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= 211 | github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= 212 | github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= 213 | github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 214 | github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 215 | github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= 216 | github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= 217 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 218 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 219 | github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= 220 | github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 221 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 222 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 223 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 224 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 225 | go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= 226 | go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= 227 | go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo= 228 | go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk= 229 | go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0= 230 | go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI= 231 | go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A= 232 | go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo= 233 | go.etcd.io/etcd/pkg/v3 v3.6.4 h1:fy8bmXIec1Q35/jRZ0KOes8vuFxbvdN0aAFqmEfJZWA= 234 | go.etcd.io/etcd/pkg/v3 v3.6.4/go.mod h1:kKcYWP8gHuBRcteyv6MXWSN0+bVMnfgqiHueIZnKMtE= 235 | go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU= 236 | go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg= 237 | go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= 238 | go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= 239 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 240 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 241 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= 242 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= 243 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= 244 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= 245 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 246 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 247 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= 248 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= 249 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= 250 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= 251 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 252 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 253 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 254 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 255 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 256 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 257 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 258 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 259 | go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= 260 | go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= 261 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 262 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 263 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 264 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 265 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 266 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 267 | go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 268 | go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 269 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 270 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 271 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 272 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 273 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 274 | golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= 275 | golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= 276 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 277 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 278 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 279 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 280 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 281 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 282 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 283 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 284 | golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= 285 | golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 286 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 287 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 288 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 289 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 290 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 291 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 292 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 293 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 294 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 296 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 297 | golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 298 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 299 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 300 | golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= 301 | golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= 302 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 303 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 304 | golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= 305 | golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 306 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 307 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 308 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 309 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 310 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 311 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 312 | golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= 313 | golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= 314 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 315 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 316 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 317 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 318 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= 319 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= 320 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= 321 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= 322 | google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= 323 | google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 324 | google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= 325 | google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 326 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 327 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 328 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 329 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 330 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 331 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 332 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 333 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 334 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 335 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 336 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 337 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 338 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 339 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 340 | gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= 341 | gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 342 | k8s.io/cri-client v0.34.0 h1:tLZro2oYinVKS5CaMtCASLmOacqVlwoHPSs9e7sBFWI= 343 | k8s.io/cri-client v0.34.0/go.mod h1:KkGaUJWMvCdpSTf15Wiqtf3WKl3qjcvkBcMApPCqpxQ= 344 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 345 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 346 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= 347 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= 348 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= 349 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 350 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM= 351 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= 352 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 353 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 354 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 355 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 356 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= 357 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= 358 | sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= 359 | sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= 360 | --------------------------------------------------------------------------------