├── .github └── workflows │ └── pull_request.yml ├── .gitignore ├── .golangci.yaml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── acceptance.bats ├── assets ├── logo.png └── sinker-pull-demo.gif ├── example ├── .images.yaml ├── bundle.yaml ├── source.txt └── target.txt ├── go.mod ├── go.sum ├── internal ├── commands │ ├── check.go │ ├── check_test.go │ ├── copy.go │ ├── create.go │ ├── default.go │ ├── list.go │ ├── pull.go │ ├── push.go │ ├── update.go │ └── version.go ├── docker │ ├── auth.go │ ├── docker.go │ ├── docker_test.go │ ├── registrypath.go │ └── registrypath_test.go └── manifest │ ├── kubernetes.go │ ├── kubernetes_test.go │ ├── manifest.go │ └── manifest_test.go ├── main.go └── test ├── README.md ├── create ├── bundle.yaml └── expected-images.yaml ├── list ├── .images.yaml ├── expected-source.txt └── expected-target.txt ├── pull └── .images.yaml ├── push └── .images.yaml └── update ├── README.md ├── bundle.yaml ├── expected.yaml └── original.yaml /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | on: [pull_request] 3 | 4 | jobs: 5 | go-tests: 6 | name: Go Tests 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: setup go 10 | uses: actions/setup-go@v2 11 | with: 12 | go-version: '1.19' 13 | 14 | - name: checkout source 15 | uses: actions/checkout@v2 16 | 17 | - name: build 18 | run: go build -tags 'containers_image_openpgp' -o sinker main.go 19 | 20 | - name: lint go 21 | run: | 22 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.46.2 23 | ./bin/golangci-lint run --build-tags='containers_image_openpgp' --timeout=5m --color=always --max-same-issues=0 --max-issues-per-linter=0 24 | 25 | - name: unit tests 26 | run: make test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | sinker* -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linter-settings: 2 | misspell: 3 | locale: US 4 | 5 | linters: 6 | disable-all: true 7 | enable: 8 | - deadcode 9 | - goconst 10 | - gofmt 11 | - goimports 12 | - gosec 13 | - gosimple 14 | - govet 15 | - ineffassign 16 | - makezero 17 | - misspell 18 | - predeclared 19 | - revive 20 | - structcheck 21 | - typecheck 22 | - unconvert 23 | - unparam 24 | - unused 25 | - varcheck 26 | - wastedassign 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Code of Conduct 2 | 3 | We follow the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 AS builder 2 | 3 | ENV CGO_ENABLED=0 4 | 5 | WORKDIR /build 6 | COPY . /build 7 | 8 | # SINKER_VERSION is set during the release process 9 | ARG SINKER_VERSION=0.0.0 10 | RUN go build -tags 'containers_image_openpgp' -ldflags="-X 'github.com/plexsystems/sinker/internal/commands.sinkerVersion=${SINKER_VERSION}'" 11 | 12 | FROM alpine:3.14.6 13 | 14 | RUN apk update && apk add --no-cache docker-cli 15 | 16 | COPY --from=builder /build/sinker /usr/bin/ 17 | 18 | LABEL org.opencontainers.image.source="https://github.com/plexsystems/sinker" 19 | LABEL org.opencontainers.image.title="sinker" 20 | LABEL org.opencontainers.image.authors="John Reese " 21 | LABEL org.opencontainers.image.description="Sync container images from one registry to another" 22 | 23 | ENTRYPOINT ["/usr/bin/sinker"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Plex Systems 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_TAGS=containers_image_openpgp 2 | 3 | .PHONY: build 4 | build: 5 | @go build -tags='$(GO_TAGS)' -ldflags="-X 'github.com/plexsystems/sinker/internal/commands.sinkerVersion=$$(git describe --tags --always --dirty)'" 6 | 7 | .PHONY: test 8 | test: 9 | @go test -tags='$(GO_TAGS)' -v ./... -count=1 10 | 11 | .PHONY: lint 12 | lint: 13 | @golangci-lint --build-tags='$(GO_TAGS)' run --fix 14 | 15 | .PHONY: acceptance 16 | acceptance: build 17 | @bats acceptance.bats 18 | 19 | .PHONY: all 20 | all: build test acceptance 21 | 22 | # When using the release target a version must be specified. 23 | # e.g. make release version=v0.1.0 24 | .PHONY: release 25 | release: 26 | @test $(version) 27 | @docker build --build-arg SINKER_VERSION=$(version) -t plexsystems/sinker:$(version) . 28 | @GOOS=darwin GOARCH=amd64 go build -tags='$(GO_TAGS)' -o sinker-darwin-amd64 -ldflags="-X 'github.com/plexsystems/sinker/internal/commands.sinkerVersion=$(version)'" 29 | @GOOS=windows GOARCH=amd64 go build -tags='$(GO_TAGS)' -o sinker-windows-amd64 -ldflags="-X 'github.com/plexsystems/sinker/internal/commands.sinkerVersion=$(version)'" 30 | @GOOS=linux GOARCH=amd64 go build -tags='$(GO_TAGS)' -o sinker-linux-amd64 -ldflags="-X 'github.com/plexsystems/sinker/internal/commands.sinkerVersion=$(version)'" 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sinker 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/plexsystems/sinker)](https://goreportcard.com/report/github.com/plexsystems/sinker) 4 | [![GitHub release](https://img.shields.io/github/release/plexsystems/sinker.svg)](https://github.com/plexsystems/sinker/releases) 5 | 6 | ![logo](assets/logo.png) 7 | 8 | `sinker` syncs container images from one registry to another. This is useful in cases when you rely on images that exist in a public container registry, but need to pull from a private registry. 9 | 10 | Images can be sync'd either by using [The image manifest](#the-image-manifest) or via the command line. 11 | 12 | See the [example](https://github.com/plexsystems/sinker/tree/main/example) folder for more details on the produced files. 13 | 14 | ## Installation 15 | 16 | `go install github.com/plexsystems/sinker@latest` 17 | 18 | Releases are also provided in the [releases](https://github.com/plexsystems/sinker/releases) tab on GitHub. 19 | 20 | ## The image manifest 21 | 22 | ### The target section 23 | 24 | ```yaml 25 | target: 26 | host: mycompany.com 27 | repository: myteam 28 | ``` 29 | 30 | The `target` section is where the images will be synced to. The above yaml would sync all images to the `myteam` repository hosted at `mycompany.com` (`mycompany.com/myteam/...`) 31 | 32 | ### The images section 33 | 34 | ```yaml 35 | target: 36 | host: mycompany.com 37 | repository: myteam 38 | sources: 39 | - repository: coreos/prometheus-operator 40 | host: quay.io 41 | tag: v0.40.0 42 | - repository: super/secret 43 | tag: v0.3.0 44 | auth: 45 | username: DOCKER_USER_ENV 46 | password: DOCKER_PASSWORD_ENV 47 | - repository: nginx 48 | digest: sha256:bbda10abb0b7dc57cfaab5d70ae55bd5aedfa3271686bace9818bba84cd22c29 49 | ``` 50 | 51 | ### Optional host defaults to Docker Hub 52 | 53 | In both the `target` and `sources` section, the `host` field is _optional_. When no host is set, the host is assumed to be Docker Hub. 54 | 55 | ### Auth 56 | 57 | All auth is handled by looking at the clients Docker auth. If the client can perform a `docker push` or `docker pull`, sinker will be able to as well. 58 | 59 | Optionally, the `auth` section allows you to set the names of _environment variables_ that will be used for creating basic auth to the registry. This could be useful in pipelines where auth is stored in environment variables. 60 | 61 | ## Sync behavior 62 | 63 | If the `target` registry supports nested paths, the entire source repository will be pushed to the target. For example, the `prometheus-operator` would be pushed to: 64 | 65 | ```text 66 | mycompany.com/myteam/coreos/prometheus-operator:v0.40.0 67 | ``` 68 | 69 | **Registries that support nested paths:** Azure Container Registry (ACR), Amazon Elastic Container Registry (ECR), Google Container Registry (GCR) 70 | 71 | If the `target` registry does _not_ support nested paths, only the base path of the source will be pushed to the target registry. For example, the `prometheus-operator` would be pushed to: 72 | 73 | ```text 74 | mycompany.com/myteam/prometheus-operator:v0.40.0 75 | ``` 76 | 77 | **Registries that do not support nested paths:** Docker Hub, GitHub Container Registry, Quay.io 78 | 79 | ## Demo 80 | 81 | An example run of the `sinker pull` command which pulls all images specified in the image manifest. 82 | 83 | ![demo](assets/sinker-pull-demo.gif) 84 | 85 | For additional help, you can run `sinker help`. 86 | -------------------------------------------------------------------------------- /acceptance.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | @test "[CHECK] Using manifest returns newer image" { 4 | run ./sinker check --manifest example 5 | [[ "$output" =~ "New versions for" ]] 6 | } 7 | 8 | @test "[CHECK] Using --images flag returns newer versions" { 9 | run ./sinker check --images plexsystems/sinker-test:0.0.1 10 | [[ "$output" =~ "New versions for" ]] 11 | } 12 | 13 | @test "[CREATE] New manifest with autodetection creates example manifest" { 14 | run ./sinker create example/bundle.yaml --target mycompany.com/myrepo --manifest example/output.yaml 15 | 16 | cmp -s "example/.images.yaml" "example/output.yaml" 17 | rm -f example/output.yaml 18 | } 19 | 20 | @test "[CREATE] New manifest with autodetection image host different from target" { 21 | run ./sinker create test/create/bundle.yaml --target myhost.com --output test/create/output.yaml 22 | 23 | cmp -s "test/create/expected-images.yaml" "test/create/output.yaml" 24 | rm -f test/create/output.yaml 25 | } 26 | 27 | @test "[UPDATE] Updating manifest matches expected manifest" { 28 | run ./sinker update test/update/bundle.yaml --manifest test/update/original.yaml --output test/update/expected.yaml 29 | git diff --quiet -- test/update/expected.yaml 30 | } 31 | 32 | @test "[LIST] List of source images matches example source list" { 33 | run ./sinker list source --manifest example --output example/source.txt 34 | git diff --quiet -- example/source.txt 35 | } 36 | 37 | @test "[LIST] List of target images matches example target list" { 38 | run ./sinker list target --manifest example --output example/target.txt 39 | git diff --quiet -- example/target.txt 40 | } 41 | 42 | @test "[LIST] List of source images matches expected output when target repository does not support nested repos" { 43 | run ./sinker list source --manifest test/list --output test/list/expected-source.txt 44 | git diff --quiet -- test/list/expected-source.txt 45 | } 46 | 47 | @test "[LIST] List of target images matches expected output when target repository does not support nested repos" { 48 | run ./sinker list target --manifest test/list --output test/list/expected-target.txt 49 | git diff --quiet -- test/list/expected-target.txt 50 | } 51 | 52 | @test "[PUSH] Using --dryrun flag lists missing images" { 53 | run ./sinker push --dryrun --manifest test/push 54 | [[ "$output" =~ "Image busybox:latest would be pushed as plexsystems/busybox:latest" ]] 55 | } 56 | 57 | @test "[PUSH] Using manifest all latest images successfully pushed" { 58 | run ./sinker push --manifest test/push 59 | [[ "$output" =~ "All images have been pushed!" ]] 60 | } 61 | 62 | @test "[PUSH] Using --images flag all latest images successfully pushed" { 63 | run ./sinker push --images busybox:latest --target plexsystems 64 | [[ "$output" =~ "All images have been pushed!" ]] 65 | } 66 | 67 | @test "[PULL] Using manifest pulls all images" { 68 | docker rmi plexsystems/sinker-test:latest -f 69 | docker rmi plexsystems/sinker-test:1.0.0 -f 70 | 71 | run ./sinker pull target --manifest test/pull 72 | [[ "$output" =~ "All images have been pulled!" ]] 73 | 74 | docker inspect plexsystems/sinker-test:latest 75 | docker inspect plexsystems/sinker-test:1.0.0 76 | } 77 | 78 | @test "[PULL] Using --images flag pulls all images" { 79 | docker rmi plexsystems/sinker-test:latest -f 80 | docker rmi plexsystems/sinker-test:1.0.0 -f 81 | 82 | run ./sinker pull --images plexsystems/sinker-test:latest,plexsystems/sinker-test:1.0.0 83 | [[ "$output" =~ "All images have been pulled!" ]] 84 | 85 | docker inspect plexsystems/sinker-test:latest 86 | docker inspect plexsystems/sinker-test:1.0.0 87 | } 88 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plexsystems/sinker/e4323d1bce8964cdb13cbecd95ff701caea7d218/assets/logo.png -------------------------------------------------------------------------------- /assets/sinker-pull-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plexsystems/sinker/e4323d1bce8964cdb13cbecd95ff701caea7d218/assets/sinker-pull-demo.gif -------------------------------------------------------------------------------- /example/.images.yaml: -------------------------------------------------------------------------------- 1 | target: 2 | host: mycompany.com 3 | repository: myrepo 4 | sources: 5 | - repository: coreos/prometheus-operator 6 | host: quay.io 7 | tag: v0.40.0 8 | - repository: jimmidyson/configmap-reload 9 | tag: v0.3.0 10 | - repository: coreos/prometheus-config-reloader 11 | host: quay.io 12 | tag: v0.39.0 13 | -------------------------------------------------------------------------------- /example/bundle.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: prometheus-operator 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app.kubernetes.io/component: controller 10 | app.kubernetes.io/name: prometheus-operator 11 | template: 12 | metadata: 13 | labels: 14 | app.kubernetes.io/component: controller 15 | app.kubernetes.io/name: prometheus-operator 16 | spec: 17 | containers: 18 | - args: 19 | - --config-reloader-image=jimmidyson/configmap-reload:v0.3.0 20 | - --prometheus-config-reloader=quay.io/coreos/prometheus-config-reloader:v0.39.0 21 | image: quay.io/coreos/prometheus-operator:v0.40.0 22 | name: prometheus-operator 23 | ports: 24 | - containerPort: 8080 25 | name: http 26 | serviceAccountName: prometheus-operator 27 | -------------------------------------------------------------------------------- /example/source.txt: -------------------------------------------------------------------------------- 1 | quay.io/coreos/prometheus-operator:v0.40.0 2 | jimmidyson/configmap-reload:v0.3.0 3 | quay.io/coreos/prometheus-config-reloader:v0.39.0 4 | -------------------------------------------------------------------------------- /example/target.txt: -------------------------------------------------------------------------------- 1 | mycompany.com/myrepo/coreos/prometheus-operator:v0.40.0 2 | mycompany.com/myrepo/jimmidyson/configmap-reload:v0.3.0 3 | mycompany.com/myrepo/coreos/prometheus-config-reloader:v0.39.0 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/plexsystems/sinker 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/avast/retry-go v3.0.0+incompatible 7 | github.com/containers/image/v5 v5.24.2 8 | github.com/docker/docker v23.0.2+incompatible 9 | github.com/ghodss/yaml v1.0.0 10 | github.com/google/go-containerregistry v0.14.0 11 | github.com/hashicorp/go-version v1.6.0 12 | github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.64.0 13 | github.com/sirupsen/logrus v1.9.0 14 | github.com/spf13/cobra v1.6.1 15 | github.com/spf13/viper v1.15.0 16 | gopkg.in/yaml.v2 v2.4.0 17 | k8s.io/api v0.26.3 18 | k8s.io/apimachinery v0.26.3 19 | ) 20 | 21 | require ( 22 | github.com/BurntSushi/toml v1.2.1 // indirect 23 | github.com/Microsoft/go-winio v0.6.0 // indirect 24 | github.com/VividCortex/ewma v1.2.0 // indirect 25 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 26 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect 27 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect 28 | github.com/containers/ocicrypt v1.1.7 // indirect 29 | github.com/containers/storage v1.45.3 // indirect 30 | github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 // indirect 31 | github.com/docker/cli v23.0.1+incompatible // indirect 32 | github.com/docker/distribution v2.8.1+incompatible // indirect 33 | github.com/docker/docker-credential-helpers v0.7.0 // indirect 34 | github.com/docker/go-connections v0.4.0 // indirect 35 | github.com/docker/go-units v0.5.0 // indirect 36 | github.com/fsnotify/fsnotify v1.6.0 // indirect 37 | github.com/go-logr/logr v1.2.3 // indirect 38 | github.com/go-openapi/analysis v0.21.4 // indirect 39 | github.com/go-openapi/errors v0.20.3 // indirect 40 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 41 | github.com/go-openapi/jsonreference v0.20.0 // indirect 42 | github.com/go-openapi/loads v0.21.2 // indirect 43 | github.com/go-openapi/runtime v0.24.1 // indirect 44 | github.com/go-openapi/spec v0.20.7 // indirect 45 | github.com/go-openapi/strfmt v0.21.3 // indirect 46 | github.com/go-openapi/swag v0.22.3 // indirect 47 | github.com/go-openapi/validate v0.22.0 // indirect 48 | github.com/gogo/protobuf v1.3.2 // indirect 49 | github.com/golang/protobuf v1.5.3 // indirect 50 | github.com/google/gofuzz v1.2.0 // indirect 51 | github.com/gorilla/mux v1.8.0 // indirect 52 | github.com/hashicorp/errwrap v1.1.0 // indirect 53 | github.com/hashicorp/go-multierror v1.1.1 // indirect 54 | github.com/hashicorp/hcl v1.0.0 // indirect 55 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 56 | github.com/josharian/intern v1.0.0 // indirect 57 | github.com/json-iterator/go v1.1.12 // indirect 58 | github.com/klauspost/compress v1.16.0 // indirect 59 | github.com/klauspost/pgzip v1.2.6-0.20220930104621-17e8dac29df8 // indirect 60 | github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf // indirect 61 | github.com/magiconair/properties v1.8.7 // indirect 62 | github.com/mailru/easyjson v0.7.7 // indirect 63 | github.com/mattn/go-runewidth v0.0.14 // indirect 64 | github.com/miekg/pkcs11 v1.1.1 // indirect 65 | github.com/mitchellh/go-homedir v1.1.0 // indirect 66 | github.com/mitchellh/mapstructure v1.5.0 // indirect 67 | github.com/moby/sys/mountinfo v0.6.2 // indirect 68 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 69 | github.com/modern-go/reflect2 v1.0.2 // indirect 70 | github.com/oklog/ulid v1.3.1 // indirect 71 | github.com/opencontainers/go-digest v1.0.0 // indirect 72 | github.com/opencontainers/image-spec v1.1.0-rc2 // indirect 73 | github.com/opencontainers/runc v1.1.4 // indirect 74 | github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 // indirect 75 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 76 | github.com/pkg/errors v0.9.1 // indirect 77 | github.com/proglottis/gpgme v0.1.3 // indirect 78 | github.com/rivo/uniseg v0.4.3 // indirect 79 | github.com/sigstore/fulcio v1.0.0 // indirect 80 | github.com/sigstore/rekor v1.0.1 // indirect 81 | github.com/sigstore/sigstore v1.5.1 // indirect 82 | github.com/spf13/afero v1.9.3 // indirect 83 | github.com/spf13/cast v1.5.0 // indirect 84 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 85 | github.com/spf13/pflag v1.0.5 // indirect 86 | github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 // indirect 87 | github.com/subosito/gotenv v1.4.2 // indirect 88 | github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect 89 | github.com/theupdateframework/go-tuf v0.5.2-0.20221207161717-9cb61d6e65f5 // indirect 90 | github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect 91 | github.com/ulikunitz/xz v0.5.11 // indirect 92 | github.com/vbatts/tar-split v0.11.2 // indirect 93 | github.com/vbauerster/mpb/v7 v7.5.3 // indirect 94 | go.etcd.io/bbolt v1.3.6 // indirect 95 | go.mongodb.org/mongo-driver v1.11.1 // indirect 96 | go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect 97 | golang.org/x/crypto v0.5.0 // indirect 98 | golang.org/x/mod v0.9.0 // indirect 99 | golang.org/x/net v0.8.0 // indirect 100 | golang.org/x/sync v0.1.0 // indirect 101 | golang.org/x/sys v0.6.0 // indirect 102 | golang.org/x/term v0.6.0 // indirect 103 | golang.org/x/text v0.8.0 // indirect 104 | golang.org/x/tools v0.7.0 // indirect 105 | google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect 106 | google.golang.org/grpc v1.52.0 // indirect 107 | google.golang.org/protobuf v1.29.0 // indirect 108 | gopkg.in/inf.v0 v0.9.1 // indirect 109 | gopkg.in/ini.v1 v1.67.0 // indirect 110 | gopkg.in/square/go-jose.v2 v2.6.0 // indirect 111 | gopkg.in/yaml.v3 v3.0.1 // indirect 112 | k8s.io/klog/v2 v2.90.0 // indirect 113 | k8s.io/utils v0.0.0-20230202215443-34013725500c // indirect 114 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 115 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 116 | ) 117 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 40 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 41 | github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= 42 | github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 43 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 44 | github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= 45 | github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= 46 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 47 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 48 | github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= 49 | github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= 50 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= 51 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= 52 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 53 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= 54 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 55 | github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= 56 | github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= 57 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 58 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 59 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 60 | github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= 61 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 62 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 63 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 64 | github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= 65 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 66 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 67 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 68 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 69 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 70 | github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= 71 | github.com/containers/image/v5 v5.24.2 h1:QcMsHBAXBPPnVYo6iEFarvaIpym7sBlwsGHPJlucxN0= 72 | github.com/containers/image/v5 v5.24.2/go.mod h1:oss5F6ssGQz8ZtC79oY+fuzYA3m3zBek9tq9gmhuvHc= 73 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= 74 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= 75 | github.com/containers/ocicrypt v1.1.7 h1:thhNr4fu2ltyGz8aMx8u48Ae0Pnbip3ePP9/mzkZ/3U= 76 | github.com/containers/ocicrypt v1.1.7/go.mod h1:7CAhjcj2H8AYp5YvEie7oVSK2AhBY8NscCYRawuDNtw= 77 | github.com/containers/storage v1.45.3 h1:GbtTvTtp3GW2/tcFg5VhgHXcYMwVn2KfZKiHjf9FAOM= 78 | github.com/containers/storage v1.45.3/go.mod h1:OdRUYHrq1HP6iAo79VxqtYuJzC5j4eA2I60jKOoCT7g= 79 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 80 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 81 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 82 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 83 | github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 h1:vU+EP9ZuFUCYE0NYLwTSob+3LNEJATzNfP/DC7SWGWI= 84 | github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= 85 | github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 86 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 87 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 88 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 89 | github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM= 90 | github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 91 | github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= 92 | github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 93 | github.com/docker/docker v23.0.2+incompatible h1:q81C2qQ/EhPm8COZMUGOQYh4qLv4Xu6CXELJ3WK/mlU= 94 | github.com/docker/docker v23.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 95 | github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= 96 | github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= 97 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 98 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 99 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 100 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 101 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 102 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 103 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 104 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 105 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 106 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 107 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 108 | github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= 109 | github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 h1:IeaD1VDVBPlx3viJT9Md8if8IxxJnO+x0JCGb054heg= 110 | github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 h1:a4DFiKFJiDRGFD1qIcqGLX/WlUMD9dyLSLDt+9QZgt8= 111 | github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= 112 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 113 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 114 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 115 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 116 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 117 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 118 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 119 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 120 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 121 | github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= 122 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 123 | github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= 124 | github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= 125 | github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= 126 | github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= 127 | github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= 128 | github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= 129 | github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc= 130 | github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= 131 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 132 | github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= 133 | github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 134 | github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= 135 | github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= 136 | github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= 137 | github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= 138 | github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= 139 | github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= 140 | github.com/go-openapi/runtime v0.24.1 h1:Sml5cgQKGYQHF+M7yYSHaH1eOjvTykrddTE/KtQVjqo= 141 | github.com/go-openapi/runtime v0.24.1/go.mod h1:AKurw9fNre+h3ELZfk6ILsfvPN+bvvlaU/M9q/r9hpk= 142 | github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= 143 | github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= 144 | github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI= 145 | github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= 146 | github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= 147 | github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= 148 | github.com/go-openapi/strfmt v0.21.2/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= 149 | github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= 150 | github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= 151 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 152 | github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 153 | github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= 154 | github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= 155 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 156 | github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= 157 | github.com/go-openapi/validate v0.22.0 h1:b0QecH6VslW/TxtpKgzpO1SNG7GU2FsaqKdP1E2T50Y= 158 | github.com/go-openapi/validate v0.22.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= 159 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 160 | github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= 161 | github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= 162 | github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= 163 | github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= 164 | github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= 165 | github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 166 | github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= 167 | github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= 168 | github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 169 | github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= 170 | github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= 171 | github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= 172 | github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= 173 | github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= 174 | github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= 175 | github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= 176 | github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= 177 | github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= 178 | github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= 179 | github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 180 | github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= 181 | github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 182 | github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= 183 | github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= 184 | github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= 185 | github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= 186 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 187 | github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 188 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 189 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 190 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 191 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 192 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 193 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 194 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 195 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 196 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 197 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 198 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 199 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 200 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 201 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 202 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 203 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 204 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 205 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 206 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 207 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 208 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 209 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 210 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 211 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 212 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 213 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 214 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 215 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 216 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 217 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 218 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 219 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 220 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 221 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 222 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 223 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 224 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 225 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 226 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 227 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 228 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 229 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 230 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 231 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 232 | github.com/google/go-containerregistry v0.14.0 h1:z58vMqHxuwvAsVwvKEkmVBz2TlgBgH5k6koEXBtlYkw= 233 | github.com/google/go-containerregistry v0.14.0/go.mod h1:aiJ2fp/SXvkWgmYHioXnbMdlgB8eXiiYOY55gfN91Wk= 234 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 235 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 236 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 237 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 238 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 239 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 240 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 241 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 242 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 243 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 244 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 245 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 246 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 247 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 248 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 249 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 250 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 251 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 252 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 253 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 254 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 255 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 256 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 257 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 258 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 259 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 260 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 261 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 262 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 263 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 264 | github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= 265 | github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 266 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 267 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 268 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 269 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 270 | github.com/honeycombio/beeline-go v1.10.0 h1:cUDe555oqvw8oD76BQJ8alk7FP0JZ/M/zXpNvOEDLDc= 271 | github.com/honeycombio/libhoney-go v1.16.0 h1:kPpqoz6vbOzgp7jC6SR7SkNj7rua7rgxvznI6M3KdHc= 272 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 273 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 274 | github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= 275 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 276 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 277 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 278 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 279 | github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548 h1:dYTbLf4m0a5u0KLmPfB6mgxbcV7588bOCx79hxa5Sr4= 280 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 281 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 282 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 283 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 284 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 285 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 286 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 287 | github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= 288 | github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= 289 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 290 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 291 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 292 | github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= 293 | github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 294 | github.com/klauspost/pgzip v1.2.6-0.20220930104621-17e8dac29df8 h1:BcxbplxjtczA1a6d3wYoa7a0WL3rq9DKBMGHeKyjEF0= 295 | github.com/klauspost/pgzip v1.2.6-0.20220930104621-17e8dac29df8/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= 296 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 297 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 298 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 299 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 300 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 301 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 302 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 303 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 304 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 305 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 306 | github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf h1:ndns1qx/5dL43g16EQkPV/i8+b3l5bYQwLeoSBe7tS8= 307 | github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf/go.mod h1:aGkAgvWY/IUcVFfuly53REpfv5edu25oij+qHRFaraA= 308 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 309 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 310 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 311 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 312 | github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 313 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 314 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 315 | github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= 316 | github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= 317 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 318 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 319 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 320 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= 321 | github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= 322 | github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= 323 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 324 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 325 | github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 326 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 327 | github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 328 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 329 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 330 | github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= 331 | github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= 332 | github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= 333 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= 334 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 335 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 336 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 337 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 338 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 339 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 340 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 341 | github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= 342 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 343 | github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= 344 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 345 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 346 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 347 | github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 348 | github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= 349 | github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= 350 | github.com/opencontainers/runc v1.1.4 h1:nRCz/8sKg6K6jgYAFLDlXzPeITBZJyX28DBVhWD+5dg= 351 | github.com/opencontainers/runc v1.1.4/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= 352 | github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 h1:3snG66yBm59tKhhSPQrQ/0bCrv1LQbKt40LnUPiUxdc= 353 | github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 354 | github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= 355 | github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 356 | github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= 357 | github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= 358 | github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 359 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 360 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 361 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 362 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 363 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 364 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 365 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 366 | github.com/proglottis/gpgme v0.1.3 h1:Crxx0oz4LKB3QXc5Ea0J19K/3ICfy3ftr5exgUK1AU0= 367 | github.com/proglottis/gpgme v0.1.3/go.mod h1:fPbW/EZ0LvwQtH8Hy7eixhp1eF3G39dtx7GUN+0Gmy0= 368 | github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.64.0 h1:bqFOzWYCuSZEcuFx/ez8DFW+fqGiUEATrgezynCjpP4= 369 | github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.64.0/go.mod h1:cfNgxpCPGyIydmt3HcwDqKDt0nYdlGRhzftl+DZH7WA= 370 | github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= 371 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 372 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 373 | github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= 374 | github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= 375 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 376 | github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= 377 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 378 | github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 379 | github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 380 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 381 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 382 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 383 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 384 | github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= 385 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 386 | github.com/sigstore/fulcio v1.0.0 h1:hBZW6qg9GXTtCX8jOg1hmyjYLrmsEKZGeMwAbW3XNEg= 387 | github.com/sigstore/fulcio v1.0.0/go.mod h1:j4MzLxX/Be0rHYh3JF2dgMorkWGzEMHBqIHwFU8I/Rw= 388 | github.com/sigstore/rekor v1.0.1 h1:rcESXSNkAPRWFYZel9rarspdvneET60F2ngNkadi89c= 389 | github.com/sigstore/rekor v1.0.1/go.mod h1:ecTKdZWGWqE1pl3U1m1JebQJLU/hSjD9vYHOmHQ7w4g= 390 | github.com/sigstore/sigstore v1.5.1 h1:iUou0QJW8eQKMUkTXbFyof9ZOblDtfaW2Sn2+QI8Tcs= 391 | github.com/sigstore/sigstore v1.5.1/go.mod h1:3i6UTWVNtFwOtbgG63FZZNID4vO9KcO8AszIJlaNI8k= 392 | github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 393 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 394 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 395 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 396 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 397 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 398 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 399 | github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= 400 | github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= 401 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 402 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 403 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 404 | github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= 405 | github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 406 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 407 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 408 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 409 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 410 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 411 | github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= 412 | github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= 413 | github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 h1:lIOOHPEbXzO3vnmx2gok1Tfs31Q8GQqKLc8vVqyQq/I= 414 | github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= 415 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 416 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 417 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 418 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 419 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 420 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 421 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 422 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 423 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 424 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 425 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 426 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 427 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 428 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 429 | github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= 430 | github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= 431 | github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= 432 | github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= 433 | github.com/theupdateframework/go-tuf v0.5.2-0.20221207161717-9cb61d6e65f5 h1:s+Yvt6bzRwHljSE7j6DLBDcfpZEdBhrvLgOUmd8f7ZM= 434 | github.com/theupdateframework/go-tuf v0.5.2-0.20221207161717-9cb61d6e65f5/go.mod h1:Le8NAjvDJK1vmLgpVYr4AR1Tqam/b/mTdQyTy37UJDA= 435 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 436 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 437 | github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= 438 | github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= 439 | github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= 440 | github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 441 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 442 | github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 443 | github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME= 444 | github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= 445 | github.com/vbauerster/mpb/v7 v7.5.3 h1:BkGfmb6nMrrBQDFECR/Q7RkKCw7ylMetCb4079CGs4w= 446 | github.com/vbauerster/mpb/v7 v7.5.3/go.mod h1:i+h4QY6lmLvBNK2ah1fSreiw3ajskRlBp9AhY/PnuOE= 447 | github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= 448 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= 449 | github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= 450 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 451 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 452 | github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= 453 | github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= 454 | github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= 455 | github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= 456 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 457 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 458 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 459 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 460 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 461 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 462 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 463 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 464 | go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= 465 | go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= 466 | go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= 467 | go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= 468 | go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= 469 | go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= 470 | go.mongodb.org/mongo-driver v1.11.1 h1:QP0znIRTuL0jf1oBQoAoM0C6ZJfBK4kx0Uumtv1A7w8= 471 | go.mongodb.org/mongo-driver v1.11.1/go.mod h1:s7p5vEtfbeR1gYi6pnj3c3/urpbLv2T5Sfd6Rp2HBB8= 472 | go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= 473 | go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= 474 | go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= 475 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 476 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 477 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 478 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 479 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 480 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 481 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 482 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 483 | golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 484 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 485 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 486 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 487 | golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 488 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 489 | golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 490 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 491 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 492 | golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 493 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 494 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 495 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 496 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 497 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 498 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 499 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 500 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 501 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 502 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 503 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 504 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 505 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 506 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 507 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 508 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 509 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 510 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 511 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 512 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 513 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 514 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 515 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 516 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 517 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 518 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 519 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 520 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 521 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 522 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 523 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 524 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 525 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 526 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 527 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 528 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 529 | golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= 530 | golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 531 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 532 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 533 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 534 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 535 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 536 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 537 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 538 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 539 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 540 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 541 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 542 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 543 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 544 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 545 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 546 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 547 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 548 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 549 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 550 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 551 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 552 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 553 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 554 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 555 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 556 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 557 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 558 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 559 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 560 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 561 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 562 | golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 563 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 564 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 565 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 566 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 567 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 568 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 569 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 570 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 571 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 572 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 573 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 574 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 575 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 576 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 577 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 578 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 579 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 580 | golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 581 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 582 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 583 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 584 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 585 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 586 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 587 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 588 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 589 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 590 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 591 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 592 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 593 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 594 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 595 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 596 | golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 597 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 598 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 599 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 600 | golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 601 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 602 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 603 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 604 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 605 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 606 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 607 | golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 608 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 609 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 610 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 611 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 612 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 613 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 614 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 615 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 616 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 617 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 618 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 619 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 620 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 621 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 622 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 623 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 624 | golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 625 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 626 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 627 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 628 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 629 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 630 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 631 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 632 | golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 633 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 634 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 635 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 636 | golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 637 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 638 | golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 639 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 640 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 641 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 642 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 643 | golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 644 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 645 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 646 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 647 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 648 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 649 | golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= 650 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 651 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 652 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 653 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 654 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 655 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 656 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 657 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 658 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 659 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 660 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 661 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 662 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 663 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 664 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 665 | golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE= 666 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 667 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 668 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 669 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 670 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 671 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 672 | golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 673 | golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 674 | golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 675 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 676 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 677 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 678 | golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 679 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 680 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 681 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 682 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 683 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 684 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 685 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 686 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 687 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 688 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 689 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 690 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 691 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 692 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 693 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 694 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 695 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 696 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 697 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 698 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 699 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 700 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 701 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 702 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 703 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 704 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 705 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 706 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 707 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 708 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 709 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 710 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 711 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 712 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 713 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 714 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 715 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 716 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 717 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 718 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 719 | golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= 720 | golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 721 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 722 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 723 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 724 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 725 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 726 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 727 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 728 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 729 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 730 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 731 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 732 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 733 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 734 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 735 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 736 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 737 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 738 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 739 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 740 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 741 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 742 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 743 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 744 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 745 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 746 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 747 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 748 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 749 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 750 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 751 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 752 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 753 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 754 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 755 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 756 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 757 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 758 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 759 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 760 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 761 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 762 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 763 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 764 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 765 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 766 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 767 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 768 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 769 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 770 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 771 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 772 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 773 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 774 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 775 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 776 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 777 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 778 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 779 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 780 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 781 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 782 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 783 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 784 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 785 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 786 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 787 | google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef h1:uQ2vjV/sHTsWSqdKeLqmwitzgvjMl7o4IdtHwUDXSJY= 788 | google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= 789 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 790 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 791 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 792 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 793 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 794 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 795 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 796 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 797 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 798 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 799 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 800 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 801 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 802 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 803 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 804 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 805 | google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk= 806 | google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= 807 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 808 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 809 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 810 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 811 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 812 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 813 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 814 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 815 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 816 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 817 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 818 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 819 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 820 | google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0= 821 | google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 822 | gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= 823 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 824 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 825 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 826 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 827 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 828 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 829 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 830 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 831 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 832 | gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 833 | gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= 834 | gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 835 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 836 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 837 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 838 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 839 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 840 | gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 841 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 842 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 843 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 844 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 845 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 846 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 847 | gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= 848 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 849 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 850 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 851 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 852 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 853 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 854 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 855 | k8s.io/api v0.26.3 h1:emf74GIQMTik01Aum9dPP0gAypL8JTLl/lHa4V9RFSU= 856 | k8s.io/api v0.26.3/go.mod h1:PXsqwPMXBSBcL1lJ9CYDKy7kIReUydukS5JiRlxC3qE= 857 | k8s.io/apimachinery v0.26.3 h1:dQx6PNETJ7nODU3XPtrwkfuubs6w7sX0M8n61zHIV/k= 858 | k8s.io/apimachinery v0.26.3/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= 859 | k8s.io/klog/v2 v2.90.0 h1:VkTxIV/FjRXn1fgNNcKGM8cfmL1Z33ZjXRTVxKCoF5M= 860 | k8s.io/klog/v2 v2.90.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 861 | k8s.io/utils v0.0.0-20230202215443-34013725500c h1:YVqDar2X7YiQa/DVAXFMDIfGF8uGrHQemlrwRU5NlVI= 862 | k8s.io/utils v0.0.0-20230202215443-34013725500c/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 863 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 864 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 865 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 866 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 867 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 868 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= 869 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= 870 | sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= 871 | -------------------------------------------------------------------------------- /internal/commands/check.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/plexsystems/sinker/internal/docker" 10 | "github.com/plexsystems/sinker/internal/manifest" 11 | 12 | "github.com/hashicorp/go-version" 13 | log "github.com/sirupsen/logrus" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | func newCheckCommand() *cobra.Command { 19 | cmd := cobra.Command{ 20 | Use: "check", 21 | Short: "Check for newer images", 22 | PreRunE: func(cmd *cobra.Command, args []string) error { 23 | if err := viper.BindPFlag("images", cmd.Flags().Lookup("images")); err != nil { 24 | return fmt.Errorf("bind images flag: %w", err) 25 | } 26 | 27 | return nil 28 | }, 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | var input string 31 | if len(args) > 0 { 32 | input = args[0] 33 | } 34 | 35 | if err := runCheckCommand(input); err != nil { 36 | return fmt.Errorf("check: %w", err) 37 | } 38 | 39 | return nil 40 | }, 41 | } 42 | 43 | cmd.Flags().StringSliceP("images", "i", []string{}, "List of images to check (e.g. host.com/repo:v1.0.0)") 44 | 45 | return &cmd 46 | } 47 | 48 | func runCheckCommand(input string) error { 49 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 50 | defer cancel() 51 | 52 | client, err := docker.New(log.Infof) 53 | if err != nil { 54 | return fmt.Errorf("new client: %w", err) 55 | } 56 | 57 | var imagesToCheck []string 58 | if input == "-" { 59 | imagesToCheck, err = manifest.GetImagesFromStandardInput() 60 | } else if len(viper.GetStringSlice("images")) > 0 { 61 | imagesToCheck = viper.GetStringSlice("images") 62 | } else { 63 | imageManifest, err := manifest.Get(viper.GetString("manifest")) 64 | if err != nil { 65 | return fmt.Errorf("get manifest: %w", err) 66 | } 67 | 68 | for _, source := range imageManifest.Sources { 69 | imagesToCheck = append(imagesToCheck, source.Image()) 70 | } 71 | } 72 | if err != nil { 73 | return fmt.Errorf("get images to check: %w", err) 74 | } 75 | 76 | var images []docker.RegistryPath 77 | for _, image := range imagesToCheck { 78 | images = append(images, docker.RegistryPath(image)) 79 | } 80 | 81 | for _, image := range images { 82 | if image.Tag() == "" { 83 | continue 84 | } 85 | 86 | imageVersion, err := version.NewVersion(image.Tag()) 87 | if err != nil { 88 | log.Infof("Image %s has an invalid version. Skipping ...", image) 89 | continue 90 | } 91 | 92 | tags, err := client.GetTagsForRepository(ctx, image.Host(), image.Repository()) 93 | if err != nil { 94 | return fmt.Errorf("get tags: %w", err) 95 | } 96 | tags = filterTags(tags) 97 | 98 | newerVersions := getNewerVersions(imageVersion, tags) 99 | if len(newerVersions) == 0 { 100 | log.Infof("Image %s is up to date!", image) 101 | continue 102 | } 103 | 104 | log.Infof("New versions for %v found: %v", image, newerVersions) 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func getNewerVersions(currentVersion *version.Version, foundTags []string) []string { 111 | var newerVersions []string 112 | for _, foundTag := range foundTags { 113 | tag, err := version.NewVersion(foundTag) 114 | if err != nil { 115 | continue 116 | } 117 | 118 | if currentVersion.LessThan(tag) { 119 | newerVersions = append(newerVersions, tag.Original()) 120 | } 121 | } 122 | 123 | // For images that are very out of date, the number of newer versions can be quite long. 124 | // Only return the latest 5 releases to keep the list manageable. 125 | if len(newerVersions) > 5 { 126 | newerVersions = newerVersions[len(newerVersions)-5:] 127 | newerVersions = append([]string{"..."}, newerVersions...) 128 | } 129 | 130 | return newerVersions 131 | } 132 | 133 | func filterTags(tags []string) []string { 134 | var filteredTags []string 135 | for _, tag := range tags { 136 | semverTag, err := version.NewSemver(tag) 137 | if err != nil { 138 | continue 139 | } 140 | 141 | if !strings.EqualFold(semverTag.String(), tag) && !strings.EqualFold("v"+semverTag.String(), tag) { 142 | continue 143 | } 144 | 145 | // Remove tags that include architectures and other strings 146 | // not necessarily related to a release. 147 | allowedPreReleases := []string{"alpha", "beta", "rc"} 148 | if strings.Contains(tag, "-") && !containsSubstring(allowedPreReleases, tag) { 149 | continue 150 | } 151 | 152 | filteredTags = append(filteredTags, tag) 153 | } 154 | 155 | return filteredTags 156 | } 157 | 158 | func containsSubstring(items []string, item string) bool { 159 | for _, currentItem := range items { 160 | if strings.Contains(item, currentItem) { 161 | return true 162 | } 163 | } 164 | 165 | return false 166 | } 167 | -------------------------------------------------------------------------------- /internal/commands/check_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/hashicorp/go-version" 8 | ) 9 | 10 | func TestFilterTags(t *testing.T) { 11 | tags := []string{ 12 | "noperiods", 13 | "contains-hypen", 14 | "1.0.0", 15 | "v1.0.0", 16 | } 17 | 18 | actual := filterTags(tags) 19 | expected := []string{"1.0.0", "v1.0.0"} 20 | 21 | if !reflect.DeepEqual(actual, expected) { 22 | t.Errorf("unexpected filtering of tags. expected %v actual %v", expected, actual) 23 | } 24 | } 25 | 26 | func TestNewerVersions(t *testing.T) { 27 | currentTag, err := version.NewVersion("v0.1.0") 28 | if err != nil { 29 | t.Fatal("new version:", err) 30 | } 31 | 32 | foundTags := []string{"v1.0.0", "v2.0.0", "v3.0.0", "v4.0.0", "v5.0.0", "v6.0.0"} 33 | 34 | actual := getNewerVersions(currentTag, foundTags) 35 | 36 | expected := []string{"...", "v2.0.0", "v3.0.0", "v4.0.0", "v5.0.0", "v6.0.0"} 37 | 38 | if !reflect.DeepEqual(actual, expected) { 39 | t.Errorf("unexpected filtering of tags. expected %v actual %v", expected, actual) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/commands/copy.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/containers/image/v5/copy" 10 | dockerv5 "github.com/containers/image/v5/docker" 11 | "github.com/containers/image/v5/signature" 12 | "github.com/containers/image/v5/types" 13 | "github.com/plexsystems/sinker/internal/docker" 14 | "github.com/plexsystems/sinker/internal/manifest" 15 | log "github.com/sirupsen/logrus" 16 | "github.com/spf13/cobra" 17 | "github.com/spf13/viper" 18 | ) 19 | 20 | func newCopyCommand() *cobra.Command { 21 | cmd := cobra.Command{ 22 | Use: "copy", 23 | Short: "Copy the images in the manifest directly from source to target repository", 24 | PreRunE: func(cmd *cobra.Command, args []string) error { 25 | flags := []string{"dryrun", "images", "target", "force", "override-arch", "override-os", "all-variants"} 26 | for _, flag := range flags { 27 | if err := viper.BindPFlag(flag, cmd.Flags().Lookup(flag)); err != nil { 28 | return fmt.Errorf("bind flag: %w", err) 29 | } 30 | } 31 | 32 | if len(viper.GetStringSlice("images")) > 0 && viper.GetString("target") == "" { 33 | return errors.New("target must be specified when using the images flag") 34 | } 35 | 36 | return nil 37 | }, 38 | 39 | RunE: func(cmd *cobra.Command, args []string) error { 40 | if err := runCopyCommand(); err != nil { 41 | return fmt.Errorf("copy: %w", err) 42 | } 43 | 44 | return nil 45 | }, 46 | } 47 | 48 | cmd.Flags().Bool("dryrun", false, "Print a list of images that would be copied to the target") 49 | cmd.Flags().StringSliceP("images", "i", []string{}, "List of images to copy to target") 50 | cmd.Flags().StringP("target", "t", "", "Registry the images will be copied to") 51 | cmd.Flags().Bool("force", false, "Force the copy of the image even if already exists at the target") 52 | cmd.Flags().StringP("override-arch", "a", "", "Architecture variant of the image if it is a multi-arch image") 53 | cmd.Flags().StringP("override-os", "o", "", "Operating system variant of the image if it is a multi-os image") 54 | cmd.Flags().Bool("all-variants", false, "Copy all variants of the image") 55 | 56 | return &cmd 57 | } 58 | 59 | func runCopyCommand() error { 60 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) 61 | defer cancel() 62 | 63 | // Use Docker client for queries that do not require access to docker socket. 64 | client, err := docker.New(log.Infof) 65 | if err != nil { 66 | return fmt.Errorf("new client: %w", err) 67 | } 68 | 69 | var sources []manifest.Source 70 | if len(viper.GetStringSlice("images")) > 0 { 71 | sources = manifest.GetSourcesFromImages(viper.GetStringSlice("images"), viper.GetString("target")) 72 | } else { 73 | imageManifest, err := manifest.Get(viper.GetString("manifest")) 74 | if err != nil { 75 | return fmt.Errorf("get manifest: %w", err) 76 | } 77 | 78 | sources = imageManifest.Sources 79 | } 80 | 81 | log.Infof("Finding images that need to be copied ...") 82 | 83 | var sourcesToCopy []manifest.Source 84 | for _, source := range sources { 85 | exists, err := client.ImageExistsAtRemote(ctx, source.TargetImage()) 86 | if err != nil { 87 | return fmt.Errorf("image exists at remote: %w", err) 88 | } 89 | 90 | if !exists || viper.GetBool("force") { 91 | sourcesToCopy = append(sourcesToCopy, source) 92 | } 93 | } 94 | 95 | if len(sourcesToCopy) == 0 { 96 | log.Infof("All images are up to date!") 97 | return nil 98 | } 99 | 100 | if viper.GetBool("dryrun") { 101 | for _, source := range sourcesToCopy { 102 | log.Infof("Image %s would be copied to %s", source.Image(), source.TargetImage()) 103 | } 104 | 105 | return nil 106 | } 107 | 108 | // Create a default image policy accepting unsigned images. 109 | policy := &signature.Policy{ 110 | Default: []signature.PolicyRequirement{ 111 | signature.NewPRInsecureAcceptAnything(), 112 | }, 113 | } 114 | policyContext, err := signature.NewPolicyContext(policy) 115 | if err != nil { 116 | return fmt.Errorf("new policy context: %w", err) 117 | } 118 | 119 | var copyOptions copy.Options 120 | if viper.GetBool("all-variants") { 121 | copyOptions.ImageListSelection = copy.CopyAllImages 122 | } else { 123 | copyOptions.ImageListSelection = copy.CopySystemImage 124 | } 125 | 126 | if viper.GetString("override-os") != "" || viper.GetString("override-arch") != "" { 127 | copyOptions.SourceCtx = &types.SystemContext{ 128 | ArchitectureChoice: viper.GetString("override-arch"), 129 | OSChoice: viper.GetString("override-os"), 130 | } 131 | } 132 | 133 | imageTransport := dockerv5.Transport 134 | for _, source := range sourcesToCopy { 135 | log.Infof("Copying image %s to %s", source.Image(), source.TargetImage()) 136 | destRef, err := imageTransport.ParseReference(fmt.Sprintf("//%s", source.TargetImage())) 137 | if err != nil { 138 | return fmt.Errorf("Error parsing target image reference: %w", err) 139 | } 140 | 141 | srcRef, err := imageTransport.ParseReference(fmt.Sprintf("//%s", source.Image())) 142 | if err != nil { 143 | return fmt.Errorf("Error parsing source image reference: %w", err) 144 | } 145 | 146 | if _, err := copy.Image(ctx, policyContext, destRef, srcRef, ©Options); err != nil { 147 | return fmt.Errorf("copy image: %w", err) 148 | } 149 | } 150 | 151 | log.Infof("All images have been copied!") 152 | return nil 153 | } 154 | -------------------------------------------------------------------------------- /internal/commands/create.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/plexsystems/sinker/internal/docker" 8 | "github.com/plexsystems/sinker/internal/manifest" 9 | 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | func newCreateCommand() *cobra.Command { 15 | cmd := cobra.Command{ 16 | Use: "create ", 17 | Short: "Create a new a manifest", 18 | PreRunE: func(cmd *cobra.Command, args []string) error { 19 | flags := []string{"output", "target"} 20 | for _, flag := range flags { 21 | if err := viper.BindPFlag(flag, cmd.Flags().Lookup(flag)); err != nil { 22 | return fmt.Errorf("bind flag: %w", err) 23 | } 24 | } 25 | 26 | return nil 27 | }, 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | var path string 30 | if len(args) > 0 { 31 | path = args[0] 32 | } 33 | 34 | if err := runCreateCommand(path); err != nil { 35 | return fmt.Errorf("create: %w", err) 36 | } 37 | 38 | return nil 39 | }, 40 | } 41 | 42 | cmd.Flags().StringP("target", "t", "", "The target repository to sync images to (e.g. host.com/repo)") 43 | cmd.MarkFlagRequired("target") 44 | 45 | cmd.Flags().StringP("output", "o", "", "Path where the manifest file will be written to") 46 | 47 | return &cmd 48 | } 49 | 50 | func runCreateCommand(path string) error { 51 | manifestPath := viper.GetString("manifest") 52 | if manifestPath == "" { 53 | manifestPath = viper.GetString("output") 54 | } 55 | 56 | if _, err := manifest.Get(manifestPath); err == nil { 57 | return errors.New("manifest file already exists") 58 | } 59 | 60 | targetPath := docker.RegistryPath(viper.GetString("target")) 61 | target := manifest.Target{ 62 | Host: targetPath.Host(), 63 | Repository: targetPath.Repository(), 64 | } 65 | 66 | var images []string 67 | var err error 68 | if path == "-" { 69 | images, err = manifest.GetImagesFromStandardInput() 70 | } else if path != "" { 71 | images, err = manifest.GetImagesFromKubernetesManifests(path) 72 | } 73 | if err != nil { 74 | return fmt.Errorf("get images: %w", err) 75 | } 76 | 77 | emptyManifest := manifest.Manifest{ 78 | Target: target, 79 | } 80 | 81 | newManifest := emptyManifest.Update(images) 82 | if err := newManifest.Write(manifestPath); err != nil { 83 | return fmt.Errorf("write manifest: %w", err) 84 | } 85 | 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /internal/commands/default.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | "github.com/spf13/cobra" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | // sinkerVersion is set at build time. 12 | var sinkerVersion = "not set" 13 | 14 | // NewDefaultCommand creates the default command. 15 | func NewDefaultCommand() *cobra.Command { 16 | cmd := cobra.Command{ 17 | Use: path.Base(os.Args[0]), 18 | Short: "sinker", 19 | Long: "A tool to sync container images to another container registry", 20 | Version: sinkerVersion, 21 | } 22 | 23 | cmd.PersistentFlags().StringP("manifest", "m", "", "Path where the manifest file is (defaults to .images.yaml in the current directory)") 24 | viper.BindPFlag("manifest", cmd.PersistentFlags().Lookup("manifest")) 25 | 26 | viper.SetEnvPrefix("SINKER") 27 | viper.AutomaticEnv() 28 | 29 | cmd.AddCommand(newCreateCommand()) 30 | cmd.AddCommand(newUpdateCommand()) 31 | cmd.AddCommand(newListCommand()) 32 | cmd.AddCommand(newPullCommand()) 33 | cmd.AddCommand(newPushCommand()) 34 | cmd.AddCommand(newCopyCommand()) 35 | cmd.AddCommand(newCheckCommand()) 36 | cmd.AddCommand(newVersionCommand()) 37 | 38 | return &cmd 39 | } 40 | -------------------------------------------------------------------------------- /internal/commands/list.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/plexsystems/sinker/internal/manifest" 9 | 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | func newListCommand() *cobra.Command { 15 | cmd := cobra.Command{ 16 | Use: "list ", 17 | Short: "List the images found in the manifest", 18 | Args: cobra.ExactValidArgs(1), 19 | ValidArgs: []string{"source", "target"}, 20 | PreRunE: func(cmd *cobra.Command, args []string) error { 21 | if err := viper.BindPFlag("output", cmd.Flags().Lookup("output")); err != nil { 22 | return fmt.Errorf("bind output flag: %w", err) 23 | } 24 | 25 | return nil 26 | }, 27 | RunE: func(cmd *cobra.Command, args []string) error { 28 | origin := args[0] 29 | 30 | if err := runListCommand(origin); err != nil { 31 | return fmt.Errorf("list: %w", err) 32 | } 33 | 34 | return nil 35 | }, 36 | } 37 | 38 | cmd.Flags().StringP("output", "o", "", "Output the images in the manifest to a file") 39 | 40 | return &cmd 41 | } 42 | 43 | func runListCommand(origin string) error { 44 | manifestPath := viper.GetString("manifest") 45 | 46 | imageManifest, err := manifest.Get(manifestPath) 47 | if err != nil { 48 | return fmt.Errorf("get manifest: %w", err) 49 | } 50 | 51 | var images []string 52 | for _, source := range imageManifest.Sources { 53 | if strings.EqualFold(origin, "target") { 54 | images = append(images, source.TargetImage()) 55 | } else { 56 | images = append(images, source.Image()) 57 | } 58 | } 59 | 60 | // When the output flag is not provided, print the images to the console. 61 | if viper.GetString("output") == "" { 62 | for _, image := range images { 63 | fmt.Println(image) 64 | } 65 | 66 | return nil 67 | } 68 | 69 | fileList, err := os.Create(viper.GetString("output")) 70 | if err != nil { 71 | return fmt.Errorf("creating file: %w", err) 72 | } 73 | defer fileList.Close() 74 | 75 | for _, value := range images { 76 | if _, err := fmt.Fprintln(fileList, value); err != nil { 77 | return fmt.Errorf("writing image to file: %w", err) 78 | } 79 | } 80 | 81 | if err := fileList.Close(); err != nil { 82 | return fmt.Errorf("close: %w", err) 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /internal/commands/pull.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/plexsystems/sinker/internal/docker" 10 | "github.com/plexsystems/sinker/internal/manifest" 11 | 12 | log "github.com/sirupsen/logrus" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | ) 16 | 17 | func newPullCommand() *cobra.Command { 18 | cmd := cobra.Command{ 19 | Use: "pull ", 20 | Short: "Pull the images in the manifest", 21 | Args: cobra.OnlyValidArgs, 22 | ValidArgs: []string{"source", "target"}, 23 | PreRunE: func(cmd *cobra.Command, args []string) error { 24 | if err := viper.BindPFlag("images", cmd.Flags().Lookup("images")); err != nil { 25 | return fmt.Errorf("bind images flag: %w", err) 26 | } 27 | 28 | return nil 29 | }, 30 | RunE: func(cmd *cobra.Command, args []string) error { 31 | origin := "source" 32 | if len(args) > 0 { 33 | origin = args[0] 34 | } 35 | 36 | if err := runPullCommand(origin); err != nil { 37 | return fmt.Errorf("pull: %w", err) 38 | } 39 | 40 | return nil 41 | }, 42 | } 43 | 44 | cmd.Flags().StringSliceP("images", "i", []string{}, "List of images to pull (e.g. host.com/repo:v1.0.0)") 45 | 46 | return &cmd 47 | } 48 | 49 | func runPullCommand(origin string) error { 50 | manifestPath := viper.GetString("manifest") 51 | 52 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) 53 | defer cancel() 54 | 55 | client, err := docker.New(log.Infof) 56 | if err != nil { 57 | return fmt.Errorf("new client: %w", err) 58 | } 59 | 60 | var images map[string]string 61 | if len(viper.GetStringSlice("images")) > 0 { 62 | images, err = getImagesFromCommandLine(viper.GetStringSlice("images")) 63 | } else { 64 | images, err = getImagesFromManifest(manifestPath, origin) 65 | } 66 | if err != nil { 67 | return fmt.Errorf("get images: %w", err) 68 | } 69 | 70 | log.Infof("Finding images that need to be pulled from %v registry ...", origin) 71 | 72 | imagesToPull := make(map[string]string) 73 | for image, auth := range images { 74 | exists, err := client.ImageExistsOnHost(ctx, image) 75 | if err != nil { 76 | return fmt.Errorf("image host existence: %w", err) 77 | } 78 | 79 | if !exists { 80 | imagesToPull[image] = auth 81 | } 82 | } 83 | 84 | // Iterate through each of the images to pull and verify if the client has 85 | // the proper authorization to be able to successfully pull the images before 86 | // performing the pull operation. 87 | for image := range imagesToPull { 88 | if _, err := client.ImageExistsAtRemote(ctx, image); err != nil { 89 | return fmt.Errorf("validating remote image: %w", err) 90 | } 91 | } 92 | 93 | for image, auth := range imagesToPull { 94 | log.Infof("Pulling %s", image) 95 | 96 | if err := client.PullAndWait(ctx, image, auth); err != nil { 97 | log.Errorf("pull image and wait: " + err.Error()) 98 | } 99 | } 100 | 101 | log.Infof("All images have been pulled!") 102 | 103 | return nil 104 | } 105 | 106 | func getImagesFromManifest(path string, origin string) (map[string]string, error) { 107 | imageManifest, err := manifest.Get(path) 108 | if err != nil { 109 | return nil, fmt.Errorf("get manifest: %w", err) 110 | } 111 | 112 | images := make(map[string]string) 113 | for _, source := range imageManifest.Sources { 114 | var image string 115 | var auth string 116 | 117 | var err error 118 | if strings.EqualFold(origin, "target") { 119 | image = source.TargetImage() 120 | auth, err = source.Target.EncodedAuth() 121 | } else { 122 | image = source.Image() 123 | auth, err = source.EncodedAuth() 124 | } 125 | if err != nil { 126 | return nil, fmt.Errorf("get %s auth: %w", origin, err) 127 | } 128 | 129 | images[image] = auth 130 | } 131 | 132 | return images, nil 133 | } 134 | 135 | func getImagesFromCommandLine(images []string) (map[string]string, error) { 136 | imgs := make(map[string]string) 137 | for _, image := range images { 138 | registryPath := docker.RegistryPath(image) 139 | 140 | auth, err := docker.GetEncodedAuthForHost(registryPath.Host()) 141 | if err != nil { 142 | return nil, fmt.Errorf("get auth: %w", err) 143 | } 144 | 145 | imgs[image] = auth 146 | } 147 | 148 | return imgs, nil 149 | } 150 | -------------------------------------------------------------------------------- /internal/commands/push.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/plexsystems/sinker/internal/docker" 10 | "github.com/plexsystems/sinker/internal/manifest" 11 | 12 | log "github.com/sirupsen/logrus" 13 | "github.com/spf13/cobra" 14 | "github.com/spf13/viper" 15 | ) 16 | 17 | func newPushCommand() *cobra.Command { 18 | cmd := cobra.Command{ 19 | Use: "push", 20 | Short: "Push the images in the manifest to the target repository", 21 | PreRunE: func(cmd *cobra.Command, args []string) error { 22 | flags := []string{"dryrun", "images", "target"} 23 | for _, flag := range flags { 24 | if err := viper.BindPFlag(flag, cmd.Flags().Lookup(flag)); err != nil { 25 | return fmt.Errorf("bind flag: %w", err) 26 | } 27 | } 28 | 29 | if len(viper.GetStringSlice("images")) > 0 && viper.GetString("target") == "" { 30 | return errors.New("target must be specified when using the images flag") 31 | } 32 | 33 | return nil 34 | }, 35 | RunE: func(cmd *cobra.Command, args []string) error { 36 | if err := runPushCommand(); err != nil { 37 | return fmt.Errorf("push: %w", err) 38 | } 39 | 40 | return nil 41 | }, 42 | } 43 | 44 | cmd.Flags().Bool("dryrun", false, "Print a list of images that would be pushed to the target") 45 | cmd.Flags().StringSliceP("images", "i", []string{}, "List of images to push to target") 46 | cmd.Flags().StringP("target", "t", "", "Registry the images will be pushed to") 47 | 48 | return &cmd 49 | } 50 | 51 | func runPushCommand() error { 52 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) 53 | defer cancel() 54 | 55 | client, err := docker.New(log.Infof) 56 | if err != nil { 57 | return fmt.Errorf("new client: %w", err) 58 | } 59 | 60 | var sources []manifest.Source 61 | if len(viper.GetStringSlice("images")) > 0 { 62 | sources = manifest.GetSourcesFromImages(viper.GetStringSlice("images"), viper.GetString("target")) 63 | } else { 64 | imageManifest, err := manifest.Get(viper.GetString("manifest")) 65 | if err != nil { 66 | return fmt.Errorf("get manifest: %w", err) 67 | } 68 | 69 | sources = imageManifest.Sources 70 | } 71 | 72 | log.Infof("Finding images that need to be pushed ...") 73 | 74 | var sourcesToPush []manifest.Source 75 | for _, source := range sources { 76 | exists, err := client.ImageExistsAtRemote(ctx, source.TargetImage()) 77 | if err != nil { 78 | return fmt.Errorf("image exists at remote: %w", err) 79 | } 80 | 81 | if !exists { 82 | sourcesToPush = append(sourcesToPush, source) 83 | } 84 | } 85 | 86 | if len(sourcesToPush) == 0 { 87 | log.Infof("All images are up to date!") 88 | return nil 89 | } 90 | 91 | if viper.GetBool("dryrun") { 92 | for _, source := range sourcesToPush { 93 | log.Infof("Image %s would be pushed as %s", source.Image(), source.TargetImage()) 94 | } 95 | return nil 96 | } 97 | 98 | for _, source := range sourcesToPush { 99 | sourceExists, err := client.ImageExistsOnHost(ctx, source.Image()) 100 | if err != nil { 101 | return fmt.Errorf("image exists: %w", err) 102 | } 103 | 104 | if !sourceExists { 105 | log.Infof("Pulling %s", source.Image()) 106 | 107 | sourceAuth, err := source.EncodedAuth() 108 | if err != nil { 109 | return fmt.Errorf("get source auth: %w", err) 110 | } 111 | if err := client.PullAndWait(ctx, source.Image(), sourceAuth); err != nil { 112 | return fmt.Errorf("pull image and wait: %w", err) 113 | } 114 | } 115 | 116 | targetExists, err := client.ImageExistsOnHost(ctx, source.TargetImage()) 117 | if err != nil { 118 | return fmt.Errorf("target exists: %w", err) 119 | } 120 | if !targetExists { 121 | if err := client.Tag(ctx, source.Image(), source.TargetImage()); err != nil { 122 | return fmt.Errorf("tag image: %w", err) 123 | } 124 | } 125 | 126 | log.Infof("Pushing %s", source.TargetImage()) 127 | 128 | targetAuth, err := source.Target.EncodedAuth() 129 | if err != nil { 130 | return fmt.Errorf("get target auth: %w", err) 131 | } 132 | if err := client.PushAndWait(ctx, source.TargetImage(), targetAuth); err != nil { 133 | return fmt.Errorf("push image and wait: %w", err) 134 | } 135 | } 136 | 137 | log.Infof("All images have been pushed!") 138 | 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /internal/commands/update.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/plexsystems/sinker/internal/manifest" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | func newUpdateCommand() *cobra.Command { 13 | cmd := cobra.Command{ 14 | Use: "update ", 15 | Short: "Update an existing manifest", 16 | Args: cobra.ExactArgs(1), 17 | PreRunE: func(cmd *cobra.Command, args []string) error { 18 | if err := viper.BindPFlag("output", cmd.Flags().Lookup("output")); err != nil { 19 | return fmt.Errorf("bind output flag: %w", err) 20 | } 21 | 22 | return nil 23 | }, 24 | RunE: func(cmd *cobra.Command, args []string) error { 25 | outputPath := viper.GetString("manifest") 26 | if viper.GetString("output") != "" { 27 | outputPath = viper.GetString("output") 28 | } 29 | 30 | sourcePath := args[0] 31 | manifestPath := viper.GetString("manifest") 32 | if err := runUpdateCommand(sourcePath, manifestPath, outputPath); err != nil { 33 | return fmt.Errorf("update: %w", err) 34 | } 35 | 36 | return nil 37 | }, 38 | } 39 | 40 | cmd.Flags().StringP("output", "o", "", "Path where the updated manifest file will be written to") 41 | 42 | return &cmd 43 | } 44 | 45 | func runUpdateCommand(path string, manifestPath string, outputPath string) error { 46 | currentManifest, err := manifest.Get(manifestPath) 47 | if err != nil { 48 | return fmt.Errorf("get current manifest: %w", err) 49 | } 50 | 51 | var updatedImages []string 52 | if path == "-" { 53 | updatedImages, err = manifest.GetImagesFromStandardInput() 54 | } else { 55 | updatedImages, err = manifest.GetImagesFromKubernetesManifests(path) 56 | } 57 | if err != nil { 58 | return fmt.Errorf("get images: %w", err) 59 | } 60 | 61 | updatedManifest := currentManifest.Update(updatedImages) 62 | if err := updatedManifest.Write(outputPath); err != nil { 63 | return fmt.Errorf("write manifest: %w", err) 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /internal/commands/version.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func newVersionCommand() *cobra.Command { 10 | cmd := cobra.Command{ 11 | Use: "version", 12 | Short: "The version of sinker", 13 | 14 | Run: func(cmd *cobra.Command, args []string) { 15 | fmt.Printf("sinker version %v\n", sinkerVersion) 16 | }, 17 | } 18 | 19 | return &cmd 20 | } 21 | -------------------------------------------------------------------------------- /internal/docker/auth.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/google/go-containerregistry/pkg/authn" 9 | "github.com/google/go-containerregistry/pkg/name" 10 | ) 11 | 12 | // GetEncodedAuthForHost returns a Base64 encoded auth for the given host. 13 | func GetEncodedAuthForHost(host string) (string, error) { 14 | registryReference, err := name.NewRegistry(host, name.WeakValidation) 15 | if err != nil { 16 | return "", fmt.Errorf("new registry: %w", err) 17 | } 18 | 19 | auth, err := authn.DefaultKeychain.Resolve(registryReference) 20 | if err != nil { 21 | return "", fmt.Errorf("resolve auth: %w", err) 22 | } 23 | 24 | authConfig, err := auth.Authorization() 25 | if err != nil { 26 | return "", fmt.Errorf("get auth: %w", err) 27 | } 28 | 29 | jsonAuth, err := json.Marshal(authConfig) 30 | if err != nil { 31 | return "", fmt.Errorf("marshal auth: %w", err) 32 | } 33 | 34 | return base64.URLEncoding.EncodeToString(jsonAuth), nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/docker/docker.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "strings" 9 | "time" 10 | 11 | "github.com/avast/retry-go" 12 | "github.com/docker/docker/api/types" 13 | "github.com/docker/docker/client" 14 | "github.com/google/go-containerregistry/pkg/authn" 15 | "github.com/google/go-containerregistry/pkg/name" 16 | "github.com/google/go-containerregistry/pkg/v1/remote" 17 | "github.com/google/go-containerregistry/pkg/v1/remote/transport" 18 | ) 19 | 20 | // Client manages the communication with the Docker client. 21 | type Client struct { 22 | docker *client.Client 23 | logInfo func(format string, args ...interface{}) 24 | } 25 | 26 | // New returns a Docker client configured with the given information logger. 27 | func New(logInfo func(format string, args ...interface{})) (Client, error) { 28 | retry.DefaultDelay = 5 * time.Second 29 | retry.DefaultAttempts = 2 30 | 31 | dockerClient, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) 32 | if err != nil { 33 | return Client{}, fmt.Errorf("new docker client: %w", err) 34 | } 35 | 36 | client := Client{ 37 | docker: dockerClient, 38 | logInfo: logInfo, 39 | } 40 | 41 | return client, nil 42 | } 43 | 44 | // PushAndWait pushes an image and waits for it to finish pushing. 45 | // If an error occurs when pushing an image, the push will be attempted again before failing. 46 | func (c Client) PushAndWait(ctx context.Context, image string, auth string) error { 47 | push := func() error { 48 | if err := c.tryPushAndWait(ctx, image, auth); err != nil { 49 | return fmt.Errorf("try push image: %w", err) 50 | } 51 | 52 | return nil 53 | } 54 | 55 | retryFunc := func(attempts uint, err error) { 56 | c.logInfo("Unable to push %v (Retrying #%v)", image, attempts+1) 57 | } 58 | 59 | if err := retry.Do(push, retry.OnRetry(retryFunc)); err != nil { 60 | return fmt.Errorf("retry: %w", err) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | // PullAndWait pulls an image and waits for it to finish pulling. 67 | // If an error occurs when pulling an image, the pull will be attempted again before failing. 68 | func (c Client) PullAndWait(ctx context.Context, image string, auth string) error { 69 | pull := func() error { 70 | if err := c.tryPullAndWait(ctx, image, auth); err != nil { 71 | return fmt.Errorf("try pull image: %w", err) 72 | } 73 | 74 | return nil 75 | } 76 | 77 | retryFunc := func(attempts uint, err error) { 78 | c.logInfo("Unable to pull %v (Retrying #%v)", image, attempts+1) 79 | } 80 | 81 | if err := retry.Do(pull, retry.OnRetry(retryFunc)); err != nil { 82 | return fmt.Errorf("retry: %w", err) 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // ImageExistsOnHost returns true if the image exists on the host machine. 89 | func (c Client) ImageExistsOnHost(ctx context.Context, image string) (bool, error) { 90 | if hasLatestTag(image) { 91 | return false, nil 92 | } 93 | 94 | var images []string 95 | var err error 96 | if strings.Contains(image, "@") { 97 | images, err = c.GetAllDigestsOnHost(ctx) 98 | } else { 99 | images, err = c.GetAllImagesOnHost(ctx) 100 | } 101 | if err != nil { 102 | return false, fmt.Errorf("get all images: %w", err) 103 | } 104 | 105 | if imageExists(image, images) { 106 | return true, nil 107 | } 108 | 109 | return false, nil 110 | } 111 | 112 | // GetAllImagesOnHost gets all of the images and their tags on the host. 113 | func (c Client) GetAllImagesOnHost(ctx context.Context) ([]string, error) { 114 | summaries, err := c.docker.ImageList(ctx, types.ImageListOptions{}) 115 | if err != nil { 116 | return nil, fmt.Errorf("list images: %w", err) 117 | } 118 | 119 | var images []string 120 | for _, summary := range summaries { 121 | images = append(images, summary.RepoTags...) 122 | } 123 | 124 | return images, nil 125 | } 126 | 127 | // GetAllDigestsOnHost gets all of the images and their digests on the host. 128 | func (c Client) GetAllDigestsOnHost(ctx context.Context) ([]string, error) { 129 | summaries, err := c.docker.ImageList(ctx, types.ImageListOptions{}) 130 | if err != nil { 131 | return nil, fmt.Errorf("list images: %w", err) 132 | } 133 | 134 | var digests []string 135 | for _, summary := range summaries { 136 | digests = append(digests, summary.RepoDigests...) 137 | } 138 | 139 | return digests, nil 140 | } 141 | 142 | // GetTagsForRepository returns all of the tags for a given repository. 143 | func (c Client) GetTagsForRepository(ctx context.Context, host string, repository string) ([]string, error) { 144 | repoPath := "index.docker.io/" + repository 145 | if host != "" { 146 | repoPath = host + "/" + repository 147 | } 148 | 149 | repo, err := name.NewRepository(repoPath) 150 | if err != nil { 151 | return nil, fmt.Errorf("new repo: %w", err) 152 | } 153 | 154 | tags, err := remote.ListWithContext(ctx, repo, remote.WithAuthFromKeychain(authn.DefaultKeychain)) 155 | if err != nil { 156 | return nil, fmt.Errorf("list: %w", err) 157 | } 158 | 159 | return tags, nil 160 | } 161 | 162 | // Tag creates a new tag from the given target image that references the source image. 163 | func (c Client) Tag(ctx context.Context, sourceImage string, targetImage string) error { 164 | if err := c.docker.ImageTag(ctx, sourceImage, targetImage); err != nil { 165 | return fmt.Errorf("tag image: %w", err) 166 | } 167 | 168 | return nil 169 | } 170 | 171 | // ImageExistsAtRemote returns true if the image exists at the remote registry. 172 | func (c Client) ImageExistsAtRemote(ctx context.Context, image string) (bool, error) { 173 | reference, err := name.ParseReference(image, name.WeakValidation) 174 | if err != nil { 175 | return false, fmt.Errorf("parse ref: %w", err) 176 | } 177 | 178 | if _, err := remote.Get(reference, remote.WithAuthFromKeychain(authn.DefaultKeychain)); err != nil { 179 | 180 | // If the error is a transport error, check that the error code is of type 181 | // MANIFEST_UNKNOWN or NOT_FOUND. These errors are expected if an image does 182 | // not exist in the registry. 183 | if t, exists := err.(*transport.Error); exists { 184 | for _, diagnostic := range t.Errors { 185 | if strings.EqualFold("MANIFEST_UNKNOWN", string(diagnostic.Code)) { 186 | return false, nil 187 | } 188 | 189 | if strings.EqualFold("NOT_FOUND", string(diagnostic.Code)) { 190 | return false, nil 191 | } 192 | } 193 | } 194 | 195 | return false, fmt.Errorf("get image: %w", err) 196 | } 197 | 198 | // Always return false if the image has the latest tag as this method 199 | // is used to determine if the image should be pushed or not. The latest 200 | // tag is assumed to always need to be pushed, but a better approach 201 | // would be to compare digests. 202 | // 203 | // This check must also be performed after the Get request to the remote 204 | // registry to ensure that the client has appropriate access to pull the image. 205 | if hasLatestTag(image) { 206 | return false, nil 207 | } 208 | 209 | return true, nil 210 | } 211 | 212 | type progressDetail struct { 213 | Current int `json:"current"` 214 | Total int `json:"total"` 215 | } 216 | 217 | type statusLine struct { 218 | ID string `json:"id"` 219 | Message string `json:"status"` 220 | ProgressDetail progressDetail `json:"progressDetail"` 221 | ErrorMessage string `json:"error"` 222 | } 223 | 224 | func (c Client) waitForScannerComplete(clientScanner *bufio.Scanner, image string, command string) error { 225 | 226 | // Read the output of the Docker client until there is nothing left to read. 227 | // When there is nothing left to read, the underlying operation can be considered complete. 228 | var scans int 229 | for clientScanner.Scan() { 230 | var status statusLine 231 | if err := json.Unmarshal(clientScanner.Bytes(), &status); err != nil { 232 | return fmt.Errorf("unmarshal status: %w", err) 233 | } 234 | 235 | if status.ErrorMessage != "" { 236 | return fmt.Errorf("returned error: %s", status.ErrorMessage) 237 | } 238 | 239 | // Serves as makeshift polling to occasionally print the status of the Docker command. 240 | if scans%25 == 0 && status.ProgressDetail.Total > 0 { 241 | progress := fmt.Sprintf("Processing %vB of %vB", status.ProgressDetail.Current, status.ProgressDetail.Total) 242 | c.logInfo("%sing %s (%s)", command, image, progress) 243 | } 244 | 245 | scans++ 246 | } 247 | 248 | if clientScanner.Err() != nil { 249 | return fmt.Errorf("scanner: %w", clientScanner.Err()) 250 | } 251 | 252 | return nil 253 | } 254 | 255 | func (c Client) tryPullAndWait(ctx context.Context, image string, auth string) error { 256 | opts := types.ImagePullOptions{ 257 | RegistryAuth: auth, 258 | } 259 | reader, err := c.docker.ImagePull(ctx, image, opts) 260 | if err != nil { 261 | return fmt.Errorf("pull image: %w", err) 262 | } 263 | 264 | clientScanner := bufio.NewScanner(reader) 265 | if err := c.waitForScannerComplete(clientScanner, image, "Pull"); err != nil { 266 | return fmt.Errorf("wait for scanner: %w", err) 267 | } 268 | 269 | if err := reader.Close(); err != nil { 270 | return fmt.Errorf("close reader: %w", err) 271 | } 272 | 273 | return nil 274 | } 275 | 276 | func (c Client) tryPushAndWait(ctx context.Context, image string, auth string) error { 277 | opts := types.ImagePushOptions{ 278 | RegistryAuth: auth, 279 | } 280 | reader, err := c.docker.ImagePush(ctx, image, opts) 281 | if err != nil { 282 | return fmt.Errorf("push image: %w", err) 283 | } 284 | 285 | clientScanner := bufio.NewScanner(reader) 286 | if err := c.waitForScannerComplete(clientScanner, image, "Push"); err != nil { 287 | return fmt.Errorf("wait for scanner: %w", err) 288 | } 289 | 290 | if err := reader.Close(); err != nil { 291 | return fmt.Errorf("close reader: %w", err) 292 | } 293 | 294 | return nil 295 | } 296 | 297 | func imageExists(image string, images []string) bool { 298 | 299 | // When an image is sourced from docker hub, the image tag does 300 | // not include docker.io (or library) on the local machine. 301 | image = strings.ReplaceAll(image, "docker.io/library/", "") 302 | image = strings.ReplaceAll(image, "docker.io/", "") 303 | 304 | for _, currentImage := range images { 305 | if strings.EqualFold(currentImage, image) { 306 | return true 307 | } 308 | } 309 | 310 | return false 311 | } 312 | 313 | func hasLatestTag(image string) bool { 314 | if strings.Contains(image, ":latest") || !strings.Contains(image, ":") { 315 | return true 316 | } 317 | 318 | return false 319 | } 320 | -------------------------------------------------------------------------------- /internal/docker/docker_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import "testing" 4 | 5 | func TestImageExists_DockerIO(t *testing.T) { 6 | imagesOnHost := []string{"busybox:1.0.0", "plexsystems/busybox:1.0.0"} 7 | image := "docker.io/busybox:1.0.0" 8 | 9 | exists := imageExists(image, imagesOnHost) 10 | 11 | if !exists { 12 | t.Errorf("expected docker.io address to exist, but it did not.") 13 | } 14 | } 15 | 16 | func TestImageExists_DockerIO_WithLibrary(t *testing.T) { 17 | imagesOnHost := []string{"busybox:1.0.0", "plexsystems/busybox:1.0.0"} 18 | image := "docker.io/library/busybox:1.0.0" 19 | 20 | exists := imageExists(image, imagesOnHost) 21 | 22 | if !exists { 23 | t.Errorf("expected docker.io address to exist, but it did not.") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/docker/registrypath.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import "strings" 4 | 5 | // RegistryPath is a registry path for a container image. 6 | type RegistryPath string 7 | 8 | // Digest returns the digest in the registry path. 9 | func (r RegistryPath) Digest() string { 10 | if !strings.Contains(string(r), "@") { 11 | return "" 12 | } 13 | 14 | digestTokens := strings.Split(string(r), "@") 15 | return digestTokens[1] 16 | } 17 | 18 | // Tag returns the tag in the registry path. 19 | func (r RegistryPath) Tag() string { 20 | if strings.Contains(string(r), "@") || !strings.Contains(string(r), ":") { 21 | return "" 22 | } 23 | 24 | tagTokens := strings.Split(string(r), ":") 25 | return tagTokens[1] 26 | } 27 | 28 | // Host returns the host in the registry path. 29 | func (r RegistryPath) Host() string { 30 | host := string(r) 31 | 32 | if r.Tag() != "" { 33 | host = strings.ReplaceAll(host, ":"+r.Tag(), "") 34 | } 35 | 36 | if !strings.Contains(host, ".") { 37 | return "" 38 | } 39 | 40 | hostTokens := strings.Split(string(r), "/") 41 | return hostTokens[0] 42 | } 43 | 44 | // Repository is the repository in the registry path. 45 | func (r RegistryPath) Repository() string { 46 | repository := string(r) 47 | 48 | if r.Tag() != "" { 49 | repository = strings.ReplaceAll(repository, ":"+r.Tag(), "") 50 | } 51 | 52 | if r.Digest() != "" { 53 | repository = strings.ReplaceAll(repository, "@"+r.Digest(), "") 54 | } 55 | 56 | if r.Host() != "" { 57 | repository = strings.ReplaceAll(repository, r.Host(), "") 58 | } 59 | 60 | repository = strings.TrimLeft(repository, "/") 61 | return repository 62 | } 63 | -------------------------------------------------------------------------------- /internal/docker/registrypath_test.go: -------------------------------------------------------------------------------- 1 | package docker 2 | 3 | import "testing" 4 | 5 | type registryPathTest struct { 6 | actualPath RegistryPath 7 | expectedHost string 8 | expectedRepository string 9 | expectedTag string 10 | expectedDigest string 11 | } 12 | 13 | func TestRegistryPath_Empty(t *testing.T) { 14 | path := RegistryPath("") 15 | 16 | test := registryPathTest{ 17 | actualPath: path, 18 | expectedHost: "", 19 | expectedRepository: "", 20 | expectedTag: "", 21 | expectedDigest: "", 22 | } 23 | 24 | verifyRegistryPath(t, test) 25 | } 26 | 27 | func TestRegistryPath_Host(t *testing.T) { 28 | path := RegistryPath("host.com") 29 | 30 | test := registryPathTest{ 31 | actualPath: path, 32 | expectedHost: "host.com", 33 | expectedRepository: "", 34 | expectedTag: "", 35 | expectedDigest: "", 36 | } 37 | 38 | verifyRegistryPath(t, test) 39 | } 40 | 41 | func TestRegistryPath_Host_WithSlash(t *testing.T) { 42 | path := RegistryPath("host.com/") 43 | 44 | test := registryPathTest{ 45 | actualPath: path, 46 | expectedHost: "host.com", 47 | expectedRepository: "", 48 | expectedTag: "", 49 | expectedDigest: "", 50 | } 51 | 52 | verifyRegistryPath(t, test) 53 | } 54 | 55 | func TestRegistryPath_Repository_NoHost(t *testing.T) { 56 | path := RegistryPath("repo:v1.0.0") 57 | 58 | test := registryPathTest{ 59 | actualPath: path, 60 | expectedHost: "", 61 | expectedRepository: "repo", 62 | expectedTag: "v1.0.0", 63 | expectedDigest: "", 64 | } 65 | 66 | verifyRegistryPath(t, test) 67 | } 68 | 69 | func TestRegistryPath_Repository_RepeatedName(t *testing.T) { 70 | path := RegistryPath("repo/repository:v1.0.0") 71 | 72 | test := registryPathTest{ 73 | actualPath: path, 74 | expectedHost: "", 75 | expectedRepository: "repo/repository", 76 | expectedTag: "v1.0.0", 77 | expectedDigest: "", 78 | } 79 | 80 | verifyRegistryPath(t, test) 81 | } 82 | 83 | func TestRegistryPath_Repository_OneLevel(t *testing.T) { 84 | path := RegistryPath("host.com/repo") 85 | 86 | test := registryPathTest{ 87 | actualPath: path, 88 | expectedHost: "host.com", 89 | expectedRepository: "repo", 90 | expectedTag: "", 91 | expectedDigest: "", 92 | } 93 | 94 | verifyRegistryPath(t, test) 95 | } 96 | 97 | func TestRegistryPath_Repository_MultipleLevels(t *testing.T) { 98 | path := RegistryPath("host.com/repo/more") 99 | 100 | test := registryPathTest{ 101 | actualPath: path, 102 | expectedHost: "host.com", 103 | expectedRepository: "repo/more", 104 | expectedTag: "", 105 | expectedDigest: "", 106 | } 107 | 108 | verifyRegistryPath(t, test) 109 | } 110 | 111 | func TestRegistryPath_Tag(t *testing.T) { 112 | path := RegistryPath("host.com/repo:v1.0.0") 113 | 114 | test := registryPathTest{ 115 | actualPath: path, 116 | expectedHost: "host.com", 117 | expectedRepository: "repo", 118 | expectedTag: "v1.0.0", 119 | expectedDigest: "", 120 | } 121 | 122 | verifyRegistryPath(t, test) 123 | } 124 | 125 | func TestRegistryPath_Tag_None(t *testing.T) { 126 | path := RegistryPath("host.com/repo") 127 | 128 | test := registryPathTest{ 129 | actualPath: path, 130 | expectedHost: "host.com", 131 | expectedRepository: "repo", 132 | expectedTag: "", 133 | expectedDigest: "", 134 | } 135 | 136 | verifyRegistryPath(t, test) 137 | } 138 | 139 | func TestRegistryPath_Digest(t *testing.T) { 140 | path := RegistryPath("host.com/repo@sha256:abc123") 141 | 142 | test := registryPathTest{ 143 | actualPath: path, 144 | expectedHost: "host.com", 145 | expectedRepository: "repo", 146 | expectedTag: "", 147 | expectedDigest: "sha256:abc123", 148 | } 149 | 150 | verifyRegistryPath(t, test) 151 | } 152 | 153 | func verifyRegistryPath(t *testing.T, test registryPathTest) { 154 | if test.actualPath.Host() != test.expectedHost { 155 | t.Errorf("expected host to be %s, actual %s", test.expectedHost, test.actualPath.Host()) 156 | } 157 | 158 | if test.actualPath.Repository() != test.expectedRepository { 159 | t.Errorf("expected repository to be %s, actual %s", test.expectedRepository, test.actualPath.Repository()) 160 | } 161 | 162 | if test.actualPath.Tag() != test.expectedTag { 163 | t.Errorf("expected tag to be %s, actual %s", test.expectedTag, test.actualPath.Tag()) 164 | } 165 | 166 | if test.actualPath.Digest() != test.expectedDigest { 167 | t.Errorf("expected digest to be %s, actual %s", test.expectedDigest, test.actualPath.Digest()) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /internal/manifest/kubernetes.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/plexsystems/sinker/internal/docker" 12 | 13 | kubeyaml "github.com/ghodss/yaml" 14 | promv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" 15 | batchv1beta1 "k8s.io/api/batch/v1beta1" 16 | corev1 "k8s.io/api/core/v1" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | ) 19 | 20 | // GetImagesFromKubernetesManifests returns all images found in Kubernetes manifests 21 | // that are located at the specified path. 22 | func GetImagesFromKubernetesManifests(path string) ([]string, error) { 23 | resources, err := getResourceContentsFromYamlFiles(path) 24 | if err != nil { 25 | return nil, fmt.Errorf("get yaml files: %w", err) 26 | } 27 | 28 | images, err := GetImagesFromKubernetesResources(resources) 29 | if err != nil { 30 | return nil, fmt.Errorf("get images from resources: %w", err) 31 | } 32 | 33 | return images, nil 34 | } 35 | 36 | // GetImagesFromKubernetesResources returns all images found in Kubernetes resources that have 37 | // already been read from the disk, or are being read from standard input. 38 | func GetImagesFromKubernetesResources(resources []string) ([]string, error) { 39 | var splitResources []string 40 | for _, resourceContents := range resources { 41 | var lineBreak string 42 | if strings.Contains(resourceContents, "\r\n") && runtime.GOOS == "windows" { 43 | lineBreak = "\r\n" 44 | } else { 45 | lineBreak = "\n" 46 | } 47 | 48 | individualResources := strings.Split(resourceContents, lineBreak+"---"+lineBreak) 49 | splitResources = append(splitResources, individualResources...) 50 | } 51 | 52 | var imageList []string 53 | for _, resource := range splitResources { 54 | images, err := getImagesFromResource(resource) 55 | if err != nil { 56 | return nil, fmt.Errorf("get images from resource: %w", err) 57 | } 58 | 59 | imageList = append(imageList, images...) 60 | } 61 | 62 | imageList = dedupeImages(imageList) 63 | return imageList, nil 64 | } 65 | 66 | func getResourceContentsFromYamlFiles(path string) ([]string, error) { 67 | var filePaths []string 68 | err := filepath.Walk(path, func(currentFilePath string, fileInfo os.FileInfo, err error) error { 69 | if err != nil { 70 | return fmt.Errorf("walk path: %w", err) 71 | } 72 | 73 | if fileInfo.IsDir() && fileInfo.Name() == ".git" { 74 | return filepath.SkipDir 75 | } 76 | 77 | if fileInfo.IsDir() { 78 | return nil 79 | } 80 | 81 | if filepath.Ext(currentFilePath) != ".yaml" && filepath.Ext(currentFilePath) != ".yml" { 82 | return nil 83 | } 84 | 85 | filePaths = append(filePaths, currentFilePath) 86 | 87 | return nil 88 | }) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | var fileContents []string 94 | for _, filePath := range filePaths { 95 | contents, err := os.ReadFile(filePath) 96 | if err != nil { 97 | return nil, fmt.Errorf("read file: %w", err) 98 | } 99 | 100 | fileContents = append(fileContents, string(contents)) 101 | } 102 | 103 | return fileContents, nil 104 | } 105 | 106 | func getImagesFromResource(resource string) ([]string, error) { 107 | byteResource := []byte(resource) 108 | 109 | // If the resource does not contain a TypeMeta, it will not be a valid 110 | // Kubernetes resource and can be assumed to have no images. 111 | var typeMeta metav1.TypeMeta 112 | if err := kubeyaml.Unmarshal(byteResource, &typeMeta); err != nil { 113 | return []string{}, nil 114 | } 115 | 116 | if typeMeta.Kind == "Prometheus" { 117 | prometheusImages, err := getPrometheusImages(byteResource) 118 | if err != nil { 119 | return nil, fmt.Errorf("get prometheus images: %w", err) 120 | } 121 | 122 | return prometheusImages, nil 123 | } 124 | 125 | if typeMeta.Kind == "Alertmanager" { 126 | alertmanagerImages, err := getAlertmanagerImages(byteResource) 127 | if err != nil { 128 | return nil, fmt.Errorf("get alertmanager images: %w", err) 129 | } 130 | 131 | return alertmanagerImages, nil 132 | } 133 | 134 | if typeMeta.Kind == "Pod" { 135 | podImages, err := getPodImages(byteResource) 136 | if err != nil { 137 | return nil, fmt.Errorf("get pod images: %w", err) 138 | } 139 | 140 | return podImages, nil 141 | } 142 | 143 | if typeMeta.Kind == "CronJob" { 144 | cronJobImages, err := getCronJobImages(byteResource) 145 | if err != nil { 146 | return nil, fmt.Errorf("get cronjob images: %w", err) 147 | } 148 | 149 | return cronJobImages, nil 150 | } 151 | 152 | type BaseSpec struct { 153 | Template corev1.PodTemplateSpec `json:"template" protobuf:"bytes,3,opt,name=template"` 154 | } 155 | 156 | type BaseType struct { 157 | Spec BaseSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` 158 | } 159 | 160 | var contents BaseType 161 | if err := kubeyaml.Unmarshal(byteResource, &contents); err != nil { 162 | return []string{}, nil 163 | } 164 | 165 | var images []string 166 | images = append(images, getImagesFromContainers(contents.Spec.Template.Spec.InitContainers)...) 167 | images = append(images, getImagesFromContainers(contents.Spec.Template.Spec.Containers)...) 168 | 169 | return images, nil 170 | } 171 | 172 | func getPrometheusImages(resource []byte) ([]string, error) { 173 | var prometheus promv1.Prometheus 174 | if err := kubeyaml.Unmarshal(resource, &prometheus); err != nil { 175 | return nil, fmt.Errorf("unmarshal prometheus: %w", err) 176 | } 177 | 178 | var prometheusImage string 179 | if prometheus.Spec.BaseImage != "" { 180 | prometheusImage = prometheus.Spec.BaseImage + ":" + prometheus.Spec.Version 181 | } else { 182 | prometheusImage = *prometheus.Spec.Image 183 | } 184 | 185 | var images []string 186 | images = append(images, getImagesFromContainers(prometheus.Spec.Containers)...) 187 | images = append(images, getImagesFromContainers(prometheus.Spec.InitContainers)...) 188 | images = append(images, prometheusImage) 189 | 190 | return images, nil 191 | } 192 | 193 | func getAlertmanagerImages(resource []byte) ([]string, error) { 194 | var alertmanager promv1.Alertmanager 195 | if err := kubeyaml.Unmarshal(resource, &alertmanager); err != nil { 196 | return nil, fmt.Errorf("unmarshal alertmanager: %w", err) 197 | } 198 | 199 | var alertmanagerImage string 200 | if alertmanager.Spec.BaseImage != "" { 201 | alertmanagerImage = alertmanager.Spec.BaseImage + ":" + alertmanager.Spec.Version 202 | } else { 203 | alertmanagerImage = *alertmanager.Spec.Image 204 | } 205 | 206 | var images []string 207 | images = append(images, getImagesFromContainers(alertmanager.Spec.Containers)...) 208 | images = append(images, getImagesFromContainers(alertmanager.Spec.InitContainers)...) 209 | images = append(images, alertmanagerImage) 210 | 211 | return images, nil 212 | } 213 | 214 | func getPodImages(resource []byte) ([]string, error) { 215 | var pod corev1.PodTemplateSpec 216 | if err := kubeyaml.Unmarshal(resource, &pod); err != nil { 217 | return nil, fmt.Errorf("unmarshal pod: %w", err) 218 | } 219 | 220 | var images []string 221 | images = append(images, getImagesFromContainers(pod.Spec.Containers)...) 222 | images = append(images, getImagesFromContainers(pod.Spec.InitContainers)...) 223 | 224 | return images, nil 225 | } 226 | 227 | func getCronJobImages(resource []byte) ([]string, error) { 228 | var cj batchv1beta1.CronJob 229 | if err := kubeyaml.Unmarshal(resource, &cj); err != nil { 230 | return nil, fmt.Errorf("unmarshal cronjob: %w", err) 231 | } 232 | 233 | var images []string 234 | images = append(images, getImagesFromContainers(cj.Spec.JobTemplate.Spec.Template.Spec.Containers)...) 235 | images = append(images, getImagesFromContainers(cj.Spec.JobTemplate.Spec.Template.Spec.InitContainers)...) 236 | 237 | return images, nil 238 | } 239 | 240 | func getImagesFromContainers(containers []corev1.Container) []string { 241 | var images []string 242 | 243 | imgStringsToFilter := []string{ 244 | // Ignore token characters. 245 | "$", 246 | 247 | // Ignore image parameters that include a URL. 248 | "http://", 249 | "https://", 250 | 251 | // Ignore Envoy/Istio log parameters 252 | // (https://www.envoyproxy.io/docs/envoy/latest/start/quick-start/run-envoy.html#debugging-envoy) 253 | ":trace", 254 | ":debug", 255 | ":info", 256 | ":warn", 257 | ":error", 258 | ":critical", 259 | ":off", 260 | } 261 | 262 | // Looking for strings like 0.0.0.0:5332 263 | regexToFilter := []string{".*\\d*\\.\\d*:\\d"} 264 | 265 | for _, container := range containers { 266 | images = append(images, container.Image) 267 | 268 | for _, arg := range container.Args { 269 | var image string 270 | if strings.Contains(arg, "=") { 271 | image = strings.Split(arg, "=")[1] 272 | } else { 273 | image = arg 274 | } 275 | 276 | if !strings.Contains(image, ":") || strings.Contains(image, "=:") { 277 | continue 278 | } 279 | 280 | var skiploop bool 281 | for _, imgString := range imgStringsToFilter { 282 | if strings.Contains(image, imgString) { 283 | skiploop = true 284 | } 285 | } 286 | 287 | for _, regexpPattern := range regexToFilter { 288 | found, _ := regexp.MatchString(regexpPattern, image) 289 | 290 | if found { 291 | skiploop = true 292 | } 293 | } 294 | 295 | if skiploop { 296 | continue 297 | } 298 | 299 | registryPath := docker.RegistryPath(image) 300 | if registryPath.Repository() == "" { 301 | continue 302 | } 303 | 304 | if strings.Contains(registryPath.Repository(), ":") { 305 | continue 306 | } 307 | 308 | images = append(images, image) 309 | } 310 | } 311 | 312 | return images 313 | } 314 | 315 | func contains(images []string, image string) bool { 316 | for _, currentImage := range images { 317 | if strings.EqualFold(currentImage, image) { 318 | return true 319 | } 320 | } 321 | 322 | return false 323 | } 324 | -------------------------------------------------------------------------------- /internal/manifest/kubernetes_test.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "testing" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | ) 8 | 9 | func TestGetImagesFromContainers_WithEqualSign(t *testing.T) { 10 | containers := []corev1.Container{ 11 | { 12 | Image: "baseimage:v1", 13 | Args: []string{ 14 | "--arg=argimage:v1", 15 | "--newline", 16 | "newlineimage:v1", 17 | }, 18 | }, 19 | } 20 | 21 | actual := getImagesFromContainers(containers) 22 | 23 | if !contains(actual, "argimage:v1") { 24 | t.Errorf("expected argimage:v1 to exist in list of images but it did not: %v", actual) 25 | } 26 | 27 | if !contains(actual, "newlineimage:v1") { 28 | t.Errorf("expected newlineimage:v1 to exist in list of images but it did not: %v", actual) 29 | } 30 | } 31 | 32 | func TestGetImagesFromContainers_WithURLParameter(t *testing.T) { 33 | containers := []corev1.Container{ 34 | { 35 | Image: "baseimage:v1", 36 | Args: []string{ 37 | "--events-addr=http://service/", 38 | "--events-addr=https://service/", 39 | }, 40 | }, 41 | } 42 | actual := getImagesFromContainers(containers) 43 | 44 | if !contains(actual, "baseimage:v1") { 45 | t.Errorf("expected baseimage:v1 to exist in list of images but it did not: %v", actual) 46 | } 47 | 48 | if contains(actual, "http://service/") { 49 | t.Errorf("Invalid image parsing for args contain http addresses: %v", actual) 50 | } 51 | 52 | if contains(actual, "https://service/") { 53 | t.Errorf("Invalid image parsing for args contain https addresses: %v", actual) 54 | } 55 | 56 | } 57 | 58 | // Envoy/Istio specify their log levels as component:level (See https://www.envoyproxy.io/docs/envoy/latest/start/quick-start/run-envoy.html#debugging-envoy) 59 | func TestGetImagesFromContainers_WithLogComponent(t *testing.T) { 60 | containers := []corev1.Container{ 61 | { 62 | Image: "baseimage:v1", 63 | Args: []string{ 64 | "--proxyComponentLogLevel=misc:error", 65 | "--log_output_level=default:info", 66 | }, 67 | }, 68 | } 69 | actual := getImagesFromContainers(containers) 70 | 71 | if !contains(actual, "baseimage:v1") { 72 | t.Errorf("expected baseimage:v1 to exist in list of images but it did not: %v", actual) 73 | } 74 | 75 | if contains(actual, "misc:error") { 76 | t.Errorf("Invalid image parsing for args contain misc:error parameter: %v", actual) 77 | } 78 | 79 | if contains(actual, "default:info") { 80 | t.Errorf("Invalid image parsing for args contain default:info parameter: %v", actual) 81 | } 82 | 83 | } 84 | 85 | func TestGetImagesFromContainers_WithIPParameter(t *testing.T) { 86 | containers := []corev1.Container{ 87 | { 88 | Image: "baseimage:v1", 89 | Args: []string{ 90 | "--serving-address=0.0.0.0:6443", 91 | }, 92 | }, 93 | } 94 | actual := getImagesFromContainers(containers) 95 | 96 | if !contains(actual, "baseimage:v1") { 97 | t.Errorf("expected baseimage:v1 to exist in list of images but it did not: %v", actual) 98 | } 99 | 100 | if contains(actual, "0.0.0.0:6443") { 101 | t.Errorf("Invalid image parsing for args contain an ip addresses: %v", actual) 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /internal/manifest/manifest.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "bufio" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/plexsystems/sinker/internal/docker" 14 | 15 | "gopkg.in/yaml.v2" 16 | ) 17 | 18 | // Manifest contains all of the sources to push to a target registry. 19 | type Manifest struct { 20 | Target Target `yaml:"target"` 21 | Sources []Source `yaml:"sources,omitempty"` 22 | } 23 | 24 | // Get returns the manifest found at the specified path. 25 | func Get(path string) (Manifest, error) { 26 | manifestLocation := getManifestLocation(path) 27 | manifestContents, err := os.ReadFile(manifestLocation) 28 | if err != nil { 29 | return Manifest{}, fmt.Errorf("reading manifest: %w", err) 30 | } 31 | 32 | var manifest Manifest 33 | if err := yaml.Unmarshal(manifestContents, &manifest); err != nil { 34 | return Manifest{}, fmt.Errorf("unmarshal manifest: %w", err) 35 | } 36 | 37 | // When a source in the manifest does not define its own target the default target 38 | // should be the target defined in the manifest. 39 | for s := range manifest.Sources { 40 | if manifest.Sources[s].Target.Host == "" { 41 | manifest.Sources[s].Target = manifest.Target 42 | } 43 | } 44 | 45 | return manifest, nil 46 | } 47 | 48 | // Write writes the contents of the manifest to disk at the specified path. 49 | func (m Manifest) Write(path string) error { 50 | imageManifestContents, err := yaml.Marshal(&m) 51 | if err != nil { 52 | return fmt.Errorf("marshal image manifest: %w", err) 53 | } 54 | 55 | manifestLocation := getManifestLocation(path) 56 | if err := os.WriteFile(manifestLocation, imageManifestContents, os.ModePerm); err != nil { 57 | return fmt.Errorf("creating file: %w", err) 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func (m Manifest) Update(images []string) Manifest { 64 | var updatedSources []Source 65 | for _, updatedImage := range images { 66 | updatedRegistryPath := docker.RegistryPath(updatedImage) 67 | 68 | updatedSource := Source{ 69 | Tag: updatedRegistryPath.Tag(), 70 | Digest: updatedRegistryPath.Digest(), 71 | } 72 | 73 | foundSource, exists := m.findSourceInManifest(updatedImage) 74 | if !exists { 75 | 76 | // When the source host and the target host are the same, this means that the 77 | // images that were retrieved are target images. Therefore, we must attempt to 78 | // find the source host from the repository of the image. 79 | // 80 | // When the source host and target host are different, we can safely use the 81 | // host found in the image manifest as the source. 82 | updatedSource.Host = updatedRegistryPath.Host() 83 | if updatedRegistryPath.Host() == m.Target.Host { 84 | updatedSource.Host = getSourceHostFromRepository(updatedRegistryPath.Repository()) 85 | } 86 | 87 | updatedRepository := updatedRegistryPath.Repository() 88 | updatedRepository = strings.Replace(updatedRepository, m.Target.Repository, "", 1) 89 | updatedRepository = strings.TrimLeft(updatedRepository, "/") 90 | updatedSource.Repository = updatedRepository 91 | 92 | updatedSources = append(updatedSources, updatedSource) 93 | continue 94 | } 95 | 96 | updatedSource.Repository = foundSource.Repository 97 | updatedSource.Host = foundSource.Host 98 | updatedSource.Auth = foundSource.Auth 99 | 100 | // If the target host (or repository) of the source does not match the manifest 101 | // target host (or repository), it has been modified by the user. 102 | // 103 | // To preserve the current settings, set the manifest host and repository values 104 | // to the ones present in the current manifest. 105 | if foundSource.Target.Host != m.Target.Host || 106 | foundSource.Target.Repository != m.Target.Repository { 107 | updatedSource.Target = foundSource.Target 108 | } 109 | 110 | updatedSources = append(updatedSources, updatedSource) 111 | } 112 | 113 | updatedManifest := Manifest{ 114 | Target: m.Target, 115 | Sources: updatedSources, 116 | } 117 | 118 | return updatedManifest 119 | } 120 | 121 | // Auth is a username and password to authenticate to a registry. 122 | type Auth struct { 123 | Username string `yaml:"username,omitempty"` 124 | Password string `yaml:"password,omitempty"` 125 | } 126 | 127 | // Target is the target registry where the images defined in 128 | // the manifest will be pushed to. 129 | type Target struct { 130 | Host string `yaml:"host,omitempty"` 131 | Repository string `yaml:"repository,omitempty"` 132 | Auth Auth `yaml:"auth,omitempty"` 133 | } 134 | 135 | // EncodedAuth returns the Base64 encoded auth for the target registry. 136 | func (t Target) EncodedAuth() (string, error) { 137 | if t.Auth.Password != "" { 138 | auth, err := getEncodedBasicAuth(os.Getenv(t.Auth.Username), os.Getenv(t.Auth.Password)) 139 | if err != nil { 140 | return "", fmt.Errorf("get encoded auth: %w", err) 141 | } 142 | 143 | return auth, nil 144 | } 145 | 146 | auth, err := docker.GetEncodedAuthForHost(t.Host) 147 | if err != nil { 148 | return "", fmt.Errorf("get encoded auth for host: %w", err) 149 | } 150 | 151 | return auth, nil 152 | } 153 | 154 | // Source is a container image in the manifest. 155 | type Source struct { 156 | Repository string `yaml:"repository"` 157 | Host string `yaml:"host,omitempty"` 158 | Target Target `yaml:"target,omitempty"` 159 | Tag string `yaml:"tag,omitempty"` 160 | Digest string `yaml:"digest,omitempty"` 161 | Auth Auth `yaml:"auth,omitempty"` 162 | } 163 | 164 | // Image returns the source image including its tag or digest. 165 | func (s Source) Image() string { 166 | var source string 167 | if s.Tag != "" { 168 | source = ":" + s.Tag 169 | } else if s.Digest != "" { 170 | source = "@" + s.Digest 171 | } 172 | 173 | if s.Repository != "" { 174 | source = "/" + s.Repository + source 175 | } 176 | 177 | if s.Host != "" { 178 | source = "/" + s.Host + source 179 | } 180 | 181 | source = strings.TrimLeft(source, "/") 182 | 183 | return source 184 | } 185 | 186 | // TargetImage returns the target image including its tag or digest. 187 | func (s Source) TargetImage() string { 188 | var target string 189 | if s.Tag != "" { 190 | target = ":" + s.Tag 191 | } else if s.Digest != "" { 192 | target = strings.ReplaceAll(s.Digest, "sha256:", "") 193 | target = ":" + target 194 | } 195 | 196 | if s.Repository != "" { 197 | if hostSupportsNestedRepositories(s.Target.Host) { 198 | target = "/" + s.Repository + target 199 | } else { 200 | target = "/" + filepath.Base(s.Repository) + target 201 | } 202 | } 203 | 204 | if s.Target.Repository != "" { 205 | target = "/" + s.Target.Repository + target 206 | } 207 | 208 | if s.Target.Host != "" { 209 | target = "/" + s.Target.Host + target 210 | } 211 | 212 | target = strings.TrimLeft(target, "/") 213 | 214 | return target 215 | } 216 | 217 | // EncodedAuth returns the Base64 encoded auth for the source registry. 218 | func (s Source) EncodedAuth() (string, error) { 219 | if s.Auth.Password != "" { 220 | auth, err := getEncodedBasicAuth(os.Getenv(s.Auth.Username), os.Getenv(s.Auth.Password)) 221 | if err != nil { 222 | return "", fmt.Errorf("get encoded auth: %w", err) 223 | } 224 | 225 | return auth, nil 226 | } 227 | 228 | auth, err := docker.GetEncodedAuthForHost(s.Host) 229 | if err != nil { 230 | return "", fmt.Errorf("get encoded auth for host: %w", err) 231 | } 232 | 233 | return auth, nil 234 | } 235 | 236 | // GetSourcesFromImages returns the given images as sources with the specified target. 237 | func GetSourcesFromImages(images []string, target string) []Source { 238 | images = dedupeImages(images) 239 | 240 | targetRegistryPath := docker.RegistryPath(target) 241 | sourceTarget := Target{ 242 | Host: targetRegistryPath.Host(), 243 | Repository: targetRegistryPath.Repository(), 244 | } 245 | 246 | var sources []Source 247 | for _, image := range images { 248 | registryPath := docker.RegistryPath(image) 249 | 250 | source := Source{ 251 | Host: registryPath.Host(), 252 | Target: sourceTarget, 253 | Repository: registryPath.Repository(), 254 | Tag: registryPath.Tag(), 255 | Digest: registryPath.Digest(), 256 | } 257 | 258 | sources = append(sources, source) 259 | } 260 | 261 | return sources 262 | } 263 | 264 | // GetImagesFromStandardInput gets a list of images passed in by standard input. 265 | func GetImagesFromStandardInput() ([]string, error) { 266 | standardInReader := io.NopCloser(bufio.NewReader(os.Stdin)) 267 | byteContents, err := io.ReadAll(standardInReader) 268 | if err != nil { 269 | return nil, fmt.Errorf("read config: %w", err) 270 | } 271 | contents := string(byteContents) 272 | 273 | if strings.Contains(contents, "---") { 274 | images, err := GetImagesFromKubernetesResources([]string{contents}) 275 | if err != nil { 276 | return nil, fmt.Errorf("get images from resources: %w", err) 277 | } 278 | 279 | return images, nil 280 | } 281 | 282 | var images []string 283 | if strings.Contains(contents, " ") { 284 | images = strings.Split(contents, " ") 285 | } else if strings.Contains(contents, "\n") { 286 | images = strings.Split(contents, "\n") 287 | } 288 | 289 | images = dedupeImages(images) 290 | return images, nil 291 | } 292 | 293 | func getSourceHostFromRepository(repository string) string { 294 | repositoryMappings := map[string]string{ 295 | "kubernetes-ingress-controller": "quay.io", 296 | "coreos": "quay.io", 297 | "open-policy-agent": "quay.io", 298 | 299 | "twistlock": "registry.twistlock.com", 300 | 301 | "etcd": "k8s.gcr.io", 302 | "kube-apiserver": "k8s.gcr.io", 303 | "coredns": "k8s.gcr.io", 304 | "kube-proxy": "k8s.gcr.io", 305 | "kube-scheduler": "k8s.gcr.io", 306 | "kube-controller-manager": "k8s.gcr.io", 307 | } 308 | 309 | for repositorySegment, host := range repositoryMappings { 310 | if strings.Contains(repository, repositorySegment) { 311 | return host 312 | } 313 | } 314 | 315 | // An empty host refers to an image that is on Docker Hub. 316 | return "" 317 | } 318 | 319 | func (m Manifest) findSourceInManifest(image string) (Source, bool) { 320 | for _, currentSource := range m.Sources { 321 | imagePath := docker.RegistryPath(image) 322 | sourceImagePath := docker.RegistryPath(currentSource.Image()) 323 | targetImagePath := docker.RegistryPath(currentSource.TargetImage()) 324 | 325 | if imagePath.Host() == sourceImagePath.Host() && imagePath.Repository() == sourceImagePath.Repository() { 326 | return currentSource, true 327 | } 328 | 329 | if imagePath.Host() == targetImagePath.Host() && imagePath.Repository() == targetImagePath.Repository() { 330 | return currentSource, true 331 | } 332 | } 333 | 334 | return Source{}, false 335 | } 336 | 337 | func getManifestLocation(path string) string { 338 | const defaultManifestFileName = ".images.yaml" 339 | 340 | location := path 341 | if !strings.Contains(location, ".yaml") && !strings.Contains(location, ".yml") { 342 | location = filepath.Join(path, defaultManifestFileName) 343 | } 344 | 345 | return location 346 | } 347 | 348 | func getEncodedBasicAuth(username string, password string) (string, error) { 349 | authConfig := Auth{ 350 | Username: username, 351 | Password: password, 352 | } 353 | jsonAuth, err := json.Marshal(authConfig) 354 | if err != nil { 355 | return "", fmt.Errorf("marshal auth: %w", err) 356 | } 357 | 358 | return base64.URLEncoding.EncodeToString(jsonAuth), nil 359 | } 360 | 361 | func hostSupportsNestedRepositories(host string) bool { 362 | // Quay.io 363 | if strings.Contains(host, "quay.io") { 364 | return false 365 | } 366 | 367 | // GHCR.io 368 | if strings.Contains(host, "ghcr.io") { 369 | return false 370 | } 371 | 372 | // Docker Registry (Docker Hub) 373 | // An empty host is assumed to be Docker Hub. 374 | if strings.Contains(host, "docker.io") || host == "" { 375 | return false 376 | } 377 | 378 | return true 379 | } 380 | 381 | func dedupeImages(images []string) []string { 382 | var dedupedImages []string 383 | for _, image := range images { 384 | if image == "" { 385 | continue 386 | } 387 | 388 | if !contains(dedupedImages, image) { 389 | dedupedImages = append(dedupedImages, image) 390 | } 391 | } 392 | 393 | return dedupedImages 394 | } 395 | -------------------------------------------------------------------------------- /internal/manifest/manifest_test.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "encoding/base64" 5 | "os" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestSource_WithoutRepository(t *testing.T) { 11 | source := Source{ 12 | Host: "source.com", 13 | Tag: "v1.0.0", 14 | } 15 | 16 | target := Target{ 17 | Host: "target.com", 18 | } 19 | 20 | targetWithRepository := Target{ 21 | Host: "target.com", 22 | Repository: "bar", 23 | } 24 | 25 | testCases := []struct { 26 | source Source 27 | target Target 28 | expectedSource string 29 | expectedTarget string 30 | }{ 31 | { 32 | source, 33 | target, 34 | "source.com:v1.0.0", 35 | "target.com:v1.0.0", 36 | }, 37 | { 38 | source, 39 | targetWithRepository, 40 | "source.com:v1.0.0", 41 | "target.com/bar:v1.0.0", 42 | }, 43 | } 44 | 45 | for _, testCase := range testCases { 46 | testCase.source.Target = testCase.target 47 | 48 | if testCase.source.Image() != testCase.expectedSource { 49 | t.Errorf("expected source %s, actual %s", testCase.expectedSource, testCase.source.Image()) 50 | } 51 | 52 | if testCase.source.TargetImage() != testCase.expectedTarget { 53 | t.Errorf("expected target %s, actual %s", testCase.expectedTarget, testCase.source.TargetImage()) 54 | } 55 | } 56 | } 57 | 58 | func TestSource_WithRepository(t *testing.T) { 59 | source := Source{ 60 | Host: "source.com", 61 | Repository: "repo", 62 | Tag: "v1.0.0", 63 | } 64 | 65 | target := Target{ 66 | Host: "target.com", 67 | } 68 | 69 | targetWithRepository := Target{ 70 | Host: "target.com", 71 | Repository: "bar", 72 | } 73 | 74 | testCases := []struct { 75 | source Source 76 | target Target 77 | expectedSource string 78 | expectedTarget string 79 | }{ 80 | { 81 | source, 82 | target, 83 | "source.com/repo:v1.0.0", 84 | "target.com/repo:v1.0.0", 85 | }, 86 | { 87 | source, 88 | targetWithRepository, 89 | "source.com/repo:v1.0.0", 90 | "target.com/bar/repo:v1.0.0", 91 | }, 92 | } 93 | 94 | for _, testCase := range testCases { 95 | testCase.source.Target = testCase.target 96 | 97 | if testCase.source.Image() != testCase.expectedSource { 98 | t.Errorf("expected source %s, actual %s", testCase.expectedSource, testCase.source.Image()) 99 | } 100 | 101 | if testCase.source.TargetImage() != testCase.expectedTarget { 102 | t.Errorf("expected target %s, actual %s", testCase.expectedTarget, testCase.source.TargetImage()) 103 | } 104 | } 105 | } 106 | 107 | func TestSource_WithNestedRepository(t *testing.T) { 108 | source := Source{ 109 | Host: "source.com", 110 | Repository: "repo/foo", 111 | Tag: "v1.0.0", 112 | } 113 | 114 | target := Target{ 115 | Host: "target.com", 116 | } 117 | 118 | targetWithRepository := Target{ 119 | Host: "target.com", 120 | Repository: "bar", 121 | } 122 | 123 | testCases := []struct { 124 | source Source 125 | target Target 126 | expectedSource string 127 | expectedTarget string 128 | }{ 129 | { 130 | source, 131 | target, 132 | "source.com/repo/foo:v1.0.0", 133 | "target.com/repo/foo:v1.0.0", 134 | }, 135 | { 136 | source, 137 | targetWithRepository, 138 | "source.com/repo/foo:v1.0.0", 139 | "target.com/bar/repo/foo:v1.0.0", 140 | }, 141 | } 142 | 143 | for _, testCase := range testCases { 144 | testCase.source.Target = testCase.target 145 | 146 | if testCase.source.Image() != testCase.expectedSource { 147 | t.Errorf("expected source %s, actual %s", testCase.expectedSource, testCase.source.Image()) 148 | } 149 | 150 | if testCase.source.TargetImage() != testCase.expectedTarget { 151 | t.Errorf("expected target %s, actual %s", testCase.expectedTarget, testCase.source.TargetImage()) 152 | } 153 | } 154 | } 155 | 156 | func TestSource_Digest(t *testing.T) { 157 | target := Target{ 158 | Host: "target.com", 159 | } 160 | 161 | source := Source{ 162 | Host: "source.com", 163 | Target: target, 164 | Repository: "repo", 165 | Digest: "sha256:123", 166 | } 167 | 168 | const expectedSource = "source.com/repo@sha256:123" 169 | if source.Image() != expectedSource { 170 | t.Errorf("unexpected source %s, actual %s", expectedSource, source.Image()) 171 | } 172 | 173 | const expectedTarget = "target.com/repo:123" 174 | if source.TargetImage() != expectedTarget { 175 | t.Errorf("unexpected target %s, actual %s", expectedTarget, source.TargetImage()) 176 | } 177 | } 178 | 179 | func TestGetSourceHostFromRepository(t *testing.T) { 180 | testCases := []struct { 181 | input string 182 | expectedSourceHost string 183 | }{ 184 | { 185 | input: "coreos", 186 | expectedSourceHost: "quay.io", 187 | }, 188 | { 189 | input: "open-policy-agent", 190 | expectedSourceHost: "quay.io", 191 | }, 192 | { 193 | input: "kubernetes-ingress-controller", 194 | expectedSourceHost: "quay.io", 195 | }, 196 | { 197 | input: "twistlock", 198 | expectedSourceHost: "registry.twistlock.com", 199 | }, 200 | } 201 | 202 | for _, testCase := range testCases { 203 | sourceHost := getSourceHostFromRepository(testCase.input) 204 | 205 | if sourceHost != testCase.expectedSourceHost { 206 | t.Errorf("expected source host to be %s, actual %s", testCase.expectedSourceHost, sourceHost) 207 | } 208 | } 209 | } 210 | 211 | func TestSource_AuthFromEnvironment(t *testing.T) { 212 | auth := Auth{ 213 | Username: "ENV_USER_KEY", 214 | Password: "ENV_PASS_KEY", 215 | } 216 | target := Target{ 217 | Auth: auth, 218 | } 219 | source := Source{ 220 | Target: target, 221 | Auth: auth, 222 | } 223 | 224 | expectedAuthJSON := []byte(`{"Username":"ENV_USER_VALUE","Password":"ENV_PASS_VALUE"}`) 225 | expectedAuth := base64.URLEncoding.EncodeToString(expectedAuthJSON) 226 | 227 | os.Setenv("ENV_USER_KEY", "ENV_USER_VALUE") 228 | os.Setenv("ENV_PASS_KEY", "ENV_PASS_VALUE") 229 | 230 | actualSourceAuth, err := source.EncodedAuth() 231 | if err != nil { 232 | t.Fatal("encoded source auth:", err) 233 | } 234 | if actualSourceAuth != expectedAuth { 235 | t.Errorf("expected source auth %s, actual %s", expectedAuth, actualSourceAuth) 236 | } 237 | 238 | actualTargetAuth, err := source.Target.EncodedAuth() 239 | if err != nil { 240 | t.Fatal("encoded target auth:", err) 241 | } 242 | if actualTargetAuth != expectedAuth { 243 | t.Errorf("expected target auth %s, actual %s", expectedAuth, actualTargetAuth) 244 | } 245 | } 246 | 247 | func TestSource_TargetDoesNotSupportNestedRepositories_SinglePath(t *testing.T) { 248 | target := Target{ 249 | Host: "", 250 | Repository: "targetrepo", 251 | } 252 | 253 | source := Source{ 254 | Host: "source.com", 255 | Target: target, 256 | Repository: "nested/sourcerepo", 257 | Tag: "v1.0.0", 258 | } 259 | 260 | const expectedTarget = "targetrepo/sourcerepo:v1.0.0" 261 | if source.TargetImage() != expectedTarget { 262 | t.Errorf("unexpected target string. expected %s, actual %s", expectedTarget, source.TargetImage()) 263 | } 264 | } 265 | 266 | func TestSource_TargetDoesNotSupportNestedRepositories_MultiplePaths(t *testing.T) { 267 | target := Target{ 268 | Host: "", 269 | Repository: "targetrepo", 270 | } 271 | 272 | source := Source{ 273 | Host: "source.com", 274 | Target: target, 275 | Repository: "really/nested/sourcerepo", 276 | Tag: "v1.0.0", 277 | } 278 | 279 | const expectedTarget = "targetrepo/sourcerepo:v1.0.0" 280 | if source.TargetImage() != expectedTarget { 281 | t.Errorf("unexpected target string. expected %s, actual %s", expectedTarget, source.TargetImage()) 282 | } 283 | } 284 | 285 | func TestManifest_Update(t *testing.T) { 286 | base := Manifest{ 287 | Target: Target{ 288 | Host: "mycr.com", 289 | Repository: "", 290 | }, 291 | } 292 | 293 | testCases := []struct { 294 | desc string 295 | existingManifest Manifest 296 | input []string 297 | expected Manifest 298 | }{ 299 | { 300 | desc: "replaces tags with latest version without repository", 301 | input: []string{"mycr.com/foo/bar:1.2.3"}, 302 | existingManifest: base, 303 | expected: Manifest{ 304 | Target: base.Target, 305 | Sources: []Source{ 306 | { 307 | Repository: "foo/bar", 308 | Tag: "1.2.3", 309 | }, 310 | }, 311 | }, 312 | }, 313 | { 314 | desc: "replaces tags with latest version with repository", 315 | input: []string{"mycr.com/foo/bar:1.2.3"}, 316 | existingManifest: Manifest{ 317 | Target: Target{ 318 | Host: base.Target.Host, 319 | Repository: "foo", 320 | }, 321 | }, 322 | expected: Manifest{ 323 | Target: Target{ 324 | Host: "mycr.com", 325 | Repository: "foo", 326 | }, 327 | Sources: []Source{ 328 | { 329 | Repository: "bar", 330 | Tag: "1.2.3", 331 | }, 332 | }, 333 | }, 334 | }, 335 | { 336 | desc: "preserves source specific host overrides from manifest", 337 | input: []string{"myothercr.com/foo/bar:1.2.3"}, 338 | existingManifest: Manifest{ 339 | Target: base.Target, 340 | Sources: []Source{ 341 | { 342 | Repository: "foo/bar", 343 | Tag: "1.0.0", 344 | Target: Target{ 345 | Host: "myothercr.com", 346 | }, 347 | }, 348 | }, 349 | }, 350 | expected: Manifest{ 351 | Target: base.Target, 352 | Sources: []Source{ 353 | { 354 | Repository: "foo/bar", 355 | Tag: "1.2.3", 356 | Target: Target{ 357 | Host: "myothercr.com", 358 | }, 359 | }, 360 | }, 361 | }, 362 | }, 363 | { 364 | desc: "omits target with matching host and repository", 365 | input: []string{"mycr.com/foo/bar:1.2.3"}, 366 | existingManifest: Manifest{ 367 | Target: Target{ 368 | Host: "mycr.com", 369 | Repository: "foo", 370 | }, 371 | Sources: []Source{ 372 | { 373 | Repository: "bar", 374 | Tag: "1.0.0", 375 | Target: Target{ 376 | Host: "mycr.com", 377 | Repository: "foo", 378 | }, 379 | }, 380 | }, 381 | }, 382 | expected: Manifest{ 383 | Target: Target{ 384 | Host: "mycr.com", 385 | Repository: "foo", 386 | }, 387 | Sources: []Source{ 388 | { 389 | Repository: "bar", 390 | Tag: "1.2.3", 391 | Target: Target{}, 392 | }, 393 | }, 394 | }, 395 | }, 396 | { 397 | desc: "includes target with matching host but different repository", 398 | input: []string{"mycr.com/foo/bar:1.2.3"}, 399 | existingManifest: Manifest{ 400 | Target: Target{ 401 | Host: "mycr.com", 402 | Repository: "", 403 | }, 404 | Sources: []Source{ 405 | { 406 | Repository: "bar", 407 | Tag: "1.0.0", 408 | Target: Target{ 409 | Host: "mycr.com", 410 | Repository: "foo", 411 | }, 412 | }, 413 | }, 414 | }, 415 | expected: Manifest{ 416 | Target: Target{ 417 | Host: "mycr.com", 418 | Repository: "", 419 | }, 420 | Sources: []Source{ 421 | { 422 | Repository: "bar", 423 | Tag: "1.2.3", 424 | Target: Target{ 425 | Host: "mycr.com", 426 | Repository: "foo", 427 | }, 428 | }, 429 | }, 430 | }, 431 | }, 432 | } 433 | 434 | for _, testCase := range testCases { 435 | t.Run(testCase.desc, func(t *testing.T) { 436 | result := testCase.existingManifest.Update(testCase.input) 437 | if !reflect.DeepEqual(result, testCase.expected) { 438 | t.Errorf("expected '%v' got '%v'", testCase.expected, result) 439 | } 440 | }) 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/plexsystems/sinker/internal/commands" 7 | ) 8 | 9 | func main() { 10 | if err := commands.NewDefaultCommand().Execute(); err != nil { 11 | os.Exit(1) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Test 2 | 3 | This folder contains tests for commands that typically require specific configurations to test each command, or need to read from and write to disk. 4 | -------------------------------------------------------------------------------- /test/create/bundle.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: test-create 5 | spec: 6 | containers: 7 | - image: otherhost.com/some/image:v1.0.0 8 | name: test-create 9 | --- 10 | apiVersion: batch/v1beta1 11 | kind: CronJob 12 | metadata: 13 | name: test-cronjob 14 | spec: 15 | schedule: '*/1 * * * *' 16 | jobTemplate: 17 | metadata: 18 | name: test-job 19 | spec: 20 | template: 21 | spec: 22 | containers: 23 | - image: yetanotherhost.com/other/jobimage:234 24 | name: test-job 25 | restartPolicy: OnFailure 26 | -------------------------------------------------------------------------------- /test/create/expected-images.yaml: -------------------------------------------------------------------------------- 1 | target: 2 | host: myhost.com 3 | sources: 4 | - repository: some/image 5 | host: otherhost.com 6 | tag: v1.0.0 7 | - repository: other/jobimage 8 | host: yetanotherhost.com 9 | tag: "234" 10 | -------------------------------------------------------------------------------- /test/list/.images.yaml: -------------------------------------------------------------------------------- 1 | target: 2 | repository: plexsystems 3 | sources: 4 | - repository: nested/sinker-test 5 | host: host.com 6 | tag: v1.0.0 7 | - repository: sinker-test 8 | host: host.com 9 | tag: v2.0.0 10 | -------------------------------------------------------------------------------- /test/list/expected-source.txt: -------------------------------------------------------------------------------- 1 | host.com/nested/sinker-test:v1.0.0 2 | host.com/sinker-test:v2.0.0 3 | -------------------------------------------------------------------------------- /test/list/expected-target.txt: -------------------------------------------------------------------------------- 1 | plexsystems/sinker-test:v1.0.0 2 | plexsystems/sinker-test:v2.0.0 3 | -------------------------------------------------------------------------------- /test/pull/.images.yaml: -------------------------------------------------------------------------------- 1 | target: 2 | repository: plexsystems 3 | sources: 4 | - repository: sinker-test 5 | tag: latest 6 | - repository: sinker-test 7 | tag: 1.0.0 8 | -------------------------------------------------------------------------------- /test/push/.images.yaml: -------------------------------------------------------------------------------- 1 | target: 2 | repository: plexsystems 3 | sources: 4 | - repository: busybox 5 | tag: latest 6 | -------------------------------------------------------------------------------- /test/update/README.md: -------------------------------------------------------------------------------- 1 | # Update Test 2 | 3 | Validates that fields that were previously set by the user (e.g. target, auth), remain intact after an update. 4 | 5 | ## Expected 6 | 7 | After running the `UPDATE` command, the `expected.yaml` should remain unchanged. 8 | The `tag` field for `some/image` should be updated from `v1.0.0` to `v2.0.0` 9 | 10 | See `acceptance.bats` for the test execution 11 | -------------------------------------------------------------------------------- /test/update/bundle.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: test-update 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app.kubernetes.io/component: test-update 10 | template: 11 | metadata: 12 | labels: 13 | app.kubernetes.io/component: test-update 14 | spec: 15 | containers: 16 | - args: 17 | - --test-digest=some/repo@sha256:abc123 18 | image: some/image:v2.0.0 19 | name: test-update 20 | -------------------------------------------------------------------------------- /test/update/expected.yaml: -------------------------------------------------------------------------------- 1 | target: 2 | host: myhost.com 3 | sources: 4 | - repository: some/image 5 | tag: v2.0.0 6 | auth: 7 | username: MY_USER 8 | password: MY_PASS 9 | - repository: some/repo 10 | target: 11 | host: MY_HOST 12 | repository: MY_REPO 13 | digest: sha256:abc123 14 | -------------------------------------------------------------------------------- /test/update/original.yaml: -------------------------------------------------------------------------------- 1 | target: 2 | host: myhost.com 3 | sources: 4 | - repository: some/image 5 | tag: v1.0.0 6 | auth: 7 | username: MY_USER 8 | password: MY_PASS 9 | - repository: some/repo 10 | target: 11 | host: MY_HOST 12 | repository: MY_REPO 13 | digest: sha256:abc123 14 | --------------------------------------------------------------------------------