├── .github ├── ISSUE_TEMPLATE │ └── problem-or-bug.md └── workflows │ ├── release.yml │ └── webhook_target.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README-containers.md ├── README.md ├── assets └── container_registries.png ├── cmd ├── docker-cli-plugin-metadata.go ├── pushrm.go └── root.go ├── go.mod ├── go.sum ├── gon.json ├── main.go ├── provider ├── dockerhub │ └── dockerhub.go ├── harbor2 │ └── harbor2.go ├── provider │ └── provider.go └── quay │ └── quay.go └── util └── util.go /.github/ISSUE_TEMPLATE/problem-or-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: problem or bug 3 | about: I have a problem with docker-pushrm 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the problem or bug** 11 | A clear and concise description of what the problem or bug is, including all steps necessary to reproduce it. 12 | 13 | **docker-pushrm version** 14 | check version with: 15 | - as Docker CLI plugin: `docker --help | grep pushrm` 16 | - as standalone: `docker-pushrm docker-cli-plugin-metadata | grep -i version` 17 | 18 | **Docker CLI version and platform** 19 | check with: `docker version` 20 | if on Linux, what distro and distro version are you running? (example: Fedora 32) 21 | 22 | **if possible: registry server version** 23 | example: *self-hosted `harbor` version 2.0.0* 24 | 25 | **exact command that you're running** 26 | example: `docker pushrm --provider quay quay.io/my-user/my-repo` 27 | 28 | **debug output** 29 | re-run that command with `--debug` flag and paste the output here 30 | (i.e.: `docker pushrm --provider quay quay.io/my-user/my-repo --debug`) 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/tags 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | pre: 10 | name: pre 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: pre1 14 | run: | 15 | env 16 | exit 0 17 | 18 | cr_release: 19 | name: create_release 20 | needs: [pre] #don't create a release too early 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Check out code for changelog creation 24 | uses: actions/checkout@v2 25 | with: 26 | fetch-depth: 0 #otherwise only one commit is fetched 27 | - name: Create Changelog 28 | id: create_changelog 29 | run: | 30 | git fetch --tags 31 | git tag #for debug 32 | git log --oneline #for debug 33 | previousTag=$(git tag --sort=-v:refname | head -2 | tail -1) 34 | echo previous tag: $previousTag #for debug 35 | changelog=$(git log --oneline --pretty="%s" $previousTag..HEAD) 36 | echo changelog1: "$changelog" #for debug 37 | changelog=$(echo "$changelog" | sed 's/^/- /') 38 | echo changelog2: "$changelog" #for debug 39 | echo changes since $previousTag: > ./changelog.md 40 | echo "$changelog" >> ./changelog.md 41 | - name: Create Release 42 | id: create_release 43 | uses: actions/create-release@v1 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # token provided by Actions, no need to set it 46 | with: 47 | tag_name: ${{ github.ref }} 48 | release_name: Release ${{ github.ref }} 49 | draft: true #we'll undraft at the end if the build step was successfull (= all assets uploaded to the release) 50 | prerelease: false 51 | body_path: ./changelog.md 52 | - name: Output Release URL File 53 | run: echo "${{ steps.create_release.outputs.upload_url }}" > release_url.txt 54 | - name: Save Release URL File for publish 55 | uses: actions/upload-artifact@v1 56 | with: 57 | name: release_url 58 | path: ./release_url.txt 59 | - name: Output Release ID File 60 | run: echo "${{ steps.create_release.outputs.id }}" > release_id.txt 61 | - name: Save Release ID File for publish 62 | uses: actions/upload-artifact@v1 63 | with: 64 | name: release_id 65 | path: ./release_id.txt 66 | 67 | build: 68 | needs: [pre, cr_release] 69 | strategy: 70 | matrix: 71 | os: [linux, darwin, windows] 72 | arch: [amd64, 386, arm64, arm] 73 | exclude: 74 | - os: windows 75 | arch: arm64 76 | - os: windows 77 | arch: arm 78 | - os: darwin 79 | arch: arm 80 | - os: darwin 81 | arch: 386 82 | include: 83 | - os: windows 84 | file_extension: '.exe' 85 | - os: windows 86 | ci_image: ubuntu-latest 87 | - os: linux 88 | file_extension: '' 89 | - os: linux 90 | ci_image: ubuntu-latest 91 | - os: darwin 92 | file_extension: '' 93 | - os: darwin 94 | ci_image: macos-latest 95 | - os: darwin 96 | no_upx: true 97 | name: Build 98 | runs-on: ${{ matrix.ci_image }} 99 | env: 100 | GOOS: ${{ matrix.os }} 101 | GOARCH: ${{ matrix.arch }} 102 | steps: 103 | 104 | - name: Set up Go 1.16 105 | uses: actions/setup-go@v2 106 | with: 107 | go-version: 1.16 108 | id: go 109 | 110 | - name: Check out code into the Go module directory 111 | uses: actions/checkout@v2 112 | 113 | - name: Get dependencies 114 | env: 115 | GOOS: ${{ matrix.os }} 116 | GOARCH: ${{ matrix.arch }} 117 | CGO_ENABLED: 0 118 | run: | 119 | go get -v -t -d . 120 | 121 | - name: Build 122 | run: go build -v -a -tags netgo -ldflags='-s -w -extldflags "-static"' . 123 | 124 | - name: compress with upx 125 | if: ${{ matrix.no_upx != true }} 126 | run: sudo apt-get -y update && sudo apt-get -y install upx && upx ./docker-pushrm${{ matrix.file_extension }} 127 | 128 | - name: Import darwin code-signing certificates 129 | if: ${{ matrix.os == 'darwin' }} 130 | uses: Apple-Actions/import-codesign-certs@v1 131 | with: 132 | p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} #dev id cert as b64 133 | p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} #pw for cert 134 | 135 | - name: Sign darwin binary with Gon 136 | if: ${{ matrix.os == 'darwin' }} 137 | env: 138 | AC_USERNAME: ${{ secrets.AC_USERNAME }} #apple id 139 | AC_PASSWORD: ${{ secrets.AC_PASSWORD }} #app specific pw for apple id 140 | run: | 141 | brew tap mitchellh/gon 142 | brew install mitchellh/gon/gon 143 | gon -log-level=debug -log-json ./gon.json 144 | rm -f ./docker-pushrm 145 | unzip docker-pushrm 146 | 147 | - name: Upload artifact 148 | uses: actions/upload-artifact@v1 149 | with: 150 | name: docker-pushrm_${{ matrix.os }}_${{ matrix.arch }}${{ matrix.file_extension }} 151 | path: ./docker-pushrm${{ matrix.file_extension }} 152 | 153 | - name: Load Release URL File from release job 154 | uses: actions/download-artifact@v1 155 | with: 156 | name: release_url 157 | - name: Get Release File Name & Upload URL 158 | id: get_release_info 159 | run: | 160 | value=`cat release_url/release_url.txt` 161 | echo ::set-output name=upload_url::$value 162 | 163 | - name: Upload Release Asset 164 | id: upload-release-asset 165 | uses: actions/upload-release-asset@v1.0.1 166 | env: 167 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 168 | with: 169 | upload_url: ${{ steps.get_release_info.outputs.upload_url }} # references get_release_info step above 170 | asset_name: docker-pushrm_${{ matrix.os }}_${{ matrix.arch }}${{ matrix.file_extension }} 171 | asset_path: ./docker-pushrm${{ matrix.file_extension }} 172 | asset_content_type: application/octet-stream 173 | 174 | cr_darwin_universal_binary: 175 | needs: [build] 176 | runs-on: macos-latest 177 | steps: 178 | 179 | - name: Check out code 180 | uses: actions/checkout@v2 181 | 182 | - name: Download darwin_amd64 artifact 183 | uses: actions/download-artifact@v1 184 | with: 185 | name: docker-pushrm_darwin_amd64 186 | 187 | - name: Download darwin_arm64 artifact 188 | uses: actions/download-artifact@v1 189 | with: 190 | name: docker-pushrm_darwin_arm64 191 | 192 | - name: package darwin universal binary 193 | run: | 194 | lipo -create docker-pushrm_darwin_arm64/docker-pushrm docker-pushrm_darwin_amd64/docker-pushrm -output docker-pushrm 195 | 196 | - name: Import darwin code-signing certificates 197 | uses: Apple-Actions/import-codesign-certs@v1 198 | with: 199 | p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} #Apple Dev ID cert as b64 200 | p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} #pw for cert 201 | 202 | - name: Sign darwin binary with Gon 203 | env: 204 | AC_USERNAME: ${{ secrets.AC_USERNAME }} #apple id 205 | AC_PASSWORD: ${{ secrets.AC_PASSWORD }} #app specific pw for apple id 206 | run: | 207 | brew tap mitchellh/gon 208 | brew install mitchellh/gon/gon 209 | gon -log-level=debug -log-json ./gon.json 210 | rm -f ./docker-pushrm 211 | unzip docker-pushrm 212 | 213 | - name: Upload artifact 214 | uses: actions/upload-artifact@v1 215 | with: 216 | name: docker-pushrm_darwin_universal2 217 | path: ./docker-pushrm 218 | 219 | - name: Load Release URL File from release job 220 | uses: actions/download-artifact@v1 221 | with: 222 | name: release_url 223 | 224 | - name: Get Release File Name & Upload URL 225 | id: get_release_info 226 | run: | 227 | value=`cat release_url/release_url.txt` 228 | echo ::set-output name=upload_url::$value 229 | 230 | - name: Upload Release Asset 231 | id: upload-release-asset 232 | uses: actions/upload-release-asset@v1.0.1 233 | env: 234 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 235 | with: 236 | upload_url: ${{ steps.get_release_info.outputs.upload_url }} # references get_release_info step above 237 | asset_name: docker-pushrm_darwin_universal2 238 | asset_path: ./docker-pushrm 239 | asset_content_type: application/octet-stream 240 | 241 | 242 | build_containers: 243 | #needs: [build] 244 | runs-on: ubuntu-latest 245 | steps: 246 | - name: Check out code 247 | uses: actions/checkout@v2 248 | - name: Split tag string into semantic version parts 249 | id: semver 250 | run: | 251 | git fetch --tags 252 | git tag #for debug 253 | export vcur=$(git tag --sort=-v:refname | head -1 | sed 's/v//1') 254 | export vmajor=$(echo $vcur | cut -d. -f1) 255 | export vminor=$(echo $vcur | cut -d. -f2) 256 | export vpatch=$(echo $vcur | cut -d. -f3) 257 | echo ::set-output name=vcur::$vcur 258 | echo ::set-output name=vmajor::$vmajor 259 | echo ::set-output name=vminor::$vminor 260 | echo ::set-output name=vpatch::$vpatch 261 | echo version: $vcur 262 | echo version major: $vmajor 263 | echo version minor: $vminor 264 | echo version patch: $vpatch 265 | - name: Prepare docker-buildx 266 | run: | 267 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 268 | docker buildx create --name mybuilder 269 | docker buildx ls #for debug 270 | docker buildx inspect --bootstrap 271 | docker buildx use mybuilder 272 | - name: Docker login 273 | env: 274 | DOCKER_USER: chko 275 | DOCKER_PASS: ${{ secrets.DOCKER_PASS }} 276 | run: | 277 | docker login -u "$DOCKER_USER" -p "$DOCKER_PASS" 278 | - name: Build and Push container images with docker-buildx 279 | env: 280 | vcur: ${{ steps.semver.outputs.vcur }} 281 | vmajor: ${{ steps.semver.outputs.vmajor }} 282 | vminor: ${{ steps.semver.outputs.vminor }} 283 | vpatch: ${{ steps.semver.outputs.vpatch }} 284 | run: | 285 | docker buildx inspect #for debug 286 | echo version: $ver - major: $vmajor - minor: $vminor - patch: $vpatch #for debug 287 | destrepo=chko/docker-pushrm 288 | docker buildx build -t "$destrepo:latest" -t "$destrepo:$vcur" -t "$destrepo:$vmajor" --platform linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/arm/v6 --push . 289 | docker buildx stop 290 | - name: Update container repo README 291 | env: 292 | #exact env var names for docker-pushrm 293 | DOCKER_USER: chko 294 | DOCKER_PASS: ${{ secrets.DOCKER_PASS }} 295 | run: | 296 | # download latest docker-pushrm release from github 297 | # (because we keep the release that this workflow creates drafted until the end of the workflow run the version we're downloading is NOT the one we're currently releasing) 298 | export FILENAME=docker-pushrm_linux_amd64 && export DESTDIR=. && DOWNLOAD_URL=$(curl --silent --fail --show-error https://api.github.com/repos/christian-korneck/docker-pushrm/releases/latest | jq -r ".assets | map(select(.name == \"$FILENAME\"))[0].browser_download_url") && curl --silent --fail --show-error -L $DOWNLOAD_URL -o "$DESTDIR/docker-pushrm" && chmod +rx "$DESTDIR/docker-pushrm" 299 | # this automatically uses README-containers.md 300 | ./docker-pushrm --short "Docker Push Readme - Update the docs of your container repo on Dockerhub, Quay, Harbor from a file" chko/docker-pushrm 301 | 302 | undraft_release: 303 | needs: [build, build_containers, cr_darwin_universal_binary] #only undraft the release when assets were uploaded 304 | runs-on: ubuntu-latest 305 | steps: 306 | - name: Load Release ID File from release job 307 | uses: actions/download-artifact@v1 308 | with: 309 | name: release_id 310 | - name: Get Release ID 311 | id: get_release_id_info 312 | run: | 313 | value=`cat release_id/release_id.txt` 314 | echo ::set-output name=release_id::$value 315 | - name: Undraft release 316 | id: undraft_release 317 | env: 318 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 319 | RELEASE_ID: ${{ steps.get_release_id_info.outputs.release_id }} # references other step 320 | run: | 321 | curl --verbose --fail --show-error --location --request PATCH "https://api.github.com/repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID" --header "Authorization: token $GITHUB_TOKEN" --header 'Content-Type: application/json' --header 'Accept: application/vnd.github.everest-preview+json' --data-raw '{"draft": false}' 322 | 323 | cleanup: 324 | needs: [build, cr_darwin_universal_binary] #currently this can start before build_containers is finished 325 | if: always() 326 | runs-on: ubuntu-latest 327 | steps: 328 | - name: call webhook to delete artifacts 329 | env: 330 | FOR_WEBHOOKS_SECRET: ${{ secrets.FOR_WEBHOOKS_SECRET }} 331 | run: | 332 | echo "::add-mask::$FOR_WEBHOOKS_SECRET" 333 | curl --verbose --fail --show-error --location --request POST "https://api.github.com/repos/$GITHUB_REPOSITORY/dispatches" --header "Authorization: token $FOR_WEBHOOKS_SECRET" --header 'Content-Type: application/json' --header 'Accept: application/vnd.github.everest-preview+json' --data-raw "{ \"event_type\": \"delete_all_artifacts\", \"client_payload\": {\"parent_runid\": \"$GITHUB_RUN_ID\", \"parent_repo\": \"$GITHUB_REPOSITORY\"} }" 334 | -------------------------------------------------------------------------------- /.github/workflows/webhook_target.yml: -------------------------------------------------------------------------------- 1 | name: delete calling job's artifacts 2 | on: repository_dispatch 3 | jobs: 4 | main: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Delete artifacts 8 | if: github.event.action == 'delete_all_artifacts' 9 | uses: christian-korneck/delete-run-artifacts-action@v1 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | with: 13 | parent_runid: ${{ github.event.client_payload.parent_runid }} 14 | parent_repo: ${{ github.event.client_payload.parent_repo }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | /docker-pushrm 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16-alpine AS builder 2 | WORKDIR /go/src/github.com/christian-korneck/docker-pushrm 3 | COPY . . 4 | RUN apk add --no-cache ca-certificates && update-ca-certificates 5 | ENV CGO_ENABLED 0 6 | RUN go build -v -a -tags netgo -ldflags='-s -w -extldflags "-static"' . 7 | #RUN apk add --no-cache upx && upx ./docker-pushrm 8 | 9 | FROM docker.io/library/busybox 10 | COPY --from=builder /go/src/github.com/christian-korneck/docker-pushrm/docker-pushrm / 11 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 12 | 13 | #nobody:nogroup 14 | USER 65534:65534 15 | ENTRYPOINT ["/docker-pushrm"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Christian Korneck 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 | -------------------------------------------------------------------------------- /README-containers.md: -------------------------------------------------------------------------------- 1 | # Docker Push Readme 2 | 3 | This is a container image of [`docker-pushrm`](https://github.com/christian-korneck/docker-pushrm), a tool that lets you update the README of your container repo on Dockerhub, Quay or Harbor from a local markdown file. 4 | 5 | ![hello moon](https://raw.githubusercontent.com/christian-korneck/docker-pushrm/master/assets/container_registries.png) 6 | 7 | ## About this tool 8 | 9 | Check the [full docs](https://github.com/christian-korneck/docker-pushrm/blob/master/README.md) for an introduction. 10 | 11 | ## How to use this container image 12 | 13 | ### Examples 14 | 15 | #### Push a README file to Dockerhub 16 | 17 | ``` 18 | $ ls 19 | README.md 20 | 21 | $ docker run --rm -t \ 22 | -v $(pwd):/myvol \ 23 | -e DOCKER_USER='my-user' -e DOCKER_PASS='my-pass' \ 24 | chko/docker-pushrm:1 --file /myvol/README.md \ 25 | --short "My short description" --debug my-user/my-repo 26 | 27 | ... 28 | DEBU content validation successfull, readme successfully pushed to repo server 29 | ``` 30 | 31 | Let's dissect this a bit: 32 | 33 | - we pin to the major version (`v1`) of this image (`chko/docker-pushrm:v1`). Recommended for most use cases. 34 | - the README file is in the current working directory on the host. We map this dir as volume to the container (mounted at `/myvol`) 35 | - we tell *docker-pushrm* where to find the README file with `--file ` 36 | - our destination repo on Dockerhub is `my-user/my-repo` 37 | - we pass the credentials for the repo as environment variables to the container (`DOCKER_USER` and `DOCKER_PASS`) 38 | - we set `--debug` to get additional log output (optional) 39 | - we set the short description for the Dockerhub repo with `--short ` (optional) 40 | 41 | **Alternatively all params can also get set with environment variables:** 42 | 43 | ``` 44 | $ docker run --rm -t \ 45 | -v $(pwd):/myvol \ 46 | -e DOCKER_USER='my-user' -e DOCKER_PASS='my-pass' \ 47 | -e PUSHRM_PROVIDER=dockerhub -e PUSHRM_FILE=/myvol/README.md \ 48 | -e PUSHRM_SHORT='my short description' \ 49 | -e PUSHRM_TARGET=docker.io/my-user/my-repo -e PUSHRM_DEBUG=1 \ 50 | chko/docker-pushrm:1 51 | ``` 52 | 53 | #### Push a README file to a Harbor v2 registry server 54 | 55 | Use the `--provider harbor2` flag: 56 | 57 | ``` 58 | $ ls 59 | README.md 60 | 61 | $ docker run --rm -t \ 62 | -v $(pwd):/myvol \ 63 | -e DOCKER_USER='my-user' -e DOCKER_PASS='my-pass' \ 64 | chko/docker-pushrm:1 --file /myvol/README.md \ 65 | --provider harbor2 --debug demo.goharbor.io/my-project/my-repo 66 | ``` 67 | 68 | #### Push a README file to Quay.io or a Quay registry server 69 | 70 | 71 | - use the `--provider quay` flag 72 | - use env var `APIKEY___` or `DOCKER_APIKEY` for apikey credentials 73 | 74 | ``` 75 | $ ls 76 | README.md 77 | 78 | $ docker run --rm -t \ 79 | -v $(pwd):/myvol \ 80 | -e APIKEY__QUAY_IO='my-apikey' \ 81 | chko/docker-pushrm:1 --file /myvol/README.md \ 82 | --provider quay --debug quay.io/my-user/my-repo 83 | ``` 84 | 85 | ### env vars 86 | 87 | | env var | example value | description 88 | | --------------------------- | ------------------------------ | ---------------------------------------- 89 | | `DOCKER_USER` | `my-user` | login username 90 | | `DOCKER_PASS` | `my-password` | login password 91 | | `DOCKER_APIKEY` | `my-quay-api-key` | quay api key 92 | | `APIKEY___` | `my-quay-api-key` | quay api key (alternative) 93 | | `PUSHRM_PROVIDER` | `dockerhub`, `quay`, `harbor2` | repo provider type 94 | | `PUSHRM_SHORT` | `my short description` | set/update repo short description 95 | | `PUSHRM_FILE` | `/myvol/README.md` | path to the README file 96 | | `PUSHRM_DEBUG` | `1` | enable verbose output 97 | | `PUSHRM_CONFIG` | `/myvol/.docker/config.json` | Docker config file (for credentials) 98 | | `PUSHRM_TARGET` | `docker.io/my-user/my-repo` | container repo ref 99 | 100 | Presedence: 101 | - Params specified with flags take precedence over env vars. 102 | - Login env vars take precedence over credentials from a Docker config file 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Push Readme 2 | 3 | Update the README of your container repo on Dockerhub, Quay or Harbor with a simple Docker command: 4 | 5 | ``` 6 | $ ls 7 | README.md 8 | $ docker pushrm my-user/hello-world 9 | ``` 10 | 11 | ![hello moon](assets/container_registries.png) 12 | 13 | ## About 14 | 15 | `docker-pushrm` is a Docker CLI plugin that adds a new `docker pushrm` (speak: *"push readme"*) command to Docker. 16 | 17 | It pushes the README file from the current working directory to a container registry server where it appears as repo description in the webinterface. 18 | 19 | It currently supports **[Dockerhub](https://hub.docker.com)** (cloud), **Red Hat Quay** ([cloud](https://quay.io) and [self-hosted](https://www.openshift.com/products/quay)/OpenShift) and **[Harbor v2](https://goharbor.io)** (self-hosted). 20 | 21 | For most registry types `docker-pushrm` uses authentication info from the Docker credentials store - so it "just works" for registry servers that you're already logged into with Docker. 22 | 23 | (For some other registry types, you'll need to pass an API key via env var or config file). 24 | 25 | ## Usage example 26 | 27 | Let's build a container image, push it to Dockerhub and then also push the README to Dockerhub: 28 | 29 | ``` 30 | $ ls 31 | Dockerfile README.md 32 | $ docker login 33 | Username: my-user 34 | Password: ******** 35 | Login Succeeded 36 | $ docker build -t my-user/hello-world . 37 | $ docker push my-user/hello-world 38 | $ docker pushrm my-user/hello-world 39 | ``` 40 | 41 | When we now browse to the repo in the Dockerhub webinterface we should find the repo's README to be updated with the contents of the local README file. 42 | 43 | The same works for Harbor version 2 registry servers: 44 | 45 | ``` 46 | docker pushrm --provider harbor2 demo.goharbor.io/myproject/hello-world 47 | ``` 48 | 49 | And also for Quay/OpenShift cloud and self-hosted registry servers: 50 | ``` 51 | docker pushrm --provider quay quay.io/my-user/hello-world 52 | ``` 53 | 54 | For Dockerhub it's also possible to set the repo's short description with `-s "some description"`. 55 | 56 | In case that you want different content to appear in the README on the container registry than on the git repo (for github/gitlab), you can create a dedicated `README-containers.md`, which takes precedence. It's also possible to specify a path to a README file with `--file `. 57 | 58 | ## Installation 59 | 60 | - make sure Docker or Docker Desktop is installed 61 | - Download `docker-pushrm` for your platform from the [release page](https://github.com/christian-korneck/docker-pushrm/releases/latest). 62 | - copy it to: 63 | - Windows: `c:\Users\\.docker\cli-plugins\docker-pushrm.exe` 64 | - Mac + Linux: `$HOME/.docker/cli-plugins/docker-pushrm` 65 | - on Mac/Linux make it executable: `chmod +x $HOME/.docker/cli-plugins/docker-pushrm` 66 | 67 | Now you should be able to run `docker pushrm --help`. 68 | 69 | ## Running `docker-pushrm` as a container 70 | 71 | There's also a Docker/OCI [container image](https://hub.docker.com/r/chko/docker-pushrm) of this tool. See [separate docs](README-containers.md) for how to use it. This is mainly intended for use in CI workflows. 72 | 73 | ## Use with github actions 74 | 75 | This tool is also available as a github action [here](https://github.com/marketplace/actions/update-container-description-action). 76 | 77 | ## Use with GitLab CI/CD 78 | 79 | Here's an example for a `.gitlab-ci.yml` that uses the `docker-pushrm` container image. (`DOCKER_USER` and `DOCKER_PASS` need to be set as project or group variables): 80 | 81 | ``` 82 | stages: 83 | - release 84 | 85 | pushrm: 86 | stage: release 87 | image: 88 | name: chko/docker-pushrm 89 | entrypoint: ["/bin/sh", "-c", "/docker-pushrm"] 90 | variables: 91 | DOCKER_USER: $DOCKER_USER 92 | DOCKER_PASS: $DOCKER_PASS 93 | PUSHRM_SHORT: My short description 94 | PUSHRM_TARGET: docker.io/$DOCKER_USER/my-repo 95 | PUSHRM_DEBUG: 1 96 | PUSHRM_FILE: $CI_PROJECT_DIR/README.md 97 | script: "/bin/true" 98 | ``` 99 | 100 | (Note: The above `entrypoint`/`script` setup is a workaround for a [GitLab limitation](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/26501). For the same reason the `docker-pushrm` container images include [busybox](https://hub.docker.com/_/busybox)). 101 | 102 | ## How to log in to container registries 103 | 104 | ### Log in to Dockerhub registry 105 | 106 | ``` 107 | docker login 108 | ``` 109 | 110 | Both password and Personal Access Token (PAT) should work. When using a PAT, make sure it has sufficient privileges (`admin` scope). 111 | 112 | ### Log in to Harbor v2 registry 113 | 114 | In the Harbor webinterface, create a `Robot Account` for your project with (at least) the privilege `repository`: `update` [[screenshot](https://github.com/christian-korneck/docker-pushrm/issues/10#issuecomment-2159212629)] and use the displayed username and password. 115 | 116 | (Login with a regular Harbor user account instead is possible too, but [won't work](https://github.com/christian-korneck/docker-pushrm/issues/10) if the Harbor instance is using OIDC auth. Using a robot account is strongly recommended). 117 | 118 | 119 | ``` 120 | docker login 121 | ``` 122 | 123 | Example: 124 | ``` 125 | docker login demo.goharbor.io 126 | ``` 127 | 128 | ### Log in to Quay registry 129 | 130 | If you want to be able to push containers, you need to log in as usual: 131 | 132 | - for Quay cloud: `docker login quay.io` 133 | - for self-hosted Quay server or OpenShift: `docker login ` (example: `docker login my-server.com`) 134 | 135 | In addition to be able to use `docker-pushrm` you need to set up an API key: 136 | 137 | First, log into the Quay webinterface and create an API key: 138 | - if you don't have an organization create a new organization (your repos don't need to be under the organization's namespace, this is just to unlock the "apps" settings page) 139 | - navigate to the org and open the `applications` tab 140 | - `create new app` and give it some name 141 | - click on the app name and open to the `generate token` tab 142 | - create a token with permissions `Read/Write to any accessible repositories` 143 | - after confirming you should now see the token secret. Write it down in a safe place. 144 | 145 | (Refer to the Quay docs for more info) 146 | 147 | Then, make the API key available to `docker-pushrm`. There are two options for that: Either set an environment variable (recommended for CI) or add it to the Docker config file (recommended for Desktop use). (If both are present, the env var takes precedence). 148 | 149 | #### env var for Quay API key 150 | set an environment variable `DOCKER_APIKEY=` or `APIKEY___=` 151 | 152 | example for servername `quay.io`: 153 | ``` 154 | export APIKEY__QUAY_IO=my-api-key 155 | docker pushrm quay.io/my-user/my-repo 156 | ``` 157 | 158 | #### configure Quay API key in Docker config file 159 | 160 | In the Docker config file (default: `$HOME/.docker/config.json`) add a json key `plugins.docker-pushrm.apikey_` with the api key as string value. 161 | 162 | Example for servername `quay.io`: 163 | 164 | ``` 165 | { 166 | 167 | ..., 168 | 169 | 170 | "plugins" : { 171 | "docker-pushrm" : { 172 | "apikey_quay.io" : "my-api-key" 173 | } 174 | }, 175 | 176 | ... 177 | } 178 | ``` 179 | 180 | ## Log in with environment variables (for CI) 181 | 182 | Alternatively credentials can be set as environment variables. Environment variables take precedence over the Docker credentials store. Environment variables can be specified with or without a server name. The variant without a server name takes precedence. 183 | 184 | This is intended for running `docker-pushrm` as a standalone tool in a CI environment (no full Docker installation needed). 185 | 186 | - `DOCKER_USER` and `DOCKER_PASS` 187 | - `DOCKER_USER___` and `DOCKER_PASS___` 188 | (example for server `docker.io`: `DOCKER_USER__DOCKER_IO=my-user` and `DOCKER_PASS__DOCKER_IO=my-password`) 189 | 190 | The provider 'quay' needs an additional env var for the API key in form of `APIKEY___=`. 191 | 192 | Example: 193 | 194 | ``` 195 | DOCKER_USER=my-user DOCKER_PASS=mypass docker-pushrm my-user/my-repo 196 | ``` 197 | 198 | 199 | ## What if I use [podman, img, k3c, buildah, ...] instead of Docker? 200 | 201 | You can still use `docker-pushrm` as standalone executable. 202 | 203 | The only obstacle is that you need to provide it credentials in the Docker style. 204 | 205 | The easiest way for that is to set up a minimal Docker config file with the registry server logins that you need. (Alternatively credentials can be passed [in environment variables](#log-in-with-environment-variables-for-ci) ) 206 | 207 | You can either create this config file on a computer with Docker installed (by running `docker login` and then copying the `$HOME/.docker/config.json` file). 208 | 209 | Or alternatively you can also set it up manually. Here's an example: 210 | 211 | ``` 212 | { 213 | "auths": { 214 | "https://index.docker.io/v1/": { 215 | "auth": "xxx" 216 | }, 217 | "https://demo.goharbor.io": { 218 | "auth": "xxx" 219 | } 220 | 221 | }, 222 | } 223 | ``` 224 | The auth value is base64 of `:` (i.e. `myuser:mypasswd`) 225 | 226 | It's also possible to use Docker [credential helpers](https://docs.docker.com/engine/reference/commandline/login/#credential-helpers) on systems that don't have Docker installed to avoid clear text passwords in the config file. The credential helper needs to be configured in the Docker config file and the credential helper executable needs to be in the `PATH`. (Check the Docker docs for details). 227 | 228 | ## Can you add support for registry [XY...]? 229 | 230 | Please open an issue. 231 | 232 | ## Installation for all users 233 | 234 | To install the plugin for all users of a system copy it to the following path (instead of to the user home dir). Requires admin/root privs. 235 | 236 | - Linux: depending on the distro, either `/usr/lib/docker/cli-plugins/docker-pushrm` or `/usr/libexec/docker/cli-plugins/docker-pushrm` 237 | - Windows: `%ProgramData%\Docker\cli-plugins\docker-pushrm.exe` 238 | - Mac: `/Applications/Docker.app/Contents/Resources/cli-plugins/docker-pushrm` 239 | 240 | On Mac/Linux make it executable and readable for all users: `chmod a+rx /docker-pushrm` 241 | 242 | ## Using env vars instead of cmdline params 243 | 244 | All cmdline parameters can also be set as env vars with prefix `PUSHRM_`. 245 | 246 | Cmdline parameters take precedence over env vars. (Except for login env vars, which take precedence over the local credentials store). 247 | 248 | This is mainly intended for running this tool in a container in [12fa](https://12factor.net/config) style. 249 | 250 | A list of all supported env vars is [here](README-containers.md#env-vars). 251 | 252 | ## Limitations 253 | 254 | (currently none) 255 | 256 | ---- 257 | All trademarks, logos and website designs belong to their respective owners. 258 | -------------------------------------------------------------------------------- /assets/container_registries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christian-korneck/docker-pushrm/084eaf042deec6846c805c99a4deaaf89c604e99/assets/container_registries.png -------------------------------------------------------------------------------- /cmd/docker-cli-plugin-metadata.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Christian Korneck 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "encoding/json" 27 | "os" 28 | 29 | "github.com/spf13/cobra" 30 | ) 31 | 32 | // dockerCliPluginMetadataCmd represents the dockerCliPluginMetadata command 33 | var dockerCliPluginMetadataCmd = &cobra.Command{ 34 | Use: "docker-cli-plugin-metadata", 35 | Short: "provides plugin metadata to the docker cli", 36 | Long: `provides plugin metadata to the docker cli 37 | 38 | `, 39 | Run: func(cmd *cobra.Command, args []string) { 40 | enc := json.NewEncoder(os.Stdout) 41 | enc.SetIndent("", " ") 42 | // specs: https://docs.docker.com/engine/extend/cli_plugins/#the-docker-cli-plugin-metadata-subcommand 43 | d := map[string]string{"SchemaVersion": "0.1.0", "Vendor": "Christian Korneck", "Version": "1.9.0", "ShortDescription": "Push Readme to container registry"} 44 | enc.Encode(d) 45 | }, 46 | } 47 | 48 | func init() { 49 | rootCmd.AddCommand(dockerCliPluginMetadataCmd) 50 | dockerCliPluginMetadataCmd.Hidden = true 51 | 52 | // Here you will define your flags and configuration settings. 53 | 54 | // Cobra supports Persistent Flags which will work for this command 55 | // and all subcommands, e.g.: 56 | // dockerCliPluginMetadataCmd.PersistentFlags().String("foo", "", "A help for foo") 57 | 58 | // Cobra supports local flags which will only run when this command 59 | // is called directly, e.g.: 60 | // dockerCliPluginMetadataCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 61 | } 62 | -------------------------------------------------------------------------------- /cmd/pushrm.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Christian Korneck 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "errors" 27 | "os" 28 | "regexp" 29 | "strings" 30 | "unicode/utf8" 31 | 32 | "github.com/christian-korneck/docker-pushrm/provider/dockerhub" 33 | "github.com/christian-korneck/docker-pushrm/provider/harbor2" 34 | "github.com/christian-korneck/docker-pushrm/provider/provider" 35 | "github.com/christian-korneck/docker-pushrm/provider/quay" 36 | "github.com/christian-korneck/docker-pushrm/util" 37 | log "github.com/sirupsen/logrus" 38 | "github.com/spf13/cobra" 39 | "github.com/spf13/viper" 40 | ) 41 | 42 | var providername string 43 | var rfile string 44 | var shortdesc string 45 | 46 | // pushrmCmd represents the pushrm command 47 | var pushrmCmd = &cobra.Command{ 48 | Use: " NAME[:TAG]", 49 | Aliases: []string{"pushrm"}, 50 | Args: cobra.MaximumNArgs(1), 51 | Short: "push README file from current working directory to container registry (Dockerhub, quay, harbor2)", 52 | Long: `help for docker pushrm 53 | 54 | docker pushrm NAME[:TAG] [flags] 55 | 56 | pushes the README.md file from the current working 57 | directory to the container registry (Dockerhub, quay, harbor2) 58 | where it appears as repo description. 59 | 60 | 61 | 62 | Usage Examples: 63 | =============== 64 | 65 | 66 | Dockerhub (hub.docker.com cloud) 67 | -------------------------------- 68 | docker pushrm myaccount/hello-world 69 | docker pushrm docker.io/my-account/hello-world 70 | 71 | 72 | Quay (quay.io cloud or self-hosted) 73 | ----------------------------------- 74 | docker pushrm quay.io/my-organization/hello-world 75 | docker pushrm --provider quay my-quay-server.com/my-user/hello-world 76 | 77 | 78 | Harbor (self-hosted) 79 | -------------------- 80 | docker pushrm --provider harbor2 my-harbor-server.com/my-project/hello-world 81 | 82 | 83 | 84 | How to login 85 | ============= 86 | 87 | 88 | For most registry providers docker-pushrm uses 89 | the registry login from Docker's credentials store. 90 | For some providers an API key needs to be specified 91 | via env var or Docker config file. 92 | 93 | Alternatively credentials can be set as environment 94 | variables. Environment variables take precedence over 95 | the Docker credentials store. 96 | 97 | Environment variables can be specified with or without 98 | a server name. The variant without a server name takes 99 | precedence: 100 | 101 | - DOCKER_USER and DOCKER_PASS 102 | - DOCKER_USER___ and DOCKER_PASS___ 103 | (example for server 'docker.io': DOCKER_USER__DOCKER_IO=my-user 104 | and DOCKER_PASS__DOCKER_IO=my-password) 105 | 106 | The provider 'quay' needs an additional env var for the API key 107 | in form of APIKEY___=. 108 | 109 | 110 | Dockerhub 111 | --------- 112 | run 'docker login' 113 | 114 | (use password or Personal Access Token (PAT) with 'admin' scope) 115 | 116 | 117 | quay 118 | ---- 119 | - get an api key (bearer token) from the quay webinterface 120 | 121 | - option 1: env var DOCKER_APIKEY= 122 | or env var APIKEY___= 123 | (example for quay.io: 'export APIKEY__QUAY_IO=myapikey') 124 | 125 | - option 2: in the Docker config file (default: "$HOME/.docker/config.json") 126 | add the json key 'plugins.docker-pushrm.apikey_' with the 127 | apikey as string value (example for quay.io: key name 128 | 'plugins.docker-pushrm.apikey_quay.io') 129 | 130 | Env var takes precedence. 131 | 132 | 133 | harbor 134 | ------ 135 | run 'docker login ' (example: 'docker login demo.goharbor.io') 136 | 137 | 138 | 139 | Creating a README file 140 | ======================= 141 | 142 | Valid names: 'README.md' or 'README-containers.md'. 143 | 144 | 'README-containers.md' takes precedence. This allows to set up a 145 | different README file for the container registry then for the git repo. 146 | 147 | The README file needs to be in the current working directory from which 148 | docker pushrm is being called. 149 | 150 | It's also possible to specify a path to a README file with 151 | '--file ' ('-f '). 152 | 153 | 154 | Optional [:TAG] argument 155 | ======================== 156 | 157 | The [:TAG] argument is optional and has no effect for the currently 158 | supported providers (which only support a README per repo, not per 159 | tag). It is in place in case that additional providers get added in 160 | the future that support READMEs on the tag level. 161 | 162 | 163 | Supported environment variables 164 | =============================== 165 | 166 | DOCKER_USER, DOCKER_PASS, DOCKER_APIKEY, APIKEY___, 167 | PUSHRM_PROVIDER, PUSHRM_SHORT, PUSHRM_FILE, PUSHRM_DEBUG, PUSHRM_CONFIG, 168 | PUSHRM_TARGET 169 | 170 | Commandline parameters take precedence over environment variables. 171 | Login environment variables take precedence over the local credentials 172 | store. 173 | 174 | 175 | 176 | `, 177 | RunE: func(cmd *cobra.Command, args []string) error { 178 | if err := run(args); err != nil { 179 | return err 180 | } 181 | return nil 182 | }, 183 | } 184 | 185 | func run(args []string) error { 186 | pushrmProvider := viper.GetString("provider") 187 | pushrmFile := viper.GetString("file") 188 | pushrmShortDesc := viper.GetString("short") 189 | 190 | log.Debug("subcommand \"pushrm\" called") 191 | 192 | //fmt.Println(os.Getenv("DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND")) 193 | 194 | // lowest common ground: 100 runes (not bytes) on Dockerhub 195 | // this check is intentially global (not per provider) to 196 | // make cmd calls portable between providers without surprises 197 | if utf8.RuneCountInString(pushrmShortDesc) > 100 { 198 | log.Error("Short description is too long (max 100 characters)") 199 | os.Exit(1) 200 | } 201 | 202 | // our only positional argument: //: (servername + tag are optional) 203 | targetinfo := os.Getenv("PUSHRM_TARGET") 204 | if len(args) > 0 { 205 | if target := args[0]; target != "" { 206 | targetinfo = target 207 | } 208 | } 209 | 210 | if targetinfo == "" { 211 | return (errors.New("Missing [IMAGE] argument. Example: docker.io/mynamespace/myrepo:latest")) 212 | //log.Error("Missing [IMAGE] argument. Example: docker.io/mynamespace/myrepo:latest") 213 | //os.Exit(1) 214 | } 215 | 216 | // fail if namespacename is missing 217 | if len(strings.Split(targetinfo, "/")) < 2 { 218 | log.Error("Invalid [IMAGE] argument - missing namespace. Example: docker.io/mynamespace/myrepo:latest") 219 | os.Exit(1) 220 | } 221 | // fill up default servername, if missing 222 | if len(strings.Split(targetinfo, "/")) < 3 { 223 | targetinfo = "docker.io/" + targetinfo 224 | } 225 | // fill up default tagname, if missing 226 | if strings.Contains(targetinfo, ":") != true { 227 | targetinfo = targetinfo + ":latest" 228 | } 229 | log.Debug("Using target: ", targetinfo) 230 | 231 | var erro error 232 | 233 | if pushrmFile == "" { 234 | pushrmFile, erro = util.FindReadmeFile() 235 | if erro != nil { 236 | log.Error(erro) 237 | os.Exit(1) 238 | } 239 | } 240 | 241 | log.Debug("using README file: " + pushrmFile) 242 | 243 | readme, erro := util.ReadFile(pushrmFile) 244 | if erro != nil { 245 | log.Error(erro) 246 | os.Exit(1) 247 | } 248 | 249 | if (len(strings.Split(targetinfo, "/")) != 3) || (len(strings.Split(strings.Split(targetinfo, "/")[2], ":")) != 2) { 250 | log.Error("Invalid [IMAGE] argument - too many separators. Example: docker.io/mynamespace/myrepo:latest") 251 | os.Exit(1) 252 | } 253 | 254 | servername := strings.ToLower(strings.Split(targetinfo, "/")[0]) 255 | namespacename := strings.Split(targetinfo, "/")[1] 256 | reponame := strings.Split(strings.Split(targetinfo, "/")[2], ":")[0] 257 | tagname := strings.Split(strings.Split(targetinfo, "/")[2], ":")[1] 258 | if servername == "docker.io" { 259 | pushrmProvider = "dockerhub" 260 | } 261 | if servername == "quay.io" { 262 | pushrmProvider = "quay" 263 | } 264 | log.Debug("server: ", servername) 265 | log.Debug("namespace: ", namespacename) 266 | log.Debug("repo: ", reponame) 267 | log.Debug("tag: ", tagname) 268 | log.Debug("repo provider: ", pushrmProvider) 269 | 270 | for _, e := range []string{namespacename, reponame, tagname, servername} { 271 | // yes, dots are allowed in all these fields 272 | if regexp.MustCompile(`^[0-9a-zA-Z\-_.]+$`).MatchString(e) == false { 273 | log.Error("Invalid [IMAGE argument] - bad characters or empty value. Example: docker.io/mynamespace/myrepo:latest") 274 | os.Exit(1) 275 | } 276 | } 277 | 278 | if pushrmProvider == "dockerhub" && servername != "docker.io" { 279 | log.Error("servername ", servername, " is not valid for provider ", pushrmProvider, " (try \"docker.io\")") 280 | os.Exit(1) 281 | } 282 | 283 | var prov provider.Provider 284 | 285 | switch pushrmProvider { 286 | case "dockerhub": 287 | prov = dockerhub.Dockerhub{} 288 | case "quay": 289 | prov = quay.Quay{} 290 | case "harbor2": 291 | prov = harbor2.Harbor2{} 292 | default: 293 | log.Error("unsupported repo provider: ", pushrmProvider+". See \"--help\" for supported providers. ") 294 | os.Exit(1) 295 | } 296 | 297 | authident := prov.GetAuthident() 298 | var authidentIsFuzzy bool 299 | authidentIsFuzzy = false 300 | 301 | if authident == "__SERVERNAME__" { 302 | authident = servername 303 | authidentIsFuzzy = true 304 | } 305 | 306 | var dockerUser string 307 | var dockerPasswd string 308 | var err error 309 | 310 | // generic env var (no servername specified) takes precedence 311 | dockerUser = os.Getenv("DOCKER_USER") 312 | dockerPasswd = os.Getenv("DOCKER_PASS") 313 | if dockerUser != "" && dockerPasswd != "" { 314 | log.Debug("using credentials for user " + dockerUser + " from generic env var") 315 | } 316 | 317 | // env var with servername is next 318 | if dockerUser == "" || dockerPasswd == "" { 319 | suffix := strings.ToUpper(strings.Replace(servername, ".", "_", -1)) 320 | dockerUser = os.Getenv("DOCKER_USER__" + suffix) 321 | dockerPasswd = os.Getenv("DOCKER_PASS__" + suffix) 322 | if dockerUser != "" && dockerPasswd != "" { 323 | log.Debug("using credentials for user " + dockerUser + " from env var for suffix " + suffix) 324 | } 325 | } 326 | 327 | // if credentials are not found in env vars, look in the Docker credentials store 328 | if (dockerUser == "" || dockerPasswd == "") && authident != "__NONE__" { 329 | log.Debug("no credentials found in env vars. Trying Docker credentials store") 330 | log.Debug("Using config file: ", viper.ConfigFileUsed()) 331 | 332 | if viper.ConfigFileUsed() == "" { 333 | log.Error("Docker config file not found. Run \"docker login\" first to create it. ") 334 | os.Exit(1) 335 | } 336 | 337 | // a provider can request to handle auth itself with authident __NONE__ 338 | if authident != "__NONE__" { 339 | dockerUser, dockerPasswd, err = util.GetDockerCreds(authident, authidentIsFuzzy) 340 | if err != nil { 341 | log.Error(err) 342 | os.Exit(1) 343 | } 344 | } else { 345 | dockerUser = "" 346 | dockerPasswd = "" 347 | } 348 | } 349 | 350 | //log.Debug("Using Docker creds: ", dockerUser, " ", dockerPasswd) 351 | log.Debug("Using Docker creds: ", dockerUser, " ", "********") 352 | 353 | err = prov.Pushrm(servername, namespacename, reponame, tagname, dockerUser, dockerPasswd, readme, pushrmShortDesc) 354 | if err != nil { 355 | log.Error(err) 356 | os.Exit(1) 357 | } 358 | 359 | return nil 360 | 361 | // --------- 362 | } 363 | 364 | func init() { 365 | 366 | const usageTemplate = ` 367 | Usage:{{if .Runnable}} 368 | {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} 369 | {{.CommandPath}} [command]{{end}} 370 | 371 | Flags: 372 | {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{if .HasAvailableInheritedFlags}} 373 | 374 | Global Flags: 375 | {{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}} 376 | ` 377 | 378 | const helpTemplate = ` 379 | {{with (or .Long .Short)}}{{. | trimTrailingWhitespaces}} 380 | {{end}}{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}} 381 | ` 382 | 383 | rootCmd.AddCommand(pushrmCmd) 384 | // Here you will define your flags and configuration settings. 385 | 386 | // Cobra supports Persistent Flags which will work for this command 387 | // and all subcommands, e.g.: 388 | // pushrmCmd.PersistentFlags().String("foo", "", "A help for foo") 389 | 390 | // Cobra supports local flags which will only run when this command 391 | // is called directly, e.g.: 392 | // pushrmCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 393 | pushrmCmd.Flags().StringVarP(&providername, "provider", "p", "dockerhub", "repo type: dockerhub, harbor2, quay") 394 | pushrmCmd.Flags().StringVarP(&rfile, "file", "f", "", "README file (defaults: \"./README-containers.md\", \"./README.md\")") 395 | pushrmCmd.Flags().StringVarP(&shortdesc, "short", "s", "", "short description (optional)") 396 | pushrmCmd.Parent().SetUsageTemplate(usageTemplate) 397 | pushrmCmd.Parent().SetHelpTemplate(helpTemplate) 398 | 399 | viper.BindPFlag("provider", pushrmCmd.Flags().Lookup("provider")) 400 | viper.BindPFlag("file", pushrmCmd.Flags().Lookup("file")) 401 | viper.BindPFlag("short", pushrmCmd.Flags().Lookup("short")) 402 | } 403 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Christian Korneck 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package cmd 24 | 25 | import ( 26 | "fmt" 27 | "os" 28 | "path/filepath" 29 | 30 | log "github.com/sirupsen/logrus" 31 | 32 | "github.com/spf13/cobra" 33 | 34 | homedir "github.com/mitchellh/go-homedir" 35 | "github.com/spf13/viper" 36 | ) 37 | 38 | var cfgFile string 39 | var isDebug bool 40 | 41 | var dockerGlobalConfig string 42 | var dockerGlobalContext string 43 | var dockerGlobalHost string 44 | var dockerGlobalTlscacert string 45 | var dockerGlobalLoglevel string 46 | var dockerGlobalTlscert string 47 | var dockerGlobalTlskey string 48 | 49 | // rootCmd represents the base command when called without any subcommands 50 | var rootCmd = &cobra.Command{ 51 | Use: "docker-pushrm", 52 | Short: "push README file from current working directory to container registry (Dockerhub, quay, harbor2)", 53 | Long: `push README file from current working directory to container registry (Dockerhub, quay, harbor2) 54 | `, 55 | // Uncomment the following line if your bare application 56 | // has an action associated with it: 57 | // Run: func(cmd *cobra.Command, args []string) { }, 58 | } 59 | 60 | // Execute adds all child commands to the root command and sets flags appropriately. 61 | // This is called by main.main(). It only needs to happen once to the rootCmd. 62 | func Execute() { 63 | if err := rootCmd.Execute(); err != nil { 64 | //at this point we don't have logrus yet, using print instead 65 | fmt.Println(err) 66 | os.Exit(1) 67 | } 68 | } 69 | 70 | func init() { 71 | 72 | cobra.OnInitialize(initConfig) 73 | 74 | // Here you will define your flags and configuration settings. 75 | // Cobra supports persistent flags, which, if defined here, 76 | // will be global for your application. 77 | 78 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default \"$HOME/.docker/config.json\")") 79 | rootCmd.PersistentFlags().BoolVarP(&isDebug, "debug", "D", false, "Enable debug mode") 80 | 81 | // these are the docker cli global flags 82 | // (we define them here so that our plugin doesn't break if they're set, but don't do anything with em) 83 | rootCmd.PersistentFlags().StringVarP(&dockerGlobalContext, "context", "c", "", "(not supported)") 84 | rootCmd.PersistentFlags().StringVarP(&dockerGlobalHost, "host", "H", "", "(not supported)") 85 | rootCmd.PersistentFlags().StringVarP(&dockerGlobalLoglevel, "log-level", "l", "", "(not supported)") 86 | rootCmd.PersistentFlags().Bool("tls", true, "(not supported)") 87 | rootCmd.PersistentFlags().Bool("tlsverify", true, "(not supported)") 88 | rootCmd.PersistentFlags().StringVar(&dockerGlobalTlscacert, "tlscacert", "", "(not supported)") 89 | rootCmd.PersistentFlags().StringVar(&dockerGlobalTlscert, "tlscert", "", "(not supported)") 90 | rootCmd.PersistentFlags().StringVar(&dockerGlobalTlskey, "tlskey", "", "(not supported)") 91 | 92 | // hide unsupported flags so that they don't show up with `docker-pushrm pushrm --help` 93 | rootCmd.PersistentFlags().MarkHidden("context") 94 | rootCmd.PersistentFlags().MarkHidden("host") 95 | rootCmd.PersistentFlags().MarkHidden("log-level") 96 | rootCmd.PersistentFlags().MarkHidden("tls") 97 | rootCmd.PersistentFlags().MarkHidden("tlsverify") 98 | rootCmd.PersistentFlags().MarkHidden("tlscacert") 99 | rootCmd.PersistentFlags().MarkHidden("tlscert") 100 | rootCmd.PersistentFlags().MarkHidden("tlskey") 101 | 102 | viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config")) 103 | viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")) 104 | 105 | // Cobra also supports local flags, which will only run 106 | // when this action is called directly. 107 | // rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 108 | 109 | } 110 | 111 | // initConfig reads in config file and ENV variables if set. 112 | func initConfig() { 113 | viper.AutomaticEnv() // read in environment variables that match 114 | viper.SetEnvPrefix("pushrm") 115 | 116 | pushrmConfig := viper.GetString("config") 117 | pushrmDebug := viper.GetBool("debug") 118 | 119 | if pushrmDebug { 120 | log.SetLevel(log.DebugLevel) 121 | } else { 122 | log.SetLevel(log.WarnLevel) 123 | } 124 | log.SetFormatter(&log.TextFormatter{DisableTimestamp: true}) 125 | 126 | log.Debug("root cmd init config") 127 | 128 | if pushrmConfig != "" { 129 | // Use config file from the flag. 130 | viper.SetConfigFile(pushrmConfig) 131 | } else { 132 | // Find home directory. 133 | home, err := homedir.Dir() 134 | if err != nil { 135 | log.Debug(err) 136 | log.Error("can't find home dir / Docker config file") 137 | os.Exit(1) 138 | } 139 | log.Debug("home dir: ", home) 140 | 141 | // Search config in home directory with name ".docker-pushrm" (without extension). 142 | viper.AddConfigPath(filepath.Join(home, "/.docker")) 143 | 144 | viper.SetConfigName("config") //filename without .json extension 145 | } 146 | 147 | // If a config file is found, read it in. 148 | if err := viper.ReadInConfig(); err == nil { 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/christian-korneck/docker-pushrm 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/mitchellh/go-homedir v1.1.0 7 | github.com/sirupsen/logrus v1.8.1 8 | github.com/spf13/cobra v1.2.1 9 | github.com/spf13/viper v1.8.1 10 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect 11 | golang.org/x/text v0.3.6 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /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.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 17 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 18 | cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= 19 | cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= 20 | cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= 21 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 22 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 23 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 24 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 25 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 26 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 27 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 28 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 29 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 30 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 31 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 32 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 33 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 34 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 35 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 36 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 37 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 38 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 39 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 40 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 41 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 42 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 43 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 44 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 45 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 46 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 47 | github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= 48 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 49 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 50 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 51 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 52 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 53 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 54 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 55 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 56 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 57 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 58 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 59 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 60 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 61 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 62 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 63 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 64 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 65 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 66 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 67 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 68 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 69 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 70 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 71 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 72 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 73 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 74 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 75 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 76 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 77 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 78 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 79 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 80 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 81 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 82 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 83 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 84 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 85 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 86 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 87 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 88 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 89 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 90 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 91 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 92 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 93 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 94 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 95 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 96 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 97 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 98 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 99 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 100 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 101 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 102 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 103 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 104 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 105 | github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= 106 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 107 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 108 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 109 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 110 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 111 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 112 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 113 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 114 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 115 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 116 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 117 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 118 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 119 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 120 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 121 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 122 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 123 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 124 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 125 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 126 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 127 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 128 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 129 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 130 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 131 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 132 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 133 | github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 134 | github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 135 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 136 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 137 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 138 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 139 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 140 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 141 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 142 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 143 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 144 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 145 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 146 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 147 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 148 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 149 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 150 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 151 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 152 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 153 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 154 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 155 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 156 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 157 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 158 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 159 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 160 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 161 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 162 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 163 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 164 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 165 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 166 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 167 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 168 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 169 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 170 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 171 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 172 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 173 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 174 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 175 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 176 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 177 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 178 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 179 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 180 | github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= 181 | github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 182 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 183 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 184 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 185 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 186 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 187 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 188 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 189 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 190 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 191 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 192 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 193 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 194 | github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 195 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 196 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 197 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 198 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 199 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 200 | github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= 201 | github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 202 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 203 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 204 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 205 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 206 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 207 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 208 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 209 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 210 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 211 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 212 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 213 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 214 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 215 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 216 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 217 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 218 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 219 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 220 | github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= 221 | github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= 222 | github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= 223 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 224 | github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= 225 | github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= 226 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 227 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 228 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 229 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 230 | github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= 231 | github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= 232 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 233 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 234 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 235 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 236 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 237 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 238 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 239 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 240 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 241 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 242 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 243 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 244 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 245 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 246 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 247 | go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= 248 | go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= 249 | go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= 250 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 251 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 252 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 253 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 254 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 255 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 256 | go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 257 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 258 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 259 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= 260 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 261 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 262 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 263 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 264 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 265 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 266 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 267 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 268 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 269 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 270 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 271 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 272 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 273 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 274 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 275 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 276 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 277 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 278 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 279 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 280 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 281 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 282 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 283 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 284 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 285 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 286 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 287 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 288 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 289 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 290 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 291 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 292 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 293 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 294 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 295 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 296 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 297 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 298 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 299 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 300 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 301 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 302 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 303 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 304 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 305 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 306 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 307 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 308 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 309 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 310 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 311 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 312 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 313 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 314 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 315 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 316 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 317 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 318 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 319 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 320 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 321 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 322 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 323 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 324 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 325 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 326 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 327 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 328 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 329 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 330 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 331 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 332 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 333 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 334 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 335 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 336 | golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= 337 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 338 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 339 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 340 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 341 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 342 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 343 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 344 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 345 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 346 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 347 | golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 348 | golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 349 | golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 350 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 351 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 352 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 353 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 354 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 355 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 356 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 357 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 358 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 359 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 360 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 361 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 362 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 363 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 364 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 365 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 366 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 367 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 368 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 369 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 370 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 371 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 372 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 373 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 374 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 375 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 376 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 377 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 378 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 379 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 380 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 381 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 382 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 383 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 384 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 385 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 386 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 387 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 388 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 389 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 390 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 391 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 392 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 393 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 394 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 395 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 396 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 397 | golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 398 | golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 399 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 400 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 401 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 402 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 403 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 404 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 405 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 406 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 407 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 408 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 409 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 410 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 411 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 412 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 413 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 414 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 415 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 416 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 417 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 418 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 419 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 420 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 421 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 422 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 423 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 424 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 425 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 426 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 427 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 428 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 429 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 430 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 431 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 432 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 433 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 434 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 435 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 436 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 437 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 438 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 439 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 440 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 441 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 442 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 443 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 444 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 445 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 446 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 447 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 448 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 449 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 450 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 451 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 452 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 453 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 454 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 455 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 456 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 457 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 458 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 459 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 460 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 461 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 462 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 463 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 464 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 465 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 466 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 467 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 468 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 469 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 470 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 471 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 472 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 473 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 474 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 475 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 476 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 477 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 478 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 479 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 480 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 481 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 482 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 483 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 484 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 485 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 486 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 487 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 488 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 489 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 490 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 491 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 492 | google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= 493 | google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= 494 | google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= 495 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 496 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 497 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 498 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 499 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 500 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 501 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 502 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 503 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 504 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 505 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 506 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 507 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 508 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 509 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 510 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 511 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 512 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 513 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 514 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 515 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 516 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 517 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 518 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 519 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 520 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 521 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 522 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 523 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 524 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 525 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 526 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 527 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 528 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 529 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 530 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 531 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 532 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 533 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 534 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 535 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 536 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 537 | google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 538 | google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 539 | google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 540 | google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 541 | google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= 542 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= 543 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 544 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 545 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 546 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 547 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 548 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 549 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 550 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 551 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 552 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 553 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 554 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 555 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 556 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 557 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 558 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 559 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 560 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 561 | google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 562 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 563 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 564 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 565 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 566 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 567 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 568 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 569 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 570 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 571 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 572 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 573 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 574 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 575 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 576 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 577 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 578 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 579 | gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= 580 | gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 581 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 582 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 583 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 584 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 585 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 586 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 587 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 588 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 589 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 590 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 591 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 592 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 593 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 594 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 595 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 596 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 597 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 598 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 599 | -------------------------------------------------------------------------------- /gon.json: -------------------------------------------------------------------------------- 1 | { 2 | "source" : ["./docker-pushrm"], 3 | "bundle_id" : "de.korneck.christian.docker-pushrm", 4 | "apple_id": { 5 | "password": "@env:AC_PASSWORD" 6 | }, 7 | "sign" :{ 8 | "application_identity" : "Developer ID Application: Christian Korneck" 9 | }, 10 | "zip" :{ 11 | "output_path" : "./docker-pushrm.zip" 12 | } 13 | } -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Christian Korneck 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package main 24 | 25 | import ( 26 | "os" 27 | 28 | "github.com/christian-korneck/docker-pushrm/cmd" 29 | "github.com/christian-korneck/docker-pushrm/util" 30 | ) 31 | 32 | func main() { 33 | 34 | // if called as a standalone tool (not as a docker cli plugin), proxy directly to the `pushrm` subcommand 35 | if (os.Getenv("DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND") == "") && (util.StringInSlice("docker-cli-plugin-metadata", os.Args) == false) { 36 | 37 | newArgs := make([]string, (len(os.Args) + 1)) 38 | 39 | newArgs[0] = os.Args[0] 40 | newArgs[1] = "pushrm" 41 | for i := 2; i <= len(os.Args); i++ { 42 | newArgs[i] = os.Args[i-1] 43 | } 44 | os.Args = newArgs 45 | 46 | } 47 | 48 | // Cobra, übernehmen Sie 49 | cmd.Execute() 50 | } 51 | -------------------------------------------------------------------------------- /provider/dockerhub/dockerhub.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Christian Korneck 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package dockerhub 24 | 25 | import ( 26 | "encoding/json" 27 | "fmt" 28 | "io/ioutil" 29 | "net/http" 30 | "strings" 31 | 32 | "github.com/christian-korneck/docker-pushrm/util" 33 | log "github.com/sirupsen/logrus" 34 | ) 35 | 36 | //Dockerhub struct 37 | type Dockerhub struct { 38 | } 39 | 40 | //Pushrm is the main provider function 41 | func (f Dockerhub) Pushrm(servername string, namespacename string, reponame string, tagname string, dockerUser string, dockerPasswd string, readme string, shortdesc string) error { 42 | 43 | log.Debug("Dockerhub.Pushrm called") 44 | jwt, err := GetJwt(dockerUser, dockerPasswd) 45 | if err != nil { 46 | log.Debug(err) 47 | return fmt.Errorf("error trying to get a JWT token from Dockerhub for the stored Docker login. Try \"docker logout\" and \"docker login\". Also, if you have 2FA auth enabled in Dockerhub you'll need to disable it for this tool to work. (This is an unfortunate Dockerhub limitation, see docs for more infos). ") 48 | } 49 | err = PatchDescription(jwt, readme, namespacename, reponame, shortdesc) 50 | if err != nil { 51 | log.Debug(err) 52 | return fmt.Errorf("error pushing readme to repo server. See error message below. Run with \"--debug\" for more details. \n\n" + err.Error()) 53 | } 54 | 55 | return nil 56 | } 57 | 58 | //GetAuthident returns authident for local Docker credentials store 59 | func (f Dockerhub) GetAuthident() (authident string) { 60 | log.Debug("Dockerhub.GetAuthident called") 61 | authident = "https://index.docker.io/v1/" 62 | return 63 | } 64 | 65 | //GetJwt Auth against Dockerhub with user/passwd and request a jwt token 66 | func GetJwt(dockerUser string, dockerPasswd string) (jwt string, error error) { 67 | 68 | url := "https://hub.docker.com/v2/users/login/" 69 | method := "POST" 70 | 71 | payloadJSON, err := json.Marshal(map[string]string{"username": dockerUser, "password": dockerPasswd}) 72 | if err != nil { 73 | log.Debug(err) 74 | return "", fmt.Errorf("error retrieving Dockerhub jwt token, error marshal payload") 75 | } 76 | 77 | payloadStr := util.BytesToString(payloadJSON) 78 | payload := strings.NewReader(payloadStr) 79 | 80 | client := &http.Client{} 81 | req, err := http.NewRequest(method, url, payload) 82 | 83 | if err != nil { 84 | log.Debug(err) 85 | return "", fmt.Errorf("error retrieving Dockerhub jwt token, error creating http request") 86 | } 87 | req.Header.Add("Content-Type", "application/json") 88 | 89 | res, err := client.Do(req) 90 | if err != nil { 91 | log.Debug(err) 92 | return "", fmt.Errorf("error retrieving Dockerhub jwt token, error making http request") 93 | } 94 | 95 | log.Debug("retrieve Dockerhub jwt token, status code: ", res.StatusCode) 96 | 97 | if res.StatusCode != 200 { 98 | return "", fmt.Errorf("error retrieving Dockerhub jwt token, bad status code for response: " + res.Status) 99 | 100 | } 101 | 102 | defer res.Body.Close() 103 | body, err := ioutil.ReadAll(res.Body) 104 | if err != nil { 105 | log.Debug(err) 106 | return "", fmt.Errorf("error retrieving Dockerhub jwt token, error reading response body") 107 | } 108 | 109 | var dat map[string]interface{} 110 | if err := json.Unmarshal(body, &dat); err != nil { 111 | log.Debug(err) 112 | return "", fmt.Errorf("error retrieving Dockerhub jwt token, error parsing json") 113 | } 114 | 115 | if dat["token"] == nil || dat["token"].(string) == "" { 116 | return "", fmt.Errorf("error retrieving Dockerhub jwt token, no jtw token received") 117 | } 118 | 119 | return dat["token"].(string), nil 120 | } 121 | 122 | //PatchDescription - api call to update the repo description 123 | func PatchDescription(jwt string, readme string, namespacename string, reponame string, shortdesc string) (error error) { 124 | 125 | // trailing slash is crucial 126 | apiurl := "https://hub.docker.com/v2/repositories/" + namespacename + "/" + reponame + "/" 127 | method := "PATCH" 128 | 129 | bodydata := make(map[string]string) 130 | bodydata["full_description"] = readme 131 | if shortdesc != "" { 132 | bodydata["description"] = shortdesc 133 | } 134 | jsonbody, _ := json.Marshal(bodydata) 135 | 136 | payload := strings.NewReader(string(jsonbody)) 137 | client := &http.Client{} 138 | req, err := http.NewRequest(method, apiurl, payload) 139 | if err != nil { 140 | log.Debug(err) 141 | return fmt.Errorf("error pushing README, error creating http request") 142 | } 143 | 144 | req.Header.Add("Authorization", "JWT "+jwt) 145 | req.Header.Add("Content-Type", "application/json") 146 | 147 | res, err := client.Do(req) 148 | if err != nil { 149 | log.Debug(err) 150 | return fmt.Errorf("error pushing README, error creating http request") 151 | } 152 | 153 | defer res.Body.Close() 154 | body, err := ioutil.ReadAll(res.Body) 155 | if err != nil { 156 | log.Debug(err) 157 | return fmt.Errorf("error pushing README, error reading response body") 158 | } 159 | 160 | log.Debug("push readme, response body: ", string(body)) 161 | 162 | var dat map[string]interface{} 163 | if err := json.Unmarshal(body, &dat); err != nil { 164 | log.Debug(err) 165 | return fmt.Errorf("error pushing README, error parsing returned json") 166 | } 167 | 168 | log.Debug("push README, status code: ", res.StatusCode) 169 | 170 | if res.StatusCode != 200 { 171 | msg := "error pushing README, bad status code for response: " + res.Status 172 | if dat["detail"] != nil { 173 | msg = msg + ". Server responded: \"" + dat["detail"].(string) + "\"" 174 | } 175 | if res.StatusCode == 403 { 176 | msg = msg + ". Try \"docker logout\" and \"docker login\". If you use a PAT token make sure it has sufficient privileges (\"admin\" scope)." 177 | 178 | } 179 | return fmt.Errorf(msg) 180 | 181 | } 182 | 183 | if dat["full_description"] != readme { 184 | return fmt.Errorf("error pushing README, pushed readme to repo server but validation failed") 185 | } 186 | 187 | if shortdesc != "" && dat["description"] != shortdesc { 188 | return fmt.Errorf("error setting Short Description, pushed to repo server but validation failed") 189 | } 190 | 191 | log.Debug("content validation successfull, readme successfully pushed to repo server") 192 | return nil 193 | 194 | } 195 | -------------------------------------------------------------------------------- /provider/harbor2/harbor2.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Christian Korneck 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package harbor2 24 | 25 | import ( 26 | "encoding/base64" 27 | "encoding/json" 28 | "fmt" 29 | "io/ioutil" 30 | "net/http" 31 | "strings" 32 | 33 | log "github.com/sirupsen/logrus" 34 | ) 35 | 36 | //Harbor2 struct 37 | type Harbor2 struct { 38 | } 39 | 40 | //Pushrm is the main provider function 41 | func (f Harbor2) Pushrm(servername string, namespacename string, reponame string, tagname string, dockerUser string, dockerPasswd string, readme string, shortdesc string) error { 42 | 43 | if shortdesc != "" { 44 | log.Warn("Short description not supported for provider \"harbor2\". Ignoring.") 45 | } 46 | 47 | log.Debug("Harbor2.Pushrm called") 48 | 49 | err := PatchDescription(dockerUser, dockerPasswd, readme, servername, namespacename, reponame) 50 | if err != nil { 51 | log.Debug(err) 52 | return fmt.Errorf("error pushing readme to repo server. See error message below. Run with \"--debug\" for more details. \n\n" + err.Error()) 53 | } 54 | 55 | return nil 56 | } 57 | 58 | //GetAuthident returns authident for local Docker credentials store 59 | func (f Harbor2) GetAuthident() (authident string) { 60 | log.Debug("Harbor2.GetAuthident called") 61 | authident = "__SERVERNAME__" 62 | return 63 | } 64 | 65 | //PatchDescription - api call to update the repo description 66 | func PatchDescription(dockerUser string, dockerPasswd string, readme string, servername string, namespacename string, reponame string) (error error) { 67 | 68 | apiurl := "https://" + servername + "/api/v2.0/projects/" + namespacename + "/repositories/" + reponame 69 | method := "PUT" 70 | 71 | jsonbody, _ := json.Marshal(map[string]string{"description": readme}) 72 | payload := strings.NewReader(string(jsonbody)) 73 | 74 | client := &http.Client{} 75 | req, err := http.NewRequest(method, apiurl, payload) 76 | if err != nil { 77 | log.Debug(err) 78 | return fmt.Errorf("error pushing README, error creating http request") 79 | } 80 | 81 | creds := base64.StdEncoding.EncodeToString([]byte(dockerUser + ":" + dockerPasswd)) 82 | req.Header.Add("Authorization", "Basic "+creds) 83 | req.Header.Add("Content-Type", "application/json") 84 | 85 | res, err := client.Do(req) 86 | if err != nil { 87 | log.Debug(err) 88 | return fmt.Errorf("error pushing README, error creating http request") 89 | } 90 | 91 | defer res.Body.Close() 92 | body, err := ioutil.ReadAll(res.Body) 93 | if err != nil { 94 | log.Debug(err) 95 | return fmt.Errorf("error pushing README, error reading response body") 96 | } 97 | 98 | log.Debug("push readme, response body: " + string(body)) 99 | 100 | var dat map[string]interface{} 101 | // json only returned in case of failure for error message, otherwise empty 102 | if res.StatusCode != 200 { 103 | if err := json.Unmarshal(body, &dat); err != nil { 104 | log.Debug(err) 105 | } 106 | } 107 | 108 | log.Debug("push README, status code: ", res.StatusCode) 109 | 110 | if res.StatusCode != 200 { 111 | msg := "error pushing README, bad status code for response: " + res.Status 112 | if dat["errors"] != nil { 113 | 114 | firsterror := dat["errors"].([]interface{})[0].(map[string]interface{}) 115 | msg = msg + ". Server responded: \"" + firsterror["code"].(string) + " - " + firsterror["message"].(string) + "\"" 116 | } 117 | if res.StatusCode == 403 { 118 | msg = msg + ". Try \"docker logout\" and \"docker login\". " 119 | 120 | } 121 | return fmt.Errorf(msg) 122 | 123 | } else { 124 | log.Debug("status code OK, readme successfully pushed to repo server") 125 | return nil 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /provider/provider/provider.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Christian Korneck 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package provider 24 | 25 | //Provider interface 26 | type Provider interface { 27 | //GetAuthident - returns the key name under which the provider credentials are stored in Docker's credentials store. Special values: __SERVERNAME__ = use servername, __NONE__ = retrieving credentials will be handled by the provider 28 | GetAuthident() (authident string) 29 | //Pushrm function - main provider function, performs the api call to update the repo description 30 | Pushrm(servername string, namespacename string, reponame string, tagname string, dockerUser string, dockerPasswd string, readme string, shortdesc string) error 31 | } 32 | -------------------------------------------------------------------------------- /provider/quay/quay.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Christian Korneck 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package quay 24 | 25 | import ( 26 | "encoding/json" 27 | "fmt" 28 | "io/ioutil" 29 | "net/http" 30 | "strings" 31 | 32 | "github.com/christian-korneck/docker-pushrm/util" 33 | log "github.com/sirupsen/logrus" 34 | ) 35 | 36 | //Quay struct 37 | type Quay struct { 38 | } 39 | 40 | //Pushrm is the main provider function 41 | func (f Quay) Pushrm(servername string, namespacename string, reponame string, tagname string, dockerUser string, dockerPasswd string, readme string, shortdesc string) error { 42 | 43 | if shortdesc != "" { 44 | log.Warn("Short description not supported for provider \"quay\". Ignoring.") 45 | } 46 | 47 | log.Debug("Quay.Pushrm called") 48 | 49 | apikey, err := util.GetApikey(servername) 50 | if err != nil { 51 | return fmt.Errorf(err.Error()) 52 | } 53 | //log.Debug("apikey: " + apikey) 54 | log.Debug("apikey: " + "********") 55 | 56 | err = PatchDescription(apikey, readme, servername, namespacename, reponame) 57 | if err != nil { 58 | log.Debug(err) 59 | return fmt.Errorf("error pushing readme to repo server. See error message below. Run with \"--debug\" for more details. \n\n" + err.Error()) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | //GetAuthident returns authident for local Docker credentials store 66 | func (f Quay) GetAuthident() (authident string) { 67 | log.Debug("Quay.GetAuthident called") 68 | authident = "__NONE__" 69 | return 70 | } 71 | 72 | //PatchDescription - api call to update the repo description 73 | func PatchDescription(quaytoken string, readme string, servername string, namespacename string, reponame string) (error error) { 74 | 75 | apiurl := "https://" + servername + "/api/v1/repository/" + namespacename + "/" + reponame 76 | method := "PUT" 77 | 78 | jsonbody, _ := json.Marshal(map[string]string{"description": readme}) 79 | payload := strings.NewReader(string(jsonbody)) 80 | 81 | client := &http.Client{} 82 | req, err := http.NewRequest(method, apiurl, payload) 83 | if err != nil { 84 | log.Debug(err) 85 | return fmt.Errorf("error pushing README, error creating http request") 86 | } 87 | 88 | req.Header.Add("Authorization", "Bearer "+quaytoken) 89 | req.Header.Add("Content-Type", "application/json") 90 | 91 | res, err := client.Do(req) 92 | if err != nil { 93 | log.Debug(err) 94 | return fmt.Errorf("error pushing README, error creating http request") 95 | } 96 | 97 | defer res.Body.Close() 98 | body, err := ioutil.ReadAll(res.Body) 99 | if err != nil { 100 | log.Debug(err) 101 | return fmt.Errorf("error pushing README, error reading response body") 102 | } 103 | 104 | log.Debug("push readme, response body: " + string(body)) 105 | 106 | var dat map[string]interface{} 107 | // we need the json response only in case of failure to get the error message 108 | if res.StatusCode != 200 { 109 | if err := json.Unmarshal(body, &dat); err != nil { 110 | log.Debug(err) 111 | } 112 | } 113 | 114 | log.Debug("push README, status code: ", res.StatusCode) 115 | 116 | if res.StatusCode != 200 { 117 | msg := "error pushing README, bad status code for response: " + res.Status 118 | if dat["error_message"] != nil { 119 | 120 | msg = msg + ". Server responded: \"" + dat["error_message"].(string) + "\"" 121 | } 122 | if res.StatusCode == 403 { 123 | msg = msg + ". Try \"docker logout\" and \"docker login\"" 124 | 125 | } 126 | return fmt.Errorf(msg) 127 | 128 | } else { 129 | log.Debug("status code OK, readme successfully pushed to repo server") 130 | return nil 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2020 Christian Korneck 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | */ 22 | 23 | package util 24 | 25 | import ( 26 | "encoding/base64" 27 | "encoding/json" 28 | "fmt" 29 | "io" 30 | "io/ioutil" 31 | "os" 32 | "os/exec" 33 | "path/filepath" 34 | "strings" 35 | 36 | log "github.com/sirupsen/logrus" 37 | 38 | "github.com/spf13/viper" 39 | ) 40 | 41 | // BytesToString converts 42 | func BytesToString(b []byte) string { 43 | return string(b[:]) 44 | } 45 | 46 | // StringInSlice checks if a string exists in a slice 47 | func StringInSlice(checkval string, list []string) bool { 48 | for _, b := range list { 49 | if b == checkval { 50 | return true 51 | } 52 | } 53 | return false 54 | } 55 | 56 | // ReadFile reads a file 57 | func ReadFile(path string) (filecontent string, error error) { 58 | content, err := ioutil.ReadFile(path) 59 | if err != nil { 60 | log.Debug(err) 61 | return "", fmt.Errorf("could not read README file: " + path) 62 | } 63 | text := string(content) 64 | return text, nil 65 | } 66 | 67 | //GetApikey retrieves an API key from env var or the local Docker config file 68 | func GetApikey(servername string) (apikey string, error error) { 69 | log.Debug("util.GetApikey called") 70 | 71 | genericEnvval := os.Getenv("DOCKER_APIKEY") 72 | if genericEnvval != "" { 73 | return genericEnvval, nil 74 | } 75 | 76 | envkey := "APIKEY__" + strings.ToUpper(strings.Replace(servername, ".", "_", -1)) 77 | querykey := "plugins.docker-pushrm.apikey_" + servername 78 | 79 | envval := os.Getenv(envkey) 80 | 81 | if envval != "" { 82 | apikey = envval 83 | return apikey, nil 84 | } else { 85 | cfgval := viper.GetString(querykey) 86 | if cfgval != "" { 87 | apikey = cfgval 88 | return apikey, nil 89 | } else { 90 | return "", fmt.Errorf("could not find api key for server " + servername + ". Either specify env var DOCKER_APIKEY or env var " + envkey + " or " + querykey + " in the local Docker config file. ") 91 | } 92 | 93 | } 94 | 95 | } 96 | 97 | //GetDockerCreds retrieves credentials from the Docker creds store 98 | func GetDockerCreds(authident string, authidentIsFuzzy bool) (dockerUser string, dockerPasswd string, error error) { 99 | 100 | var candidates []string 101 | if authidentIsFuzzy == true { 102 | candidates = []string{authident, ("https://" + authident), ("https://" + authident + "/"), ("http://" + authident), ("http://" + authident + "/")} 103 | } else { 104 | candidates = []string{authident} 105 | } 106 | for _, candidate := range candidates { 107 | dockerUser, dockerPasswd = "", "" 108 | dockerUser, dockerPasswd, err := QueryDockerCreds(candidate) 109 | if err != nil { 110 | log.Debug("tried candidate " + candidate + ", got error: " + err.Error()) 111 | } 112 | 113 | if dockerUser == "" && dockerPasswd == "" { 114 | log.Debug("tried candidate " + candidate + ": could not find credentials") 115 | } else { 116 | log.Debug("tried candidate " + candidate + ": found credentials for user " + dockerUser) 117 | return dockerUser, dockerPasswd, nil 118 | } 119 | 120 | } 121 | 122 | return "", "", fmt.Errorf("no Docker credentials found for this server/provider. Run 'docker login' first. ") 123 | 124 | } 125 | 126 | //QueryDockerCreds fetches credentials for an authid 127 | func QueryDockerCreds(authident string) (dockerUser string, dockerPasswd string, error error) { 128 | 129 | log.Debug("util.GetDockerCreds called") 130 | 131 | if viper.GetString("auths."+authident+".auth") != "" { 132 | 133 | credsb64 := viper.GetString("auths." + authident + ".auth") 134 | credsclearb, err := base64.StdEncoding.DecodeString(credsb64) 135 | if err != nil { 136 | log.Debug(err) 137 | return "", "", fmt.Errorf("Error parsing auth info from the Docker config file. Check your local Docker config. ") 138 | } 139 | credsclear := string(credsclearb) 140 | credssplit := strings.Split(credsclear, ":") 141 | dockerUser = credssplit[0] 142 | dockerPasswd = credsclear[len(dockerUser)+1:] 143 | 144 | } else { 145 | if viper.GetString("credsStore") != "" { 146 | executable := "docker-credential-" + viper.GetString("credsStore") 147 | if viper.GetString("credsStore") == "wincred" { 148 | executable = executable + ".exe" 149 | } 150 | shx := exec.Command(executable, "get") 151 | stdin, err := shx.StdinPipe() 152 | if err != nil { 153 | log.Debug(err) 154 | return "", "", fmt.Errorf("Error executing the Docker credentials helper. Check your local Docker config and/or installation. ") 155 | } 156 | 157 | done := make(chan bool) 158 | 159 | go func() { 160 | defer func() { done <- true }() 161 | defer stdin.Close() 162 | io.WriteString(stdin, authident) 163 | 164 | }() 165 | 166 | <-done 167 | 168 | out, err := shx.CombinedOutput() 169 | if err != nil { 170 | log.Debug(err) 171 | return "", "", fmt.Errorf("no Docker credentials found for this server/provider. Run 'docker login' first. ") 172 | } 173 | 174 | var dat map[string]interface{} 175 | if err := json.Unmarshal(out, &dat); err != nil { 176 | log.Debug(err) 177 | return "", "", fmt.Errorf("Error parsing credentials from Docker creds provider. Run 'docker login' first. ") 178 | } 179 | dockerUser = dat["Username"].(string) 180 | dockerPasswd = dat["Secret"].(string) 181 | 182 | } else { 183 | return "", "", fmt.Errorf("no Docker credentials found for this server/provider. Run 'docker login' first. ") 184 | } 185 | } 186 | 187 | return dockerUser, dockerPasswd, nil 188 | } 189 | 190 | //FindReadmeFile trys to find a readme file in the cwd 191 | func FindReadmeFile() (foundfile string, error error) { 192 | preferedfilenames := []string{"./README-containers.md", "./README.md"} //prefer these filenames in this order 193 | 194 | for _, preferedfilename := range preferedfilenames { 195 | matches, err := filepath.Glob(preferedfilename) 196 | if err != nil { 197 | log.Debug(err) 198 | return "", fmt.Errorf("error while searching for default readme file") 199 | } 200 | if len(matches) >= 1 { 201 | foundfile = preferedfilename 202 | break 203 | } 204 | } 205 | 206 | if foundfile == "" { 207 | matches, err := filepath.Glob("./[R|r][E|e][A|a][D|d][M|m][E|e]*") 208 | if err != nil { 209 | log.Debug(err) 210 | return "", fmt.Errorf("error while searching for alternate readme file") 211 | } 212 | if len(matches) < 1 { 213 | return "", fmt.Errorf("README file not found in the current working directory. Create a file \"README-containers.md\" or \"README.md\" or \"cd\" into a directory that contains a README file. ") 214 | } else { 215 | foundfile = matches[0] 216 | } 217 | } 218 | 219 | return foundfile, nil 220 | } 221 | --------------------------------------------------------------------------------