diff --git a/.all-contributorsrc b/.all-contributorsrc index e98384d..270f462 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -5,6 +5,30 @@ "imageSize": 100, "commit": false, "contributors": [ + { + "login": "piksel", + "name": "nils måsén", + "avatar_url": "https://avatars2.githubusercontent.com/u/807383?v=4", + "profile": "https://piksel.se", + "contributions": [ + "code", + "doc", + "maintenance", + "review" + ] + }, + { + "login": "simskij", + "name": "Simon Aronsson", + "avatar_url": "https://avatars0.githubusercontent.com/u/1596025?v=4", + "profile": "http://simme.dev", + "contributions": [ + "code", + "doc", + "maintenance", + "review" + ] + }, { "login": "Codelica", "name": "James", @@ -273,18 +297,6 @@ "code" ] }, - { - "login": "simskij", - "name": "Simon Aronsson", - "avatar_url": "https://avatars0.githubusercontent.com/u/1596025?v=4", - "profile": "http://simme.dev", - "contributions": [ - "code", - "maintenance", - "review", - "doc" - ] - }, { "login": "Ansem93", "name": "Ansem93", @@ -508,16 +520,6 @@ "doc" ] }, - { - "login": "piksel", - "name": "nils måsén", - "avatar_url": "https://avatars2.githubusercontent.com/u/807383?v=4", - "profile": "https://piksel.se", - "contributions": [ - "doc", - "code" - ] - }, { "login": "arnested", "name": "Arne Jørgensen", @@ -693,7 +695,8 @@ "profile": "https://github.com/ksurl", "contributions": [ "doc", - "code" + "code", + "infra" ] }, { @@ -812,6 +815,67 @@ "contributions": [ "doc" ] + }, + { + "login": "Foxite", + "name": "Dirk Kok", + "avatar_url": "https://avatars.githubusercontent.com/u/20421657?v=4", + "profile": "https://ko-fi.com/foxite", + "contributions": [ + "code" + ] + }, + { + "login": "EDIflyer", + "name": "EDIflyer", + "avatar_url": "https://avatars.githubusercontent.com/u/13610277?v=4", + "profile": "https://github.com/EDIflyer", + "contributions": [ + "doc" + ] + }, + { + "login": "jauderho", + "name": "Jauder Ho", + "avatar_url": "https://avatars.githubusercontent.com/u/13562?v=4", + "profile": "https://github.com/jauderho", + "contributions": [ + "code" + ] + }, + { + "login": "andriibratanin", + "name": "Andrii Bratanin", + "avatar_url": "https://avatars.githubusercontent.com/u/20169213?v=4", + "profile": "https://github.com/andriibratanin" + }, + { + "login": "IAmTamal", + "name": "Tamal Das ", + "avatar_url": "https://avatars.githubusercontent.com/u/72851613?v=4", + "profile": "https://tamal.vercel.app/", + "contributions": [ + "doc" + ] + }, + { + "login": "testwill", + "name": "guangwu", + "avatar_url": "https://avatars.githubusercontent.com/u/8717479?v=4", + "profile": "https://github.com/testwill", + "contributions": [ + "doc" + ] + }, + { + "login": "nothub", + "name": "Florian Hübner", + "avatar_url": "https://avatars.githubusercontent.com/u/48992448?v=4", + "profile": "http://hub.lol", + "contributions": [ + "doc", + "code" + ] } ], "contributorsPerLine": 7, @@ -820,5 +884,6 @@ "repoType": "github", "repoHost": "https://github.com", "commitConvention": "none", - "skipCi": true + "skipCi": true, + "commitType": "docs" } diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fa2b0d3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.css] +indent_style = space +indent_size = 2 + +[{go.mod,go.sum,*.go}] +indent_style = tab +indent_size = 4 \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 419e96c..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -custom: https://www.amazon.com/hz/wishlist/ls/F94JJV822VX6 -github: simskij diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..d4b87f1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,71 @@ +name: 🐛 Bug report +description: Create a report to help us improve +labels: ["Priority: Medium, Status: Available, Type: Bug"] + +body: + - type: markdown + attributes: + value: Before submitting your issue, please make sure you're using the containrrr/watchtower:latest image. If not, switch to this image prior to posting your report. Other forks, or the old `v2tec` image are **not** supported. + + - type: textarea + id: description + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + description: Steps to reproduce the behavior + value: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: true + + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Please add screenshots if applicable + validations: + required: false + + - type: textarea + attributes: + label: Environment + description: We would want to know the following things + value: | + - Platform + - Architecture + - Docker Version + validations: + required: true + + - type: textarea + attributes: + label: Your logs + description: Paste the logs from running watchtower with the `--debug` option. + render: text + validations: + required: true + + - type: textarea + attributes: + label: Additional context + description: Add any other context about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 53e1a53..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: 'Priority: Medium, Status: Available, Type: Bug' -assignees: '' - ---- - - -**Describe the bug** - - -**To Reproduce** - - -**Expected behavior** - - -**Screenshots** - -**Environment** - - -
- Logs from running watchtower with the --debug option - -``` - -``` - -
- -**Additional context** - diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index ca13f1d..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: 'Priority: Low, Status: Available, Type: Enhancement' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..c1cc511 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,36 @@ +name: 💡 Feature request +description: Have a new idea/feature ? Please suggest! +labels: ["Priority: Low, Status: Available, Type: Enhancement"] +body: + - type: textarea + id: description + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true + + - type: textarea + id: extrainfo + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + validations: + required: false + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fe53bc9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + + - package-ecosystem: "docker" # See documentation for possible values + directory: "/dockerfiles" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 2437bb2..c479d05 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. @@ -44,7 +44,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -55,7 +55,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -69,4 +69,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/dependabot-approve.yml b/.github/workflows/dependabot-approve.yml new file mode 100644 index 0000000..46f9d18 --- /dev/null +++ b/.github/workflows/dependabot-approve.yml @@ -0,0 +1,12 @@ +name: Auto approve dependabot PRs + +on: pull_request_target + +jobs: + auto-approve: + runs-on: ubuntu-latest + permissions: + pull-requests: write + if: github.actor == 'dependabot[bot]' + steps: + - uses: hmarr/auto-approve-action@v3 diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index 20302f0..83ff8a0 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -1,6 +1,11 @@ name: Greetings -on: [issues] +on: + # Runs in the context of the target (containrrr/watchtower) repository, and as such has access to GITHUB_TOKEN + pull_request_target: + types: [opened] + issues: + types: [opened] jobs: greeting: diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 6247c69..4818e54 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -14,14 +14,24 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.20.x + - name: Build tplprev + run: scripts/build-tplprev.sh + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: 'pip' + cache-dependency-path: | + docs-requirements.txt - name: Install mkdocs run: | - pip install \ - mkdocs \ - mkdocs-material \ - md-toc + pip install -r docs-requirements.txt - name: Generate docs - run: mkdocs gh-deploy --strict \ No newline at end of file + run: mkdocs gh-deploy --strict diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 80eb3e3..f6866af 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -12,26 +12,24 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: 1.15.x - - name: Install linter - run: | - go get -u golang.org/x/lint/golint - - name: Lint files - run: | - golint -set_exit_status ./... + go-version: 1.20.x + - uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0 + with: + version: "2023.1.6" + install-go: "false" # StaticCheck uses go v1.17 which does not support `any` test: name: Test strategy: fail-fast: false matrix: go-version: - - 1.15.x + - 1.20.x platform: - macos-latest - windows-latest @@ -39,18 +37,18 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: 1.15.x + go-version: 1.20.x - name: Run tests run: | go test -v -coverprofile coverage.out -covermode atomic ./... - name: Publish coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} build: @@ -58,15 +56,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: 1.15.x + go-version: 1.20.x - name: Build - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3 with: version: v0.155.0 args: --snapshot --skip-publish --debug diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml index 4b8f238..95ee68d 100644 --- a/.github/workflows/release-dev.yaml +++ b/.github/workflows/release-dev.yaml @@ -10,25 +10,27 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Go - uses: actions/setup-go@v2 + - uses: actions/checkout@v4 with: - go-version: 1.15 + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: 1.20.x - name: Build run: ./build.sh test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: 1.15 + go-version: 1.20.x - name: Test run: go test -v -coverprofile coverage.out -covermode atomic ./... - name: Publish coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} publish: @@ -37,9 +39,9 @@ jobs: - test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Publish to Docker Hub - uses: jerray/publish-docker-action@master + uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} @@ -47,7 +49,7 @@ jobs: repository: containrrr/watchtower tags: latest-dev - name: Publish to GHCR - uses: jerray/publish-docker-action@master + uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14 with: username: ${{ secrets.BOT_USERNAME }} password: ${{ secrets.BOT_GHCR_PAT }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 919783b..370d395 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,9 +2,7 @@ name: Release (Production) on: workflow_dispatch: {} - release: - types: - - created + push: tags: - 'v[0-9]+.[0-9]+.[0-9]+' - '**/v[0-9]+.[0-9]+.[0-9]+' @@ -15,26 +13,24 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: 1.15.x - - name: Install linter - run: | - go get -u golang.org/x/lint/golint - - name: Lint files - run: | - golint -set_exit_status ./... + go-version: 1.20.x + - uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0 + with: + version: "2022.1.1" + install-go: "false" # StaticCheck uses go v1.17 which does not support `any` test: name: Test strategy: matrix: go-version: - - 1.15.x + - 1.20.x platform: - ubuntu-latest - macos-latest @@ -42,13 +38,13 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: 1.15.x + go-version: 1.20.x - name: Run tests run: | go test ./... -coverprofile coverage.out @@ -61,29 +57,29 @@ jobs: - lint env: CGO_ENABLED: 0 - TAG: ${{ github.event.release.tag_name }} + TAG: ${{ github.ref_name }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: 1.15.x + go-version: 1.20.x - name: Login to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc #v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR - uses: docker/login-action@v1 + uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc #v2 with: username: ${{ secrets.BOT_USERNAME }} password: ${{ secrets.BOT_GHCR_PAT }} registry: ghcr.io - name: Build - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3 with: version: v0.155.0 args: --debug @@ -95,7 +91,7 @@ jobs: echo '{"experimental": "enabled"}' > ~/.docker/config.json - name: Create manifest for version run: | - export DH_TAG=$(echo $TAG | sed 's/^v*//') + export DH_TAG=$(git tag --points-at HEAD | sed 's/^v*//') docker manifest create \ containrrr/watchtower:$DH_TAG \ containrrr/watchtower:amd64-$DH_TAG \ @@ -193,7 +189,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Pull new module version - uses: andrewslotin/go-proxy-pull-action@master + uses: andrewslotin/go-proxy-pull-action@50fea06a976087614babb9508e5c528b464f4645 #master@2022-10-14 diff --git a/.gitignore b/.gitignore index c371f41..9519257 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ dist .DS_Store /site coverage.out -*.coverprofile \ No newline at end of file +*.coverprofile + +docs/assets/wasm_exec.js +docs/assets/*.wasm \ No newline at end of file diff --git a/README.md b/README.md index e6754b0..f550302 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ $ docker run --detach \ containrrr/watchtower ``` +Watchtower is intended to be used in homelabs, media centers, local dev environments, and similar. We do **not** recommend using Watchtower in a commercial or production environment. If that is you, you should be looking into using Kubernetes. If that feels like too big a step for you, please look into solutions like [MicroK8s](https://microk8s.io/) and [k3s](https://k3s.io/) that take away a lot of the toil of running a Kubernetes cluster. + ## Documentation The full documentation is available at https://containrrr.dev/watchtower. @@ -42,119 +44,130 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

James

⚠️ 🤔

Florian

👀 📖

Brian DeHamer

💻 🚧

Ross Cadogan

💻

stffabi

💻 🚧

Austin

📖

David Gardner

👀 📖

Tanguy ⧓ Herrmann

💻

Rodrigo Damazio Bovendorp

💻 📖

Ryan Kuba

🚇

cnrmck

📖

Harry Walter

💻

Robotex

📖

Gerald Pape

📖

fomk

💻

Sven Gottwald

🚇

techknowlogick

💻

waja

📖

Scott Albertson

📖

Jason Huddleston

📖

Napster

💻

Maxim

💻 📖

Max Schmitt

📖

cron410

📖

Paulo Henrique

📖

Kaleb Elwert

📖

Bill Butler

📖

Mario Tacke

💻

Mark Woodbridge

💻

Simon Aronsson

💻 🚧 👀 📖

Ansem93

📖

Luka Peschke

💻 📖

Zois Pagoulatos

💻 👀 🚧

Alexandre Menif

💻

Andrey

📖

Armando Lüscher

📖

Ryan Budke

📖

Kaloyan Raev

💻 ⚠️

sixth

📖

Gina Häußge

💻

Max H.

💻

Jungkook Park

📖

Jan Kristof Nidzwetzki

📖

lukas

💻

Ameya Shenoy

💻

Raymon de Looff

💻

John Clayton

💻

Germs2004

📖

Lukas Willburger

💻

Oliver Cervera

📖

Victor Moura

⚠️ 💻 📖

Maximilian Brandau

💻 ⚠️

Andrew

📖

sixcorners

📖

nils måsén

📖 💻

Arne Jørgensen

⚠️ 👀

PatSki123

📖

Valentine Zavadsky

💻 📖 ⚠️

Alexander Voronin

💻 🐛

Oliver Mueller

📖

Sebastiaan Tammer

💻

miosame

📖

Andrew Metzger

🐛 💡

Pierre Grimaud

📖

Matt Doran

📖

MihailITPlace

💻

bugficks

💻 📖

Michael

💻

D. Domig

📖

Ben Osheroff

💻

David H.

💻

Chander Ganesan

📖

yrien30

💻

ksurl

📖 💻

rg9400

💻

Turtle Kalus

💻

Srihari Thalla

📖

Thomas Gaudin

📖

hydrargyrum

📖

Reinout van Rees

📖

DasSkelett

💻

zenjabba

📖

Dan Quan

📖

modem7

📖

Igor Zibarev

💻

Patrice

💻

James White

📖
nils måsén
nils måsén

💻 📖 🚧 👀
Simon Aronsson
Simon Aronsson

💻 📖 🚧 👀
James
James

⚠️ 🤔
Florian
Florian

👀 📖
Brian DeHamer
Brian DeHamer

💻 🚧
Ross Cadogan
Ross Cadogan

💻
stffabi
stffabi

💻 🚧
Austin
Austin

📖
David Gardner
David Gardner

👀 📖
Tanguy ⧓ Herrmann
Tanguy ⧓ Herrmann

💻
Rodrigo Damazio Bovendorp
Rodrigo Damazio Bovendorp

💻 📖
Ryan Kuba
Ryan Kuba

🚇
cnrmck
cnrmck

📖
Harry Walter
Harry Walter

💻
Robotex
Robotex

📖
Gerald Pape
Gerald Pape

📖
fomk
fomk

💻
Sven Gottwald
Sven Gottwald

🚇
techknowlogick
techknowlogick

💻
waja
waja

📖
Scott Albertson
Scott Albertson

📖
Jason Huddleston
Jason Huddleston

📖
Napster
Napster

💻
Maxim
Maxim

💻 📖
Max Schmitt
Max Schmitt

📖
cron410
cron410

📖
Paulo Henrique
Paulo Henrique

📖
Kaleb Elwert
Kaleb Elwert

📖
Bill Butler
Bill Butler

📖
Mario Tacke
Mario Tacke

💻
Mark Woodbridge
Mark Woodbridge

💻
Ansem93
Ansem93

📖
Luka Peschke
Luka Peschke

💻 📖
Zois Pagoulatos
Zois Pagoulatos

💻 👀 🚧
Alexandre Menif
Alexandre Menif

💻
Andrey
Andrey

📖
Armando Lüscher
Armando Lüscher

📖
Ryan Budke
Ryan Budke

📖
Kaloyan Raev
Kaloyan Raev

💻 ⚠️
sixth
sixth

📖
Gina Häußge
Gina Häußge

💻
Max H.
Max H.

💻
Jungkook Park
Jungkook Park

📖
Jan Kristof Nidzwetzki
Jan Kristof Nidzwetzki

📖
lukas
lukas

💻
Ameya Shenoy
Ameya Shenoy

💻
Raymon de Looff
Raymon de Looff

💻
John Clayton
John Clayton

💻
Germs2004
Germs2004

📖
Lukas Willburger
Lukas Willburger

💻
Oliver Cervera
Oliver Cervera

📖
Victor Moura
Victor Moura

⚠️ 💻 📖
Maximilian Brandau
Maximilian Brandau

💻 ⚠️
Andrew
Andrew

📖
sixcorners
sixcorners

📖
Arne Jørgensen
Arne Jørgensen

⚠️ 👀
PatSki123
PatSki123

📖
Valentine Zavadsky
Valentine Zavadsky

💻 📖 ⚠️
Alexander Voronin
Alexander Voronin

💻 🐛
Oliver Mueller
Oliver Mueller

📖
Sebastiaan Tammer
Sebastiaan Tammer

💻
miosame
miosame

📖
Andrew Metzger
Andrew Metzger

🐛 💡
Pierre Grimaud
Pierre Grimaud

📖
Matt Doran
Matt Doran

📖
MihailITPlace
MihailITPlace

💻
bugficks
bugficks

💻 📖
Michael
Michael

💻
D. Domig
D. Domig

📖
Ben Osheroff
Ben Osheroff

💻
David H.
David H.

💻
Chander Ganesan
Chander Ganesan

📖
yrien30
yrien30

💻
ksurl
ksurl

📖 💻 🚇
rg9400
rg9400

💻
Turtle Kalus
Turtle Kalus

💻
Srihari Thalla
Srihari Thalla

📖
Thomas Gaudin
Thomas Gaudin

📖
hydrargyrum
hydrargyrum

📖
Reinout van Rees
Reinout van Rees

📖
DasSkelett
DasSkelett

💻
zenjabba
zenjabba

📖
Dan Quan
Dan Quan

📖
modem7
modem7

📖
Igor Zibarev
Igor Zibarev

💻
Patrice
Patrice

💻
James White
James White

📖
Dirk Kok
Dirk Kok

💻
EDIflyer
EDIflyer

📖
Jauder Ho
Jauder Ho

💻
Tamal Das
Tamal Das

📖
guangwu
guangwu

📖
Florian Hübner
Florian Hübner

📖 💻

Andrii Bratanin

📖
diff --git a/build.sh b/build.sh index 304786d..78b1bfc 100755 --- a/build.sh +++ b/build.sh @@ -1,5 +1,9 @@ #!/bin/bash +BINFILE=watchtower +if [ -n "$MSYSTEM" ]; then + BINFILE=watchtower.exe +fi VERSION=$(git describe --tags) echo "Building $VERSION..." -go build -o watchtower -ldflags "-X github.com/containrrr/watchtower/internal/meta.Version=$VERSION" +go build -o $BINFILE -ldflags "-X github.com/containrrr/watchtower/internal/meta.Version=$VERSION" diff --git a/cmd/notify-upgrade.go b/cmd/notify-upgrade.go new file mode 100644 index 0000000..9991ee6 --- /dev/null +++ b/cmd/notify-upgrade.go @@ -0,0 +1,111 @@ +// Package cmd contains the watchtower (sub-)commands +package cmd + +import ( + "fmt" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/containrrr/watchtower/internal/flags" + "github.com/containrrr/watchtower/pkg/container" + "github.com/containrrr/watchtower/pkg/notifications" + "github.com/spf13/cobra" +) + +var notifyUpgradeCommand = NewNotifyUpgradeCommand() + +// NewNotifyUpgradeCommand creates the notify upgrade command for watchtower +func NewNotifyUpgradeCommand() *cobra.Command { + return &cobra.Command{ + Use: "notify-upgrade", + Short: "Upgrade legacy notification configuration to shoutrrr URLs", + Run: runNotifyUpgrade, + } +} + +func runNotifyUpgrade(cmd *cobra.Command, args []string) { + if err := runNotifyUpgradeE(cmd, args); err != nil { + logf("Notification upgrade failed: %v", err) + } +} + +func runNotifyUpgradeE(cmd *cobra.Command, _ []string) error { + f := cmd.Flags() + flags.ProcessFlagAliases(f) + + notifier = notifications.NewNotifier(cmd) + urls := notifier.GetURLs() + + logf("Found notification configurations for: %v", strings.Join(notifier.GetNames(), ", ")) + + outFile, err := os.CreateTemp("/", "watchtower-notif-urls-*") + if err != nil { + return fmt.Errorf("failed to create output file: %v", err) + } + logf("Writing notification URLs to %v", outFile.Name()) + logf("") + + sb := strings.Builder{} + sb.WriteString("WATCHTOWER_NOTIFICATION_URL=") + + for i, u := range urls { + if i != 0 { + sb.WriteRune(' ') + } + sb.WriteString(u) + } + + _, err = fmt.Fprint(outFile, sb.String()) + tryOrLog(err, "Failed to write to output file") + + tryOrLog(outFile.Sync(), "Failed to sync output file") + tryOrLog(outFile.Close(), "Failed to close output file") + + containerID := "" + cid, err := container.GetRunningContainerID() + tryOrLog(err, "Failed to get running container ID") + if cid != "" { + containerID = cid.ShortID() + } + logf("To get the environment file, use:") + logf("cp %v:%v ./watchtower-notifications.env", containerID, outFile.Name()) + logf("") + logf("Note: This file will be removed in 5 minutes or when this container is stopped!") + + signalChannel := make(chan os.Signal, 1) + time.AfterFunc(5*time.Minute, func() { + signalChannel <- syscall.SIGALRM + }) + + signal.Notify(signalChannel, os.Interrupt) + signal.Notify(signalChannel, syscall.SIGTERM) + + switch <-signalChannel { + case syscall.SIGALRM: + logf("Timed out!") + case os.Interrupt, syscall.SIGTERM: + logf("Stopping...") + default: + } + + if err := os.Remove(outFile.Name()); err != nil { + logf("Failed to remove file, it may still be present in the container image! Error: %v", err) + } else { + logf("Environment file has been removed.") + } + + return nil +} + +func tryOrLog(err error, message string) { + if err != nil { + logf("%v: %v\n", message, err) + } +} + +func logf(format string, v ...interface{}) { + fmt.Fprintln(os.Stderr, fmt.Sprintf(format, v...)) +} diff --git a/cmd/root.go b/cmd/root.go index 1be52a8..eef13ce 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,7 +1,7 @@ package cmd import ( - "github.com/containrrr/watchtower/internal/meta" + "errors" "math" "net/http" "os" @@ -11,12 +11,12 @@ import ( "syscall" "time" - apiMetrics "github.com/containrrr/watchtower/pkg/api/metrics" - "github.com/containrrr/watchtower/pkg/api/update" - "github.com/containrrr/watchtower/internal/actions" "github.com/containrrr/watchtower/internal/flags" + "github.com/containrrr/watchtower/internal/meta" "github.com/containrrr/watchtower/pkg/api" + apiMetrics "github.com/containrrr/watchtower/pkg/api/metrics" + "github.com/containrrr/watchtower/pkg/api/update" "github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/filters" "github.com/containrrr/watchtower/pkg/metrics" @@ -29,18 +29,20 @@ import ( ) var ( - client container.Client - scheduleSpec string - cleanup bool - noRestart bool - monitorOnly bool - enableLabel bool - notifier t.Notifier - timeout time.Duration - lifecycleHooks bool - rollingRestart bool - scope string - // Set on build using ldflags + client container.Client + scheduleSpec string + cleanup bool + noRestart bool + noPull bool + monitorOnly bool + enableLabel bool + disableContainers []string + notifier t.Notifier + timeout time.Duration + lifecycleHooks bool + rollingRestart bool + scope string + labelPrecedence bool ) var rootCmd = NewRootCommand() @@ -56,6 +58,7 @@ func NewRootCommand() *cobra.Command { `, Run: Run, PreRun: PreRun, + Args: cobra.ArbitraryArgs, } } @@ -68,6 +71,7 @@ func init() { // Execute the root func and exit in case of errors func Execute() { + rootCmd.AddCommand(notifyUpgradeCommand) if err := rootCmd.Execute(); err != nil { log.Fatal(err) } @@ -76,37 +80,12 @@ func Execute() { // PreRun is a lifecycle hook that runs before the command is executed. func PreRun(cmd *cobra.Command, _ []string) { f := cmd.PersistentFlags() - - if enabled, _ := f.GetBool("no-color"); enabled { - log.SetFormatter(&log.TextFormatter{ - DisableColors: true, - }) - } else { - // enable logrus built-in support for https://bixense.com/clicolors/ - log.SetFormatter(&log.TextFormatter{ - EnvironmentOverrideColors: true, - }) + flags.ProcessFlagAliases(f) + if err := flags.SetupLogging(f); err != nil { + log.Fatalf("Failed to initialize logging: %s", err.Error()) } - if enabled, _ := f.GetBool("debug"); enabled { - log.SetLevel(log.DebugLevel) - } - if enabled, _ := f.GetBool("trace"); enabled { - log.SetLevel(log.TraceLevel) - } - - pollingSet := f.Changed("interval") - schedule, _ := f.GetString("schedule") - cronLen := len(schedule) - - if pollingSet && cronLen > 0 { - log.Fatal("Only schedule or interval can be defined, not both.") - } else if cronLen > 0 { - scheduleSpec, _ = f.GetString("schedule") - } else { - interval, _ := f.GetInt("interval") - scheduleSpec = "@every " + strconv.Itoa(interval) + "s" - } + scheduleSpec, _ = f.GetString("schedule") flags.GetSecretsFromFiles(cmd) cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd) @@ -116,11 +95,15 @@ func PreRun(cmd *cobra.Command, _ []string) { } enableLabel, _ = f.GetBool("label-enable") + disableContainers, _ = f.GetStringSlice("disable-containers") lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks") rollingRestart, _ = f.GetBool("rolling-restart") scope, _ = f.GetString("scope") + labelPrecedence, _ = f.GetBool("label-take-precedence") - log.Debug(scope) + if scope != "" { + log.Debugf(`Using scope %q`, scope) + } // configure environment vars for client err := flags.EnvConfig(cmd) @@ -128,7 +111,7 @@ func PreRun(cmd *cobra.Command, _ []string) { log.Fatal(err) } - noPull, _ := f.GetBool("no-pull") + noPull, _ = f.GetBool("no-pull") includeStopped, _ := f.GetBool("include-stopped") includeRestarting, _ := f.GetBool("include-restarting") reviveStopped, _ := f.GetBool("revive-stopped") @@ -139,26 +122,36 @@ func PreRun(cmd *cobra.Command, _ []string) { log.Warn("Using `WATCHTOWER_NO_PULL` and `WATCHTOWER_MONITOR_ONLY` simultaneously might lead to no action being taken at all. If this is intentional, you may safely ignore this message.") } - client = container.NewClient( - !noPull, - includeStopped, - reviveStopped, - removeVolumes, - includeRestarting, - warnOnHeadPullFailed, - ) + client = container.NewClient(container.ClientOptions{ + IncludeStopped: includeStopped, + ReviveStopped: reviveStopped, + RemoveVolumes: removeVolumes, + IncludeRestarting: includeRestarting, + WarnOnHeadFailed: container.WarningStrategy(warnOnHeadPullFailed), + }) notifier = notifications.NewNotifier(cmd) + notifier.AddLogHook() } // Run is the main execution flow of the command func Run(c *cobra.Command, names []string) { - filter, filterDesc := filters.BuildFilter(names, enableLabel, scope) + filter, filterDesc := filters.BuildFilter(names, disableContainers, enableLabel, scope) runOnce, _ := c.PersistentFlags().GetBool("run-once") enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update") enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics") unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls") apiToken, _ := c.PersistentFlags().GetString("http-api-token") + healthCheck, _ := c.PersistentFlags().GetBool("health-check") + + if healthCheck { + // health check should not have pid 1 + if os.Getpid() == 1 { + time.Sleep(1 * time.Second) + log.Fatal("The health check flag should never be passed to the main watchtower container process") + } + os.Exit(0) + } if rollingRestart && monitorOnly { log.Fatal("Rolling restarts is not compatible with the global monitor only flag") @@ -189,9 +182,12 @@ func Run(c *cobra.Command, names []string) { httpAPI := api.New(apiToken) if enableUpdateAPI { - updateHandler := update.New(func() { runUpdatesWithNotifications(filter) }, updateLock) + updateHandler := update.New(func(images []string) { + metric := runUpdatesWithNotifications(filters.FilterByImage(images, filter)) + metrics.RegisterScan(metric) + }, updateLock) httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle) - // If polling isn't enabled the scheduler is never started and + // If polling isn't enabled the scheduler is never started, and // we need to trigger the startup messages manually. if !unblockHTTPAPI { writeStartupMessage(c, time.Time{}, filterDesc) @@ -203,7 +199,7 @@ func Run(c *cobra.Command, names []string) { httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle) } - if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && err != http.ErrServerClosed { + if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Error("failed to start API", err) } @@ -293,7 +289,7 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) { startupLog.Info("Scheduling first run: " + sched.Format("2006-01-02 15:04:05 -0700 MST")) startupLog.Info("Note that the first check will be performed in " + until) } else if runOnce, _ := c.PersistentFlags().GetBool("run-once"); runOnce { - startupLog.Info("Running a one time update.") + startupLog.Info("Running a one time update.") } else { startupLog.Info("Periodic runs are not enabled.") } @@ -363,13 +359,15 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string, func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric { notifier.StartNotification() updateParams := t.UpdateParams{ - Filter: filter, - Cleanup: cleanup, - NoRestart: noRestart, - Timeout: timeout, - MonitorOnly: monitorOnly, - LifecycleHooks: lifecycleHooks, - RollingRestart: rollingRestart, + Filter: filter, + Cleanup: cleanup, + NoRestart: noRestart, + Timeout: timeout, + MonitorOnly: monitorOnly, + LifecycleHooks: lifecycleHooks, + RollingRestart: rollingRestart, + LabelPrecedence: labelPrecedence, + NoPull: noPull, } result, err := actions.Update(client, updateParams) if err != nil { diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile index c3ed14b..2fc571d 100644 --- a/dockerfiles/Dockerfile +++ b/dockerfiles/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM alpine:3.15 as alpine +FROM --platform=$BUILDPLATFORM alpine:3.19.0 as alpine RUN apk add --no-cache \ ca-certificates \ @@ -17,4 +17,7 @@ COPY --from=alpine \ EXPOSE 8080 COPY watchtower / + +HEALTHCHECK CMD [ "/watchtower", "--health-check"] + ENTRYPOINT ["/watchtower"] diff --git a/dockerfiles/Dockerfile.dev-self-contained b/dockerfiles/Dockerfile.dev-self-contained index 79dbe39..1a39c26 100644 --- a/dockerfiles/Dockerfile.dev-self-contained +++ b/dockerfiles/Dockerfile.dev-self-contained @@ -7,6 +7,13 @@ FROM golang:alpine as builder # use version (for example "v0.3.3") or "main" ARG WATCHTOWER_VERSION=main +# Pre download required modules to avoid redownloading at each build thanks to docker layer caching. +# Copying go.mod and go.sum ensure to invalid the layer/build cache if there is a change in module requirement +WORKDIR /watchtower +COPY go.mod . +COPY go.sum . +RUN go mod download + RUN apk add --no-cache \ alpine-sdk \ ca-certificates \ @@ -35,4 +42,6 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certifi COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=builder /watchtower/watchtower /watchtower +HEALTHCHECK CMD [ "/watchtower", "--health-check"] + ENTRYPOINT ["/watchtower"] diff --git a/dockerfiles/Dockerfile.self-contained b/dockerfiles/Dockerfile.self-contained index 303fc53..04a6047 100644 --- a/dockerfiles/Dockerfile.self-contained +++ b/dockerfiles/Dockerfile.self-contained @@ -35,4 +35,6 @@ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certifi COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo COPY --from=builder /go/watchtower/watchtower /watchtower +HEALTHCHECK CMD [ "/watchtower", "--health-check"] + ENTRYPOINT ["/watchtower"] diff --git a/dockerfiles/container-networking/docker-compose.yml b/dockerfiles/container-networking/docker-compose.yml new file mode 100644 index 0000000..24cd00d --- /dev/null +++ b/dockerfiles/container-networking/docker-compose.yml @@ -0,0 +1,17 @@ +services: + producer: + image: qmcgaw/gluetun:v3.35.0 + cap_add: + - NET_ADMIN + environment: + - VPN_SERVICE_PROVIDER=${VPN_SERVICE_PROVIDER} + - OPENVPN_USER=${OPENVPN_USER} + - OPENVPN_PASSWORD=${OPENVPN_PASSWORD} + - SERVER_COUNTRIES=${SERVER_COUNTRIES} + consumer: + depends_on: + - producer + image: nginx:1.25.1 + network_mode: "service:producer" + labels: + - "com.centurylinklabs.watchtower.depends-on=/wt-contnet-producer-1" diff --git a/docs-requirements.txt b/docs-requirements.txt new file mode 100644 index 0000000..3a0104e --- /dev/null +++ b/docs-requirements.txt @@ -0,0 +1,3 @@ +mkdocs +mkdocs-material +md-toc diff --git a/docs/arguments.md b/docs/arguments.md index 7280631..d7ed0b0 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -27,6 +27,33 @@ In the example above, watchtower will execute an upgrade attempt on the containe When no arguments are specified, watchtower will monitor all running containers. +## Secrets/Files + +Some arguments can also reference a file, in which case the contents of the file are used as the value. +This can be used to avoid putting secrets in the configuration file or command line. + +The following arguments are currently supported (including their corresponding `WATCHTOWER_` environment variables): + - `notification-url` + - `notification-email-server-password` + - `notification-slack-hook-url` + - `notification-msteams-hook` + - `notification-gotify-token` + - `http-api-token` + +### Example docker-compose usage +```yaml +secrets: + access_token: + file: access_token + +services: + watchtower: + secrets: + - access_token + environment: + - WATCHTOWER_HTTP_API_TOKEN=/run/secrets/access_token +``` + ## Help Shows documentation about the supported flags. @@ -58,8 +85,8 @@ Environment Variable: WATCHTOWER_CLEANUP Default: false ``` -## Remove attached volumes -Removes attached volumes after updating. When this flag is specified, watchtower will remove all attached volumes from the container before restarting with a new image. Use this option to force new volumes to be populated as containers are updated. +## Remove anonymous volumes +Removes anonymous volumes after updating. When this flag is specified, watchtower will remove all anonymous volumes from the container before restarting with a new image. Named volumes will not be removed! ```text Argument: --remove-volumes @@ -71,6 +98,10 @@ Environment Variable: WATCHTOWER_REMOVE_VOLUMES ## Debug Enable debug mode with verbose logging. +!!! note "Notes" + Alias for `--log-level debug`. See [Maximum log level](#maximum-log-level). + Does _not_ take an argument when used as an argument. Using `--debug true` will **not** work. + ```text Argument: --debug, -d Environment Variable: WATCHTOWER_DEBUG @@ -81,6 +112,10 @@ Environment Variable: WATCHTOWER_DEBUG ## Trace Enable trace mode with very verbose logging. Caution: exposes credentials! +!!! note "Notes" + Alias for `--log-level trace`. See [Maximum log level](#maximum-log-level). + Does _not_ take an argument when used as an argument. Using `--trace true` will **not** work. + ```text Argument: --trace Environment Variable: WATCHTOWER_TRACE @@ -88,6 +123,28 @@ Environment Variable: WATCHTOWER_TRACE Default: false ``` +## Maximum log level + +The maximum log level that will be written to STDERR (shown in `docker log` when used in a container). + +```text + Argument: --log-level +Environment Variable: WATCHTOWER_LOG_LEVEL + Possible values: panic, fatal, error, warn, info, debug or trace + Default: info +``` + +## Logging format + +Sets what logging format to use for console output. + +```text + Argument: --log-format, -l +Environment Variable: WATCHTOWER_LOG_FORMAT + Possible values: Auto, LogFmt, Pretty or JSON + Default: Auto +``` + ## ANSI colors Disable ANSI color escape codes in log output. @@ -132,7 +189,7 @@ Environment Variable: WATCHTOWER_INCLUDE_RESTARTING Will also include created and exited containers. ```text - Argument: --include-stopped + Argument: --include-stopped, -S Environment Variable: WATCHTOWER_INCLUDE_STOPPED Type: Boolean Default: false @@ -159,7 +216,7 @@ Environment Variable: WATCHTOWER_POLL_INTERVAL ``` ## Filter by enable label -Update containers that have a `com.centurylinklabs.watchtower.enable` label set to true. +Monitor and update containers that have a `com.centurylinklabs.watchtower.enable` label set to true. ```text Argument: --label-enable @@ -169,10 +226,23 @@ Environment Variable: WATCHTOWER_LABEL_ENABLE ``` ## Filter by disable label -__Do not__ update containers that have `com.centurylinklabs.watchtower.enable` label set to false and +__Do not__ Monitor and update containers that have `com.centurylinklabs.watchtower.enable` label set to false and no `--label-enable` argument is passed. Note that only one or the other (targeting by enable label) can be used at the same time to target containers. +## Filter by disabling specific container names +Monitor and update containers whose names are not in a given set of names. + +This can be used to exclude specific containers, when setting labels is not an option. +The listed containers will be excluded even if they have the enable filter set to true. + +```text + Argument: --disable-containers, -x +Environment Variable: WATCHTOWER_DISABLE_CONTAINERS + Type: Comma- or space-separated string list + Default: "" +``` + ## Without updating containers Will only monitor for new images, send notifications and invoke the [pre-check/post-check hooks](https://containrrr.dev/watchtower/lifecycle-hooks/), but will __not__ update the @@ -192,6 +262,19 @@ Environment Variable: WATCHTOWER_MONITOR_ONLY Note that monitor-only can also be specified on a per-container basis with the `com.centurylinklabs.watchtower.monitor-only` label set on those containers. +See [With label taking precedence over arguments](#With-label-taking-precedence-over-arguments) for behavior when both argument and label are set + +## With label taking precedence over arguments + +By default, arguments will take precedence over labels. This means that if you set `WATCHTOWER_MONITOR_ONLY` to true or use `--monitor-only`, a container with `com.centurylinklabs.watchtower.monitor-only` set to false will not be updated. If you set `WATCHTOWER_LABEL_TAKE_PRECEDENCE` to true or use `--label-take-precedence`, then the container will also be updated. This also apply to the no pull option. if you set `WATCHTOWER_NO_PULL` to true or use `--no-pull`, a container with `com.centurylinklabs.watchtower.no-pull` set to false will not pull the new image. If you set `WATCHTOWER_LABEL_TAKE_PRECEDENCE` to true or use `--label-take-precedence`, then the container will pull image + +```text + Argument: --label-take-precedence +Environment Variable: WATCHTOWER_LABEL_TAKE_PRECEDENCE + Type: Boolean + Default: false +``` + ## Without restarting containers Do not restart containers after updating. This option can be useful when the start of the containers is managed by an external system such as systemd. @@ -215,6 +298,11 @@ Environment Variable: WATCHTOWER_NO_PULL Default: false ``` +Note that no-pull can also be specified on a per-container basis with the +`com.centurylinklabs.watchtower.no-pull` label set on those containers. + +See [With label taking precedence over arguments](#With-label-taking-precedence-over-arguments) for behavior when both argument and label are set + ## Without sending a startup message Do not send a message after watchtower started. Otherwise there will be an info-level notification. @@ -229,7 +317,7 @@ Environment Variable: WATCHTOWER_NO_STARTUP_MESSAGE Run an update attempt against a container name list one time immediately and exit. ```text - Argument: --run-once + Argument: --run-once, -R Environment Variable: WATCHTOWER_RUN_ONCE Type: Boolean Default: false @@ -248,6 +336,7 @@ Environment Variable: WATCHTOWER_HTTP_API_UPDATE ## HTTP API Token Sets an authentication token to HTTP API requests. +Can also reference a file, in which case the contents of the file are used. ```text Argument: --http-api-token @@ -270,6 +359,11 @@ Environment Variable: WATCHTOWER_HTTP_API_PERIODIC_POLLS Update containers that have a `com.centurylinklabs.watchtower.scope` label set with the same value as the given argument. This enables [running multiple instances](https://containrrr.dev/watchtower/running-multiple-instances). +!!! note "Filter by lack of scope" + If you want other instances of watchtower to ignore the scoped containers, set this argument to `none`. + When omitted, watchtower will update all containers regardless of scope. + + ```text Argument: --scope Environment Variable: WATCHTOWER_SCOPE @@ -342,3 +436,32 @@ Environment Variable: WATCHTOWER_WARN_ON_HEAD_FAILURE Possible values: always, auto, never Default: auto ``` + +## Health check + +Returns a success exit code to enable usage with docker `HEALTHCHECK`. This check is naive and only returns checks whether there is another process running inside the container, as it is the only known form of failure state for watchtowers container. + +!!! note "Only for HEALTHCHECK use" + Never put this on the main container executable command line as it is only meant to be run from docker HEALTHCHECK. + +```text + Argument: --health-check +``` + +## Programatic Output (porcelain) + +Writes the session results to STDOUT using a stable, machine-readable format (indicated by the argument VERSION). + +Alias for: + +```text + --notification-url logger:// + --notification-log-stdout + --notification-report + --notification-template porcelain.VERSION.summary-no-log + + Argument: --porcelain, -P +Environment Variable: WATCHTOWER_PORCELAIN + Possible values: v1 + Default: - +``` diff --git a/docs/container-selection.md b/docs/container-selection.md index b5ccafb..8327c66 100644 --- a/docs/container-selection.md +++ b/docs/container-selection.md @@ -7,33 +7,58 @@ There are two options: ## Full Exclude -If you need to exclude some containers, set the _com.centurylinklabs.watchtower.enable_ label to `false`. +If you need to exclude some containers, set the _com.centurylinklabs.watchtower.enable_ label to `false`. For clarity this should be set **on the container(s)** you wish to be ignored, this is not set on watchtower. -```docker -LABEL com.centurylinklabs.watchtower.enable="false" -``` +=== "dockerfile" -Or, it can be specified as part of the `docker run` command line: + ```docker + LABEL com.centurylinklabs.watchtower.enable="false" + ``` +=== "docker run" -```bash -docker run -d --label=com.centurylinklabs.watchtower.enable=false someimage -``` + ```bash + docker run -d --label=com.centurylinklabs.watchtower.enable=false someimage + ``` -If you need to [include only containers with the enable label](https://containrrr.github.io/watchtower/arguments/#filter_by_enable_label), pass the `--label-enable` flag or the `WATCHTOWER_LABEL_ENABLE` environment variable on startup and set the _com.centurylinklabs.watchtower.enable_ label with a value of `true` for the containers you want to watch. +=== "docker-compose" -```docker -LABEL com.centurylinklabs.watchtower.enable="true" -``` + ``` yaml + version: "3" + services: + someimage: + container_name: someimage + labels: + - "com.centurylinklabs.watchtower.enable=false" + ``` -Or, it can be specified as part of the `docker run` command line: +If instead you want to [only include containers with the enable label](https://containrrr.github.io/watchtower/arguments/#filter_by_enable_label), pass the `--label-enable` flag or the `WATCHTOWER_LABEL_ENABLE` environment variable on startup for watchtower and set the _com.centurylinklabs.watchtower.enable_ label with a value of `true` on the containers you want to watch. -```bash -docker run -d --label=com.centurylinklabs.watchtower.enable=true someimage -``` +=== "dockerfile" + + ```docker + LABEL com.centurylinklabs.watchtower.enable="true" + ``` +=== "docker run" + + ```bash + docker run -d --label=com.centurylinklabs.watchtower.enable=true someimage + ``` + +=== "docker-compose" + + ``` yaml + version: "3" + services: + someimage: + container_name: someimage + labels: + - "com.centurylinklabs.watchtower.enable=true" + ``` If you wish to create a monitoring scope, you will need to [run multiple instances and set a scope for each of them](https://containrrr.github.io/watchtower/running-multiple-instances). Watchtower filters running containers by testing them against each configured criteria. A container is monitored if all criteria are met. For example: + - If a container's name is on the monitoring name list (not empty `--name` argument) but it is not enabled (_centurylinklabs.watchtower.enable=false_), it won't be monitored; - If a container's name is not on the monitoring name list (not empty `--name` argument), even if it is enabled (_centurylinklabs.watchtower.enable=true_ and `--label-enable` flag is set), it won't be monitored; diff --git a/docs/http-api-mode.md b/docs/http-api-mode.md index 2cf082a..69812bb 100644 --- a/docs/http-api-mode.md +++ b/docs/http-api-mode.md @@ -35,3 +35,11 @@ Notice that there is an environment variable named WATCHTOWER_HTTP_API_TOKEN. To ```bash curl -H "Authorization: Bearer mytoken" localhost:8080/v1/update ``` + +--- + +In order to update only certain images, the image names can be provided as URL query parameters. The following `curl` command would trigger an update for the images `foo/bar` and `foo/baz`: + +```bash +curl -H "Authorization: Bearer mytoken" localhost:8080/v1/update?image=foo/bar,foo/baz +``` diff --git a/docs/introduction.md b/docs/introduction.md index ded074f..cbbc3a3 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -11,5 +11,5 @@ CONTAINER ID IMAGE STATUS PORTS 6cc4d2a9d1a5 containrrr/watchtower Up 15 minutes watchtower ``` -Every few minutes watchtower will pull the latest _centurylink/wetty-cli_ image and compare it to the one that was used to run the "wetty" container. If it sees that the image has changed it will stop/remove the "wetty" container and then restart it using the new image and the same `docker run` options that were used to start the container initially (in this case, that would include the `-p 8080:3000` port mapping). +Every day watchtower will pull the latest _centurylink/wetty-cli_ image and compare it to the one that was used to run the "wetty" container. If it sees that the image has changed it will stop/remove the "wetty" container and then restart it using the new image and the same `docker run` options that were used to start the container initially (in this case, that would include the `-p 8080:3000` port mapping). diff --git a/docs/linked-containers.md b/docs/linked-containers.md index 240fb97..c7e9be8 100644 --- a/docs/linked-containers.md +++ b/docs/linked-containers.md @@ -2,4 +2,6 @@ Watchtower will detect if there are links between any of the running containers For example, imagine you were running a _mysql_ container and a _wordpress_ container which had been linked to the _mysql_ container. If watchtower were to detect that the _mysql_ container required an update, it would first shut down the linked _wordpress_ container followed by the _mysql_ container. When restarting the containers it would handle _mysql_ first and then _wordpress_ to ensure that the link continued to work. -If you want to override existing links you can use special `com.centurylinklabs.watchtower.depends-on` label with dependent container names, separated by a comma. +If you want to override existing links, or if you are not using links, you can use special `com.centurylinklabs.watchtower.depends-on` label with dependent container names, separated by a comma. + +When you have a depending container that is using `network_mode: service:container` then watchtower will treat that container as an implicit link. diff --git a/docs/metrics.md b/docs/metrics.md index 7bb6383..480d7c6 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -4,7 +4,7 @@ Metrics can be used to track how Watchtower behaves over time. -To use this feature, you have to set an [API token](arguments.md#http-api-token) and [enable the metrics API](arguments.md#http-api-metrics), +To use this feature, you have to set an [API token](arguments.md#http_api_token) and [enable the metrics API](arguments.md#http_api_metrics), as well as creating a port mapping for your container for port `8080`. The metrics API endpoint is `/v1/metrics`. diff --git a/docs/notifications.md b/docs/notifications.md index faa4e4a..d5da4fe 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -1,15 +1,7 @@ # Notifications Watchtower can send notifications when containers are updated. Notifications are sent via hooks in the logging -system, [logrus](http://github.com/sirupsen/logrus). The types of notifications to send are set by passing a -comma-separated list of values to the `--notifications` option -(or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values: - -- `email` to send notifications via e-mail -- `slack` to send notifications through a Slack webhook -- `msteams` to send notifications via MSTeams webhook -- `gotify` to send notifications via Gotify -- `shoutrrr` to send notifications via [containrrr/shoutrrr](https://github.com/containrrr/shoutrrr) +system, [logrus](http://github.com/sirupsen/logrus). !!! note "Using multiple notifications with environment variables" There is currently a bug in Viper (https://github.com/spf13/viper/issues/380), which prevents comma-separated slices to @@ -26,9 +18,231 @@ comma-separated list of values to the `--notifications` option - `--notifications-level` (env. `WATCHTOWER_NOTIFICATIONS_LEVEL`): Controls the log level which is used for the notifications. If omitted, the default log level is `info`. Possible values are: `panic`, `fatal`, `error`, `warn`, `info`, `debug` or `trace`. - `--notifications-hostname` (env. `WATCHTOWER_NOTIFICATIONS_HOSTNAME`): Custom hostname specified in subject/title. Useful to override the operating system hostname. +- `--notifications-delay` (env. `WATCHTOWER_NOTIFICATIONS_DELAY`): Delay before sending notifications expressed in seconds. - Watchtower will post a notification every time it is started. This behavior [can be changed](https://containrrr.github.io/watchtower/arguments/#without_sending_a_startup_message) with an argument. +- `--notification-title-tag` (env. `WATCHTOWER_NOTIFICATION_TITLE_TAG`): Prefix to include in the title. Useful when running multiple watchtowers. +- `--notification-skip-title` (env. `WATCHTOWER_NOTIFICATION_SKIP_TITLE`): Do not pass the title param to notifications. This will not pass a dynamic title override to notification services. If no title is configured for the service, it will remove the title all together. +- `--notification-log-stdout` (env. `WATCHTOWER_NOTIFICATION_LOG_STDOUT`): Enable output from `logger://` shoutrrr service to stdout. -## Available services +## [Shoutrrr](https://github.com/containrrr/shoutrrr) notifications + +To send notifications via shoutrrr, the following command-line options, or their corresponding environment variables, can be set: + +- `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. This option can also reference a file, in which case the contents of the file are used. + + +Go to [containrrr.dev/shoutrrr/v0.8/services/overview](https://containrrr.dev/shoutrrr/v0.8/services/overview) to +learn more about the different service URLs you can use. You can define multiple services by space separating the +URLs. (See example below) + +You can customize the message posted by setting a template. + +- `--notification-template` (env. `WATCHTOWER_NOTIFICATION_TEMPLATE`): The template used for the message. + +The template is a Go [template](https://golang.org/pkg/text/template/) that either format a list +of [log entries](https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry) or a `notification.Data` struct. + +Simple templates are used unless the `notification-report` flag is specified: + +- `--notification-report` (env. `WATCHTOWER_NOTIFICATION_REPORT`): Use the session report as the notification template data. + +## Simple templates + +The default value if not set is `{{range .}}{{.Message}}{{println}}{{end}}`. The example below uses a template that also +outputs timestamp and log level. + +!!! tip "Custom date format" + If you want to adjust the date/time format it must show how the + [reference time](https://golang.org/pkg/time/#pkg-constants) (_Mon Jan 2 15:04:05 MST 2006_) would be displayed in your + custom format. + i.e., The day of the year has to be 1, the month has to be 2 (february), the hour 3 (or 15 for 24h time) etc. + +!!! note "Skipping notifications" + To skip sending notifications that do not contain any information, you can wrap your template with `{{if .}}` and `{{end}}`. + + +Example: + +```bash +docker run -d \ + --name watchtower \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \ + -e WATCHTOWER_NOTIFICATION_TEMPLATE="{{range .}}{{.Time.Format \"2006-01-02 15:04:05\"}} ({{.Level}}): {{.Message}}{{println}}{{end}}" \ + containrrr/watchtower +``` + +## Report templates + +The default template for report notifications are the following: +```go +{{- if .Report -}} + {{- with .Report -}} + {{- if ( or .Updated .Failed ) -}} +{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed + {{- range .Updated}} +- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}} + {{- end -}} + {{- range .Fresh}} +- {{.Name}} ({{.ImageName}}): {{.State}} + {{- end -}} + {{- range .Skipped}} +- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- range .Failed}} +- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- else -}} + {{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}} +{{- end -}} +``` + +It will be used to send a summary of every session if there are any containers that were updated or which failed to update. + +!!! note "Skipping notifications" + Whenever the result of applying the template results in an empty string, no notifications will + be sent. This is by default used to limit the notifications to only be sent when there something noteworthy occurred. + + You can replace `{{- if ( or .Updated .Failed ) -}}` with any logic you want to decide when to send the notifications. + +Example using a custom report template that always sends a session report after each run: + +=== "docker run" + + ```bash + docker run -d \ + --name watchtower \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e WATCHTOWER_NOTIFICATION_REPORT="true" \ + -e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \ + -e WATCHTOWER_NOTIFICATION_TEMPLATE=" + {{- if .Report -}} + {{- with .Report -}} + {{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed + {{- range .Updated}} + - {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}} + {{- end -}} + {{- range .Fresh}} + - {{.Name}} ({{.ImageName}}): {{.State}} + {{- end -}} + {{- range .Skipped}} + - {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- range .Failed}} + - {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- end -}} + {{- else -}} + {{range .Entries -}}{{.Message}}{{\"\n\"}}{{- end -}} + {{- end -}} + " \ + containrrr/watchtower + ``` + +=== "docker-compose" + + ``` yaml + version: "3" + services: + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + env: + WATCHTOWER_NOTIFICATION_REPORT: "true" + WATCHTOWER_NOTIFICATION_URL: > + discord://token@channel + slack://watchtower@token-a/token-b/token-c + WATCHTOWER_NOTIFICATION_TEMPLATE: | + {{- if .Report -}} + {{- with .Report -}} + {{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed + {{- range .Updated}} + - {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}} + {{- end -}} + {{- range .Fresh}} + - {{.Name}} ({{.ImageName}}): {{.State}} + {{- end -}} + {{- range .Skipped}} + - {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- range .Failed}} + - {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- end -}} + {{- else -}} + {{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}} + {{- end -}} + ``` + +## Legacy notifications + +For backwards compatibility, the notifications can also be configured using legacy notification options. These will automatically be converted to shoutrrr URLs when used. +The types of notifications to send are set by passing a comma-separated list of values to the `--notifications` option +(or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values: + +- `email` to send notifications via e-mail +- `slack` to send notifications through a Slack webhook +- `msteams` to send notifications via MSTeams webhook +- `gotify` to send notifications via Gotify + +### `notify-upgrade` +If watchtower is started with `notify-upgrade` as it's first argument, it will generate a .env file with your current legacy notification options converted to shoutrrr URLs. + +=== "docker run" + + ```bash + $ docker run -d \ + --name watchtower \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -e WATCHTOWER_NOTIFICATIONS=slack \ + -e WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL="https://hooks.slack.com/services/xxx/yyyyyyyyyyyyyyy" \ + containrrr/watchtower \ + notify-upgrade + ``` + +=== "docker-compose.yml" + + ```yaml + version: "3" + services: + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + env: + WATCHTOWER_NOTIFICATIONS: slack + WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL: https://hooks.slack.com/services/xxx/yyyyyyyyyyyyyyy + command: notify-upgrade + ``` + + +You can then copy this file from the container (a message with the full command to do so will be logged) and use it with your current setup: + +=== "docker run" + + ```bash + $ docker run -d \ + --name watchtower \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --env-file watchtower-notifications.env \ + containrrr/watchtower + ``` + +=== "docker-compose.yml" + + ```yaml + version: "3" + services: + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + env_file: + - watchtower-notifications.env + ``` ### Email @@ -42,7 +256,7 @@ To receive notifications by email, the following command-line options, or their - `--notification-email-server-user` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER`): The username to authenticate with the SMTP server with. - `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with. Can also reference a file, in which case the contents of the file are used. - `--notification-email-delay` (env. `WATCHTOWER_NOTIFICATION_EMAIL_DELAY`): Delay before sending notifications expressed in seconds. -- `--notification-email-subjecttag` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG`): Prefix to include in the subject tag. Useful when running multiple watchtowers. +- `--notification-email-subjecttag` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG`): Prefix to include in the subject tag. Useful when running multiple watchtowers. **NOTE:** This will affect all notification types. Example: @@ -174,40 +388,3 @@ docker run -d \ If you want to disable TLS verification for the Gotify instance, you can use either `-e WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY=true` or `--notification-gotify-tls-skip-verify`. -### [containrrr/shoutrrr](https://github.com/containrrr/shoutrrr) - -To send notifications via shoutrrr, the following command-line options, or their corresponding environment variables, can be set: - -- `--notification-url` (env. `WATCHTOWER_NOTIFICATION_URL`): The shoutrrr service URL to be used. - -Go to [containrrr.dev/shoutrrr/v0.5/services/overview](https://containrrr.dev/shoutrrr/v0.5/services/overview) to -learn more about the different service URLs you can use. You can define multiple services by space separating the -URLs. (See example below) - -You can customize the message posted by setting a template. - -- `--notification-template` (env. `WATCHTOWER_NOTIFICATION_TEMPLATE`): The template used for the message. - -The template is a Go [template](https://golang.org/pkg/text/template/) and that format a list -of [log entries](https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry). - -The default value if not set is `{{range .}}{{.Message}}{{println}}{{end}}`. The example below uses a template that also -outputs timestamp and log level. - -!!! tip "Custom date format" - If you want to adjust the date/time format it must show how the - [reference time](https://golang.org/pkg/time/#pkg-constants) (_Mon Jan 2 15:04:05 MST 2006_) would be displayed in your - custom format. - i.e., The day of the year has to be 1, the month has to be 2 (february), the hour 3 (or 15 for 24h time) etc. - -Example: - -```bash -docker run -d \ - --name watchtower \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -e WATCHTOWER_NOTIFICATIONS=shoutrrr \ - -e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \ - -e WATCHTOWER_NOTIFICATION_TEMPLATE="{{range .}}{{.Time.Format \"2006-01-02 15:04:05\"}} ({{.Level}}): {{.Message}}{{println}}{{end}}" \ - containrrr/watchtower -``` diff --git a/docs/private-registries.md b/docs/private-registries.md index 94f9a81..5367a8c 100644 --- a/docs/private-registries.md +++ b/docs/private-registries.md @@ -23,19 +23,29 @@ password `auth` string: ``` `` needs to be replaced by the name of your private registry -(e.g., `my-private-registry.example.org`) +(e.g., `my-private-registry.example.org`). -!!! important "Using private images on docker hub" - When using private images on docker hub, the containers beeing watched needs to use the full image name, including the repository prefix `index.docker.io`. - So instead of - ``` - docker run -d myuser/myimage - ``` - you would run it as - ``` - docker run -d index.docker.io/myuser/myimage - ``` +!!! info "Using private images on Docker Hub" + To access private repositories on Docker Hub, + `` should be `https://index.docker.io/v1/`. + In this special case, the registry domain does not have to be specified + in `docker run` or `docker-compose`. Like Docker, Watchtower will use the + Docker Hub registry and its credentials when no registry domain is specified. + + Watchtower will recognize credentials with `` `index.docker.io`, + but the Docker CLI will not. +!!! important "Using a private registry on a local host" + To use a private registry hosted locally, make sure to correctly specify the registry host + in both `config.json` and the `docker run` command or `docker-compose` file. + Valid hosts are `localhost[:PORT]`, `HOST:PORT`, + or any multi-part `domain.name` or IP-address with or without a port. + + Examples: + * `localhost` -> `localhost/myimage` + * `127.0.0.1` -> `127.0.0.1/myimage:mytag` + * `host.domain` -> `host.domain/myorganization/myimage` + * `other-lan-host:80` -> `other-lan-host:80/imagename:latest` The required `auth` string can be generated as follows: @@ -75,7 +85,7 @@ When creating the watchtower container via docker-compose, use the following lin version: "3.4" services: watchtower: - image: index.docker.io/containrrr/watchtower:latest + image: containrrr/watchtower:latest volumes: - /var/run/docker.sock:/var/run/docker.sock - /.docker/config.json:/config.json @@ -114,7 +124,7 @@ in a volume that may be mounted onto your watchtower container. 1. Create the Dockerfile (contents below): ```Dockerfile - FROM golang:1.16 + FROM golang:1.20 ENV GO111MODULE off ENV CGO_ENABLED 0 @@ -145,7 +155,7 @@ in a volume that may be mounted onto your watchtower container. ``` 3. Create a configuration file for docker, and store it in $HOME/.docker/config.json (replace the - placeholders with your AWS Account ID): + placeholders with your AWS Account ID and with your AWS ECR Region): ```json { "credsStore" : "ecr-login", @@ -153,10 +163,10 @@ in a volume that may be mounted onto your watchtower container. "User-Agent" : "Docker-Client/19.03.1 (XXXXXX)" }, "auths" : { - ".dkr.ecr.us-west-1.amazonaws.com" : {} + ".dkr.ecr..amazonaws.com" : {} }, "credHelpers": { - ".dkr.ecr.us-west-1.amazonaws.com" : "ecr-login" + ".dkr.ecr..amazonaws.com" : "ecr-login" } } ``` diff --git a/docs/running-multiple-instances.md b/docs/running-multiple-instances.md index 3899095..5a82c80 100644 --- a/docs/running-multiple-instances.md +++ b/docs/running-multiple-instances.md @@ -1,10 +1,11 @@ By default, Watchtower will clean up other instances and won't allow multiple instances running on the same Docker host or swarm. It is possible to override this behavior by defining a [scope](https://containrrr.github.io/watchtower/arguments/#filter_by_scope) to each running instance. -Notice that: -- Multiple instances can't run with the same scope; -- An instance without a scope will clean up other running instances, even if they have a defined scope; +!!! note + - Multiple instances can't run with the same scope; + - An instance without a scope will clean up other running instances, even if they have a defined scope; + - Supplying `none` as the scope will treat `com.centurylinklabs.watchtower.scope=none`, `com.centurylinklabs.watchtower.scope=` and the lack of a `com.centurylinklabs.watchtower.scope` label as the scope `none`. This effectly enables you to run both scoped and unscoped watchtower instances on the same machine. -To define an instance monitoring scope, use the `--scope` argument or the `WATCHTOWER_SCOPE` environment variable on startup and set the _com.centurylinklabs.watchtower.scope_ label with the same value for the containers you want to include in this instance's scope (including the instance itself). +To define an instance monitoring scope, use the `--scope` argument or the `WATCHTOWER_SCOPE` environment variable on startup and set the `com.centurylinklabs.watchtower.scope` label with the same value for the containers you want to include in this instance's scope (including the instance itself). For example, in a Docker Compose config file: @@ -12,16 +13,29 @@ For example, in a Docker Compose config file: version: '3' services: - app-monitored-by-watchtower: + app-with-scope: image: myapps/monitored-by-watchtower - labels: - - "com.centurylinklabs.watchtower.scope=myscope" + labels: [ "com.centurylinklabs.watchtower.scope=myscope" ] - watchtower: + scoped-watchtower: image: containrrr/watchtower - volumes: - - /var/run/docker.sock:/var/run/docker.sock + volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ] command: --interval 30 --scope myscope - labels: - - "com.centurylinklabs.watchtower.scope=myscope" + labels: [ "com.centurylinklabs.watchtower.scope=myscope" ] + + unscoped-app-a: + image: myapps/app-a + + unscoped-app-b: + image: myapps/app-b + labels: [ "com.centurylinklabs.watchtower.scope=none" ] + + unscoped-app-c: + image: myapps/app-b + labels: [ "com.centurylinklabs.watchtower.scope=" ] + + unscoped-watchtower: + image: containrrr/watchtower + volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ] + command: --interval 30 --scope none ``` diff --git a/docs/stylesheets/theme.css b/docs/stylesheets/theme.css index 34f507d..e552129 100644 --- a/docs/stylesheets/theme.css +++ b/docs/stylesheets/theme.css @@ -1,16 +1,87 @@ [data-md-color-scheme="containrrr"] { - --md-primary-fg-color: #406170; - --md-primary-fg-color--light:#acbfc7; - --md-primary-fg-color--dark: #003343; - --md-accent-fg-color: #003343; - --md-accent-fg-color--transparent: #00334310; + /* Primary and accent */ + --md-primary-fg-color: #406170; + --md-primary-fg-color--light:#acbfc7; + --md-primary-fg-color--dark: #003343; + --md-accent-fg-color: #003343; + --md-accent-fg-color--transparent: #00334310; + + /* Typeset overrides */ + --md-typeset-a-color: var(--md-primary-fg-color); +} + +[data-md-color-scheme="containrrr-dark"] { + --md-hue: 199; + + /* Primary and accent */ + --md-primary-fg-color: hsl(199deg 27% 35% / 100%); + --md-primary-fg-color--link: hsl(199deg 45% 65% / 100%); + --md-primary-fg-color--light: hsl(198deg 19% 73% / 100%); + --md-primary-fg-color--dark: hsl(194deg 100% 13% / 100%); + --md-accent-fg-color: hsl(194deg 45% 50% / 100%); + --md-accent-fg-color--transparent: hsl(194deg 45% 50% / 6.3%); + + /* Default */ + --md-default-fg-color: hsl(var(--md-hue) 75% 95% / 100%); + --md-default-fg-color--light: hsl(var(--md-hue) 75% 90% / 62%); + --md-default-fg-color--lighter: hsl(var(--md-hue) 75% 90% / 32%); + --md-default-fg-color--lightest: hsl(var(--md-hue) 75% 90% / 12%); + --md-default-bg-color: hsl(var(--md-hue) 15% 21% / 100%); + --md-default-bg-color--light: hsl(var(--md-hue) 15% 21% / 54%); + --md-default-bg-color--lighter: hsl(var(--md-hue) 15% 21% / 26%); + --md-default-bg-color--lightest: hsl(var(--md-hue) 15% 21% / 7%); + + /* Code */ + --md-code-fg-color: hsl(var(--md-hue) 18% 86% / 100%); + --md-code-bg-color: hsl(var(--md-hue) 15% 15% / 100%); + --md-code-hl-color: hsl(218deg 100% 63% / 15%); + --md-code-hl-number-color: hsl(346deg 74% 63% / 100%); + --md-code-hl-special-color: hsl(320deg 83% 66% / 100%); + --md-code-hl-function-color: hsl(271deg 57% 65% / 100%); + --md-code-hl-constant-color: hsl(230deg 62% 70% / 100%); + --md-code-hl-keyword-color: hsl(199deg 33% 64% / 100%); + --md-code-hl-string-color: hsl( 50deg 34% 74% / 100%); + --md-code-hl-name-color: var(--md-code-fg-color); + --md-code-hl-operator-color: var(--md-default-fg-color--light); + --md-code-hl-punctuation-color: var(--md-default-fg-color--light); + --md-code-hl-comment-color: var(--md-default-fg-color--light); + --md-code-hl-generic-color: var(--md-default-fg-color--light); + --md-code-hl-variable-color: hsl(241deg 22% 60% / 100%); + + /* Typeset */ + --md-typeset-color: var(--md-default-fg-color); + --md-typeset-a-color: var(--md-primary-fg-color--link); + --md-typeset-mark-color: hsl(218deg 100% 63% / 30%); + --md-typeset-kbd-color: hsl(var(--md-hue) 15% 94% / 12%); + --md-typeset-kbd-accent-color: hsl(var(--md-hue) 15% 94% / 20%); + --md-typeset-kbd-border-color: hsl(var(--md-hue) 15% 14% / 100%); + --md-typeset-table-color: hsl(var(--md-hue) 75% 95% / 12%); + + /* Admonition */ + --md-admonition-fg-color: var(--md-default-fg-color); + --md-admonition-bg-color: var(--md-default-bg-color); + + /* Footer */ + --md-footer-bg-color: hsl(var(--md-hue) 15% 12% / 87%); + --md-footer-bg-color--dark: hsl(var(--md-hue) 15% 10% / 100%); + + /* Shadows */ + --md-shadow-z1: + 0 0.2rem 0.50rem rgba(0 0 0 20%), + 0 0 0.05rem rgba(0 0 0 10%); + --md-shadow-z2: + 0 0.2rem 0.50rem rgba(0 0 0 30%), + 0 0 0.05rem rgba(0 0 0 25%); + --md-shadow-z3: + 0 0.2rem 0.50rem rgba(0 0 0 40%), + 0 0 0.05rem rgba(0 0 0 35%); } .md-header-nav__button.md-logo { - padding: 0; + padding: 0; } .md-header-nav__button.md-logo img { - width: 1.6rem; - height: 1.6rem; -} \ No newline at end of file + width: 1.6rem; + height: 1.6rem; +} diff --git a/docs/template-preview.md b/docs/template-preview.md new file mode 100644 index 0000000..3d99ce9 --- /dev/null +++ b/docs/template-preview.md @@ -0,0 +1,251 @@ + + + +
+
loading wasm...
+
+ +
+
+
+ + + + + + + + +
+
+ + + + + + +
+ +
+
+

+
+
+ \ No newline at end of file diff --git a/docs/usage-overview.md b/docs/usage-overview.md index 1462ba6..1cac352 100644 --- a/docs/usage-overview.md +++ b/docs/usage-overview.md @@ -27,12 +27,12 @@ docker run -d \ Also check out [this Stack Overflow answer](https://stackoverflow.com/a/30494145/7872793) for more options on how to pass environment variables. -Mounting the host's docker config file: +Alternatively if you 2FA authentication setup on Docker Hub then passing username and password will be insufficient. Instead you can run `docker login` to store your credentials in `$HOME/.docker/config.json` and then mount this config file to make it available to the Watchtower container: ```bash docker run -d \ --name watchtower \ - -v /home//.docker/config.json:/config.json \ + -v $HOME/.docker/config.json:/config.json \ -v /var/run/docker.sock:/var/run/docker.sock \ containrrr/watchtower container_to_watch --debug ``` @@ -48,14 +48,14 @@ docker run -d \ If you mount the config file as described above, be sure to also prepend the URL for the registry when starting up your watched image (you can omit the https://). Here is a complete docker-compose.yml file that starts up a docker container -from a private repo at Docker Hub and monitors it with watchtower. Note the command argument changing the interval to -30s rather than the default 24 hours. +from a private repo on the GitHub Registry and monitors it with watchtower. Note the command argument changing the interval +to 30s rather than the default 24 hours. ```yaml version: "3" services: cavo: - image: index.docker.io//: + image: ghcr.io//: ports: - "443:3443" - "80:3080" diff --git a/go.mod b/go.mod index b730648..6c20d11 100644 --- a/go.mod +++ b/go.mod @@ -1,31 +1,74 @@ module github.com/containrrr/watchtower -go 1.12 - -// Use non-vulnerable runc (until github.com/containerd/containerd v1.6.0 is stable) -replace github.com/opencontainers/runc => github.com/opencontainers/runc v1.0.3 +go 1.20 require ( - github.com/containerd/containerd v1.5.9 // indirect - github.com/containrrr/shoutrrr v0.5.2 - github.com/docker/cli v20.10.8+incompatible - github.com/docker/distribution v2.7.1+incompatible - github.com/docker/docker v20.10.8+incompatible - github.com/docker/docker-credential-helpers v0.6.1 // indirect + github.com/containrrr/shoutrrr v0.8.0 + github.com/distribution/reference v0.5.0 + github.com/docker/cli v24.0.7+incompatible + github.com/docker/docker v24.0.7+incompatible github.com/docker/go-connections v0.4.0 - github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22 // indirect - github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07 + github.com/onsi/ginkgo v1.16.5 + github.com/onsi/gomega v1.30.0 + github.com/prometheus/client_golang v1.18.0 + github.com/robfig/cron v1.2.0 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.8.0 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.18.2 + github.com/stretchr/testify v1.8.4 + golang.org/x/net v0.19.0 +) + +require github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + +require ( + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.4.17 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.6.1 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect - github.com/onsi/ginkgo v1.14.2 - github.com/onsi/gomega v1.10.3 - github.com/prometheus/client_golang v1.7.1 - github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 - github.com/sirupsen/logrus v1.8.1 - github.com/spf13/cobra v1.0.0 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.6.3 - github.com/stretchr/testify v1.6.1 - golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 - golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 + golang.org/x/time v0.5.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.0.3 // indirect ) diff --git a/go.sum b/go.sum index b09128a..cab338f 100644 --- a/go.sum +++ b/go.sum @@ -1,1077 +1,240 @@ -bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= -github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.4.17 h1:iT12IBVClFevaf8PuVyi3UmZOVh4OqnaLxDTW2O6j3w= github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= -github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= -github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= -github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= -github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= -github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= -github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg= -github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= -github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= -github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= -github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= -github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= -github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= -github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= -github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= -github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= -github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= -github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= -github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= -github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= -github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= -github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= -github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= -github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= -github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= -github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= -github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= -github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= -github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= -github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= -github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= -github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= -github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= -github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= -github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= -github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= -github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= -github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= -github.com/containerd/containerd v1.5.9 h1:rs6Xg1gtIxaeyG+Smsb/0xaSDu1VgFhOCKBXxMxbsF4= -github.com/containerd/containerd v1.5.9/go.mod h1:fvQqCfadDGga5HZyn3j4+dx56qj2I9YwBrlSdalvJYQ= -github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= -github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= -github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= -github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= -github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= -github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= -github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= -github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= -github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= -github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= -github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= -github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= -github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= -github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= -github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= -github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= -github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= -github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= -github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ= -github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= -github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= -github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= -github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= -github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= -github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= -github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= -github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= -github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= -github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= -github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= -github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= -github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= -github.com/containrrr/shoutrrr v0.5.2 h1:97P+wZDpN2gzBKLt4PDP1ZUTLz+k8AJVs40WOu9NNw8= -github.com/containrrr/shoutrrr v0.5.2/go.mod h1:XSU8tOIZ1JG8m6OuPozfGLpj6Ed+S8ZrRJaEodQhbzw= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec= +github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= -github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= -github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= -github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= -github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= -github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/cli v20.10.8+incompatible h1:/zO/6y9IOpcehE49yMRTV9ea0nBpb8OeqSskXLNfH1E= -github.com/docker/cli v20.10.8+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= -github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.8+incompatible h1:RVqD337BgQicVCzYrrlhLDWhq6OAD2PJDUg2LsEUvKM= -github.com/docker/docker v20.10.8+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg= +github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.1 h1:Dq4iIfcM7cNtddhLVWe9h4QDjsi4OER3Z8voPu/I52g= github.com/docker/docker-credential-helpers v0.6.1/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= -github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= -github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= -github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= -github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= -github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I= -github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= -github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA= -github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22 h1:jKUP9TQ0c7X3w6+IPyMit07RE42MtTWNd77sN2cHngQ= -github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22/go.mod h1:u0Jo4f2dNlTJeeOywkM6bLwxq6gC3pZ9rEFHn3AhTdk= -github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07 h1:+kBG/8rjCa6vxJZbUjAiE4MQmBEBYc8nLEb51frnvBY= -github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07/go.mod h1:j1kV/8f3jowErEq4XyeypkCdvg5EeHkf0YCKCcq5Ybo= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= -github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= -github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.6 h1:11TGpSHY7Esh/i/qnq02Jo5oVrI1Gue8Slbq0ujPZFQ= -github.com/nxadm/tail v1.4.6/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= -github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= -github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= -github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v1.0.3/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= -github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= -github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= -github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= -github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= -github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= -github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 h1:x7xEyJDP7Hv3LVgvWhzioQqbC/KtuUhTigKlH/8ehhE= -github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs= -github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= -github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= -github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= -github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= -github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= -github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= -github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= -github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= -go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= -go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e h1:XMgFehsDnnLGtjvjOfqWSUzt0alpTR1RSEuznObga2c= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= -gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gosrc.io/xmpp v0.5.1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= -gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= -k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= -k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= -k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= -k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= -k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= -k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= -k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= -k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= -k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= -k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= -k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= -k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= -k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= -k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= -k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= -k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= -nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY= -nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/grafana/dashboards/dashboard.json b/grafana/dashboards/dashboard.json index 998485b..3b7aa8f 100644 --- a/grafana/dashboards/dashboard.json +++ b/grafana/dashboards/dashboard.json @@ -102,7 +102,7 @@ "properties": [ { "id": "displayName", - "value": "Faled" + "value": "Failed" } ] }, @@ -290,4 +290,4 @@ "title": "Watchtower", "uid": "d7bdoT-Gz", "version": 1 -} \ No newline at end of file +} diff --git a/internal/actions/actions_suite_test.go b/internal/actions/actions_suite_test.go index 5110fea..c320564 100644 --- a/internal/actions/actions_suite_test.go +++ b/internal/actions/actions_suite_test.go @@ -1,12 +1,13 @@ package actions_test import ( - "github.com/sirupsen/logrus" "testing" "time" + "github.com/sirupsen/logrus" + "github.com/containrrr/watchtower/internal/actions" - "github.com/containrrr/watchtower/pkg/container" + "github.com/containrrr/watchtower/pkg/types" . "github.com/containrrr/watchtower/internal/actions/mocks" . "github.com/onsi/ginkgo" @@ -37,7 +38,7 @@ var _ = Describe("the actions package", func() { It("should not do anything", func() { client := CreateMockClient( &TestData{ - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container", "test-container", @@ -59,7 +60,7 @@ var _ = Describe("the actions package", func() { client = CreateMockClient( &TestData{ NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container-01", "test-container-01", @@ -89,7 +90,7 @@ var _ = Describe("the actions package", func() { BeforeEach(func() { client = CreateMockClient( &TestData{ - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container-01", "test-container-01", diff --git a/internal/actions/check.go b/internal/actions/check.go index 436931f..77a2266 100644 --- a/internal/actions/check.go +++ b/internal/actions/check.go @@ -2,16 +2,15 @@ package actions import ( "fmt" - "github.com/containrrr/watchtower/pkg/types" "sort" "time" + "github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/filters" "github.com/containrrr/watchtower/pkg/sorter" + "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" - - "github.com/containrrr/watchtower/pkg/container" ) // CheckForSanity makes sure everything is sane before starting @@ -40,7 +39,11 @@ func CheckForSanity(client container.Client, filter types.Filter, rollingRestart // will stop and remove all but the most recently started container. This behaviour can be bypassed // if a scope UID is defined. func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool, scope string) error { - containers, err := client.ListContainers(filters.FilterByScope(scope, filters.WatchtowerContainersFilter)) + filter := filters.WatchtowerContainersFilter + if scope != "" { + filter = filters.FilterByScope(scope, filter) + } + containers, err := client.ListContainers(filter) if err != nil { return err @@ -55,7 +58,7 @@ func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool, return cleanupExcessWatchtowers(containers, client, cleanup) } -func cleanupExcessWatchtowers(containers []container.Container, client container.Client, cleanup bool) error { +func cleanupExcessWatchtowers(containers []types.Container, client container.Client, cleanup bool) error { var stopErrors int sort.Sort(sorter.ByCreated(containers)) diff --git a/internal/actions/mocks/client.go b/internal/actions/mocks/client.go index 4844a55..737404a 100644 --- a/internal/actions/mocks/client.go +++ b/internal/actions/mocks/client.go @@ -3,7 +3,6 @@ package mocks import ( "errors" "fmt" - "github.com/containrrr/watchtower/pkg/container" "time" t "github.com/containrrr/watchtower/pkg/types" @@ -20,7 +19,8 @@ type MockClient struct { type TestData struct { TriedToRemoveImageCount int NameOfContainerToKeep string - Containers []container.Container + Containers []t.Container + Staleness map[string]bool } // TriedToRemoveImage is a test helper function to check whether RemoveImageByID has been called @@ -38,12 +38,12 @@ func CreateMockClient(data *TestData, pullImages bool, removeVolumes bool) MockC } // ListContainers is a mock method returning the provided container testdata -func (client MockClient) ListContainers(_ t.Filter) ([]container.Container, error) { +func (client MockClient) ListContainers(_ t.Filter) ([]t.Container, error) { return client.TestData.Containers, nil } // StopContainer is a mock method -func (client MockClient) StopContainer(c container.Container, _ time.Duration) error { +func (client MockClient) StopContainer(c t.Container, _ time.Duration) error { if c.Name() == client.TestData.NameOfContainerToKeep { return errors.New("tried to stop the instance we want to keep") } @@ -51,12 +51,12 @@ func (client MockClient) StopContainer(c container.Container, _ time.Duration) e } // StartContainer is a mock method -func (client MockClient) StartContainer(_ container.Container) (t.ContainerID, error) { +func (client MockClient) StartContainer(_ t.Container) (t.ContainerID, error) { return "", nil } // RenameContainer is a mock method -func (client MockClient) RenameContainer(_ container.Container, _ string) error { +func (client MockClient) RenameContainer(_ t.Container, _ string) error { return nil } @@ -67,7 +67,7 @@ func (client MockClient) RemoveImageByID(_ t.ImageID) error { } // GetContainer is a mock method -func (client MockClient) GetContainer(_ t.ContainerID) (container.Container, error) { +func (client MockClient) GetContainer(_ t.ContainerID) (t.Container, error) { return client.TestData.Containers[0], nil } @@ -85,12 +85,16 @@ func (client MockClient) ExecuteCommand(_ t.ContainerID, command string, _ int) } } -// IsContainerStale is always true for the mock client -func (client MockClient) IsContainerStale(_ container.Container) (bool, t.ImageID, error) { - return true, "", nil +// IsContainerStale is true if not explicitly stated in TestData for the mock client +func (client MockClient) IsContainerStale(cont t.Container, params t.UpdateParams) (bool, t.ImageID, error) { + stale, found := client.TestData.Staleness[cont.Name()] + if !found { + stale = true + } + return stale, "", nil } // WarnOnHeadPullFailed is always true for the mock client -func (client MockClient) WarnOnHeadPullFailed(_ container.Container) bool { +func (client MockClient) WarnOnHeadPullFailed(_ t.Container) bool { return true } diff --git a/internal/actions/mocks/container.go b/internal/actions/mocks/container.go index 167d571..e830587 100644 --- a/internal/actions/mocks/container.go +++ b/internal/actions/mocks/container.go @@ -2,18 +2,19 @@ package mocks import ( "fmt" + "strconv" + "strings" + "time" + "github.com/containrrr/watchtower/pkg/container" wt "github.com/containrrr/watchtower/pkg/types" "github.com/docker/docker/api/types" dockerContainer "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" - "strconv" - "strings" - "time" ) // CreateMockContainer creates a container substitute valid for testing -func CreateMockContainer(id string, name string, image string, created time.Time) container.Container { +func CreateMockContainer(id string, name string, image string, created time.Time) wt.Container { content := types.ContainerJSON{ ContainerJSONBase: &types.ContainerJSONBase{ ID: id, @@ -30,24 +31,29 @@ func CreateMockContainer(id string, name string, image string, created time.Time ExposedPorts: map[nat.Port]struct{}{}, }, } - return *container.NewContainer( + return container.NewContainer( &content, - &types.ImageInspect{ - ID: image, - RepoDigests: []string{ - image, - }, - }, + CreateMockImageInfo(image), ) } +// CreateMockImageInfo returns a mock image info struct based on the passed image +func CreateMockImageInfo(image string) *types.ImageInspect { + return &types.ImageInspect{ + ID: image, + RepoDigests: []string{ + image, + }, + } +} + // CreateMockContainerWithImageInfo should only be used for testing -func CreateMockContainerWithImageInfo(id string, name string, image string, created time.Time, imageInfo types.ImageInspect) container.Container { +func CreateMockContainerWithImageInfo(id string, name string, image string, created time.Time, imageInfo types.ImageInspect) wt.Container { return CreateMockContainerWithImageInfoP(id, name, image, created, &imageInfo) } // CreateMockContainerWithImageInfoP should only be used for testing -func CreateMockContainerWithImageInfoP(id string, name string, image string, created time.Time, imageInfo *types.ImageInspect) container.Container { +func CreateMockContainerWithImageInfoP(id string, name string, image string, created time.Time, imageInfo *types.ImageInspect) wt.Container { content := types.ContainerJSON{ ContainerJSONBase: &types.ContainerJSONBase{ ID: id, @@ -60,21 +66,21 @@ func CreateMockContainerWithImageInfoP(id string, name string, image string, cre Labels: make(map[string]string), }, } - return *container.NewContainer( + return container.NewContainer( &content, imageInfo, ) } // CreateMockContainerWithDigest should only be used for testing -func CreateMockContainerWithDigest(id string, name string, image string, created time.Time, digest string) container.Container { +func CreateMockContainerWithDigest(id string, name string, image string, created time.Time, digest string) wt.Container { c := CreateMockContainer(id, name, image, created) c.ImageInfo().RepoDigests = []string{digest} return c } // CreateMockContainerWithConfig creates a container substitute valid for testing -func CreateMockContainerWithConfig(id string, name string, image string, running bool, restarting bool, created time.Time, config *dockerContainer.Config) container.Container { +func CreateMockContainerWithConfig(id string, name string, image string, running bool, restarting bool, created time.Time, config *dockerContainer.Config) wt.Container { content := types.ContainerJSON{ ContainerJSONBase: &types.ContainerJSONBase{ ID: id, @@ -91,16 +97,14 @@ func CreateMockContainerWithConfig(id string, name string, image string, running }, Config: config, } - return *container.NewContainer( + return container.NewContainer( &content, - &types.ImageInspect{ - ID: image, - }, + CreateMockImageInfo(image), ) } // CreateContainerForProgress creates a container substitute for tracking session/update progress -func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (container.Container, wt.ImageID) { +func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (wt.Container, wt.ImageID) { indexStr := strconv.Itoa(idPrefix + index) mockID := indexStr + strings.Repeat("0", 61-len(indexStr)) contID := "c79" + mockID @@ -114,3 +118,26 @@ func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (con c := CreateMockContainerWithConfig(contID, contName, oldImgID, true, false, time.Now(), config) return c, wt.ImageID(newImgID) } + +// CreateMockContainerWithLinks should only be used for testing +func CreateMockContainerWithLinks(id string, name string, image string, created time.Time, links []string, imageInfo *types.ImageInspect) wt.Container { + content := types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + ID: id, + Image: image, + Name: name, + Created: created.String(), + HostConfig: &dockerContainer.HostConfig{ + Links: links, + }, + }, + Config: &dockerContainer.Config{ + Image: image, + Labels: make(map[string]string), + }, + } + return container.NewContainer( + &content, + imageInfo, + ) +} diff --git a/internal/actions/mocks/progress.go b/internal/actions/mocks/progress.go index 6883b48..23fc441 100644 --- a/internal/actions/mocks/progress.go +++ b/internal/actions/mocks/progress.go @@ -2,6 +2,7 @@ package mocks import ( "errors" + "github.com/containrrr/watchtower/pkg/session" wt "github.com/containrrr/watchtower/pkg/types" ) @@ -21,16 +22,13 @@ func CreateMockProgressReport(states ...session.State) wt.Report { case session.SkippedState: c, _ := CreateContainerForProgress(index, 41, "skip%d") progress.AddSkipped(c, errors.New("unpossible")) - break case session.FreshState: c, _ := CreateContainerForProgress(index, 31, "frsh%d") progress.AddScanned(c, c.ImageID()) - break case session.UpdatedState: c, newImage := CreateContainerForProgress(index, 11, "updt%d") progress.AddScanned(c, newImage) progress.MarkForUpdate(c.ID()) - break case session.FailedState: c, newImage := CreateContainerForProgress(index, 21, "fail%d") progress.AddScanned(c, newImage) diff --git a/internal/actions/update.go b/internal/actions/update.go index e0f7065..8853c6e 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -2,6 +2,7 @@ package actions import ( "errors" + "github.com/containrrr/watchtower/internal/util" "github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/lifecycle" @@ -9,7 +10,6 @@ import ( "github.com/containrrr/watchtower/pkg/sorter" "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" - "strings" ) // Update looks at the running Docker containers to see if any of the images @@ -33,8 +33,8 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e staleCheckFailed := 0 for i, targetContainer := range containers { - stale, newestImage, err := client.IsContainerStale(targetContainer) - shouldUpdate := stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly() + stale, newestImage, err := client.IsContainerStale(targetContainer, params) + shouldUpdate := stale && !params.NoRestart && !targetContainer.IsMonitorOnly(params) if err == nil && shouldUpdate { // Check to make sure we have all the necessary information for recreating the container err = targetContainer.VerifyConfiguration() @@ -57,7 +57,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e } else { progress.AddScanned(targetContainer, newestImage) } - containers[i].Stale = stale + containers[i].SetStale(stale) if stale { staleCount++ @@ -71,13 +71,11 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e UpdateImplicitRestart(containers) - var containersToUpdate []container.Container - if !params.MonitorOnly { - for _, c := range containers { - if !c.IsMonitorOnly() { - containersToUpdate = append(containersToUpdate, c) - progress.MarkForUpdate(c.ID()) - } + var containersToUpdate []types.Container + for _, c := range containers { + if !c.IsMonitorOnly(params) { + containersToUpdate = append(containersToUpdate, c) + progress.MarkForUpdate(c.ID()) } } @@ -96,7 +94,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e return progress.Report(), nil } -func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error { +func performRollingRestart(containers []types.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error { cleanupImageIDs := make(map[types.ImageID]bool, len(containers)) failed := make(map[types.ContainerID]error, len(containers)) @@ -108,8 +106,10 @@ func performRollingRestart(containers []container.Container, client container.Cl } else { if err := restartStaleContainer(containers[i], client, params); err != nil { failed[containers[i].ID()] = err + } else if containers[i].IsStale() { + // Only add (previously) stale containers' images to cleanup + cleanupImageIDs[containers[i].ImageID()] = true } - cleanupImageIDs[containers[i].ImageID()] = true } } } @@ -120,21 +120,22 @@ func performRollingRestart(containers []container.Container, client container.Cl return failed } -func stopContainersInReversedOrder(containers []container.Container, client container.Client, params types.UpdateParams) (failed map[types.ContainerID]error, stopped map[types.ImageID]bool) { +func stopContainersInReversedOrder(containers []types.Container, client container.Client, params types.UpdateParams) (failed map[types.ContainerID]error, stopped map[types.ImageID]bool) { failed = make(map[types.ContainerID]error, len(containers)) stopped = make(map[types.ImageID]bool, len(containers)) for i := len(containers) - 1; i >= 0; i-- { if err := stopStaleContainer(containers[i], client, params); err != nil { failed[containers[i].ID()] = err } else { - stopped[containers[i].ImageID()] = true + // NOTE: If a container is restarted due to a dependency this might be empty + stopped[containers[i].SafeImageID()] = true } } return } -func stopStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error { +func stopStaleContainer(container types.Container, client container.Client, params types.UpdateParams) error { if container.IsWatchtower() { log.Debugf("This is the watchtower container %s", container.Name()) return nil @@ -143,6 +144,14 @@ func stopStaleContainer(container container.Container, client container.Client, if !container.ToRestart() { return nil } + + // Perform an additional check here to prevent us from stopping a linked container we cannot restart + if container.IsLinkedToRestarting() { + if err := container.VerifyConfiguration(); err != nil { + return err + } + } + if params.LifecycleHooks { skipUpdate, err := lifecycle.ExecutePreUpdateCommand(client, container) if err != nil { @@ -163,7 +172,7 @@ func stopStaleContainer(container container.Container, client container.Client, return nil } -func restartContainersInSortedOrder(containers []container.Container, client container.Client, params types.UpdateParams, stoppedImages map[types.ImageID]bool) map[types.ContainerID]error { +func restartContainersInSortedOrder(containers []types.Container, client container.Client, params types.UpdateParams, stoppedImages map[types.ImageID]bool) map[types.ContainerID]error { cleanupImageIDs := make(map[types.ImageID]bool, len(containers)) failed := make(map[types.ContainerID]error, len(containers)) @@ -171,11 +180,13 @@ func restartContainersInSortedOrder(containers []container.Container, client con if !c.ToRestart() { continue } - if stoppedImages[c.ImageID()] { + if stoppedImages[c.SafeImageID()] { if err := restartStaleContainer(c, client, params); err != nil { failed[c.ID()] = err + } else if c.IsStale() { + // Only add (previously) stale containers' images to cleanup + cleanupImageIDs[c.ImageID()] = true } - cleanupImageIDs[c.ImageID()] = true } } @@ -188,13 +199,16 @@ func restartContainersInSortedOrder(containers []container.Container, client con func cleanupImages(client container.Client, imageIDs map[types.ImageID]bool) { for imageID := range imageIDs { + if imageID == "" { + continue + } if err := client.RemoveImageByID(imageID); err != nil { log.Error(err) } } } -func restartStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error { +func restartStaleContainer(container types.Container, client container.Client, params types.UpdateParams) error { // Since we can't shutdown a watchtower container immediately, we need to // start the new one while the old one is still running. This prevents us // from re-using the same container name so we first rename the current @@ -219,7 +233,7 @@ func restartStaleContainer(container container.Container, client container.Clien // UpdateImplicitRestart iterates through the passed containers, setting the // `LinkedToRestarting` flag if any of it's linked containers are marked for restart -func UpdateImplicitRestart(containers []container.Container) { +func UpdateImplicitRestart(containers []types.Container) { for ci, c := range containers { if c.ToRestart() { @@ -233,7 +247,7 @@ func UpdateImplicitRestart(containers []container.Container) { "linked": c.Name(), }).Debug("container is linked to restarting") // NOTE: To mutate the array, the `c` variable cannot be used as it's a copy - containers[ci].LinkedToRestarting = true + containers[ci].SetLinkedToRestarting(true) } } @@ -241,12 +255,8 @@ func UpdateImplicitRestart(containers []container.Container) { // linkedContainerMarkedForRestart returns the name of the first link that matches a // container marked for restart -func linkedContainerMarkedForRestart(links []string, containers []container.Container) string { +func linkedContainerMarkedForRestart(links []string, containers []types.Container) string { for _, linkName := range links { - // Since the container names need to start with '/', let's prepend it if it's missing - if !strings.HasPrefix(linkName, "/") { - linkName = "/" + linkName - } for _, candidate := range containers { if candidate.Name() == linkName && candidate.ToRestart() { return linkName diff --git a/internal/actions/update_test.go b/internal/actions/update_test.go index 7d392d7..9209dcd 100644 --- a/internal/actions/update_test.go +++ b/internal/actions/update_test.go @@ -1,55 +1,75 @@ package actions_test import ( + "time" + "github.com/containrrr/watchtower/internal/actions" - "github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/types" + dockerTypes "github.com/docker/docker/api/types" dockerContainer "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" - "time" . "github.com/containrrr/watchtower/internal/actions/mocks" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) +func getCommonTestData(keepContainer string) *TestData { + return &TestData{ + NameOfContainerToKeep: keepContainer, + Containers: []types.Container{ + CreateMockContainer( + "test-container-01", + "test-container-01", + "fake-image:latest", + time.Now().AddDate(0, 0, -1)), + CreateMockContainer( + "test-container-02", + "test-container-02", + "fake-image:latest", + time.Now()), + CreateMockContainer( + "test-container-02", + "test-container-02", + "fake-image:latest", + time.Now()), + }, + } +} + +func getLinkedTestData(withImageInfo bool) *TestData { + staleContainer := CreateMockContainer( + "test-container-01", + "/test-container-01", + "fake-image1:latest", + time.Now().AddDate(0, 0, -1)) + + var imageInfo *dockerTypes.ImageInspect + if withImageInfo { + imageInfo = CreateMockImageInfo("test-container-02") + } + linkingContainer := CreateMockContainerWithLinks( + "test-container-02", + "/test-container-02", + "fake-image2:latest", + time.Now(), + []string{staleContainer.Name()}, + imageInfo) + + return &TestData{ + Staleness: map[string]bool{linkingContainer.Name(): false}, + Containers: []types.Container{ + staleContainer, + linkingContainer, + }, + } +} + var _ = Describe("the update action", func() { - var client MockClient - When("watchtower has been instructed to clean up", func() { - BeforeEach(func() { - pullImages := false - removeVolumes := false - //goland:noinspection GoBoolExpressions - client = CreateMockClient( - &TestData{ - NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ - CreateMockContainer( - "test-container-01", - "test-container-01", - "fake-image:latest", - time.Now().AddDate(0, 0, -1)), - CreateMockContainer( - "test-container-02", - "test-container-02", - "fake-image:latest", - time.Now()), - CreateMockContainer( - "test-container-02", - "test-container-02", - "fake-image:latest", - time.Now()), - }, - }, - pullImages, - removeVolumes, - ) - }) - When("there are multiple containers using the same image", func() { It("should only try to remove the image once", func() { - + client := CreateMockClient(getCommonTestData(""), false, false) _, err := actions.Update(client, types.UpdateParams{Cleanup: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) @@ -57,8 +77,9 @@ var _ = Describe("the update action", func() { }) When("there are multiple containers using different images", func() { It("should try to remove each of them", func() { - client.TestData.Containers = append( - client.TestData.Containers, + testData := getCommonTestData("") + testData.Containers = append( + testData.Containers, CreateMockContainer( "unique-test-container", "unique-test-container", @@ -66,28 +87,49 @@ var _ = Describe("the update action", func() { time.Now(), ), ) + client := CreateMockClient(testData, false, false) _, err := actions.Update(client, types.UpdateParams{Cleanup: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(2)) }) }) + When("there are linked containers being updated", func() { + It("should not try to remove their images", func() { + client := CreateMockClient(getLinkedTestData(true), false, false) + _, err := actions.Update(client, types.UpdateParams{Cleanup: true}) + Expect(err).NotTo(HaveOccurred()) + Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) + }) + }) When("performing a rolling restart update", func() { It("should try to remove the image once", func() { - + client := CreateMockClient(getCommonTestData(""), false, false) _, err := actions.Update(client, types.UpdateParams{Cleanup: true, RollingRestart: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) }) }) + When("updating a linked container with missing image info", func() { + It("should gracefully fail", func() { + client := CreateMockClient(getLinkedTestData(false), false, false) + + report, err := actions.Update(client, types.UpdateParams{}) + Expect(err).NotTo(HaveOccurred()) + // Note: Linked containers that were skipped for recreation is not counted in Failed + // If this happens, an error is emitted to the logs, so a notification should still be sent. + Expect(report.Updated()).To(HaveLen(1)) + Expect(report.Fresh()).To(HaveLen(1)) + }) + }) }) When("watchtower has been instructed to monitor only", func() { When("certain containers are set to monitor only", func() { - BeforeEach(func() { - client = CreateMockClient( + It("should not update those containers", func() { + client := CreateMockClient( &TestData{ NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container-01", "test-container-01", @@ -110,9 +152,6 @@ var _ = Describe("the update action", func() { false, false, ) - }) - - It("should not update those containers", func() { _, err := actions.Update(client, types.UpdateParams{Cleanup: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) @@ -120,10 +159,10 @@ var _ = Describe("the update action", func() { }) When("monitor only is set globally", func() { - BeforeEach(func() { - client = CreateMockClient( + It("should not update any containers", func() { + client := CreateMockClient( &TestData{ - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container-01", "test-container-01", @@ -139,25 +178,94 @@ var _ = Describe("the update action", func() { false, false, ) - }) - - It("should not update any containers", func() { - _, err := actions.Update(client, types.UpdateParams{MonitorOnly: true}) + _, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0)) }) - }) + When("watchtower has been instructed to have label take precedence", func() { + It("it should update containers when monitor only is set to false", func() { + client := CreateMockClient( + &TestData{ + //NameOfContainerToKeep: "test-container-02", + Containers: []types.Container{ + CreateMockContainerWithConfig( + "test-container-02", + "test-container-02", + "fake-image2:latest", + false, + false, + time.Now(), + &dockerContainer.Config{ + Labels: map[string]string{ + "com.centurylinklabs.watchtower.monitor-only": "false", + }, + }), + }, + }, + false, + false, + ) + _, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true}) + Expect(err).NotTo(HaveOccurred()) + Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) + }) + It("it should update not containers when monitor only is set to true", func() { + client := CreateMockClient( + &TestData{ + //NameOfContainerToKeep: "test-container-02", + Containers: []types.Container{ + CreateMockContainerWithConfig( + "test-container-02", + "test-container-02", + "fake-image2:latest", + false, + false, + time.Now(), + &dockerContainer.Config{ + Labels: map[string]string{ + "com.centurylinklabs.watchtower.monitor-only": "true", + }, + }), + }, + }, + false, + false, + ) + _, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true}) + Expect(err).NotTo(HaveOccurred()) + Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0)) + }) + It("it should update not containers when monitor only is not set", func() { + client := CreateMockClient( + &TestData{ + Containers: []types.Container{ + CreateMockContainer( + "test-container-01", + "test-container-01", + "fake-image:latest", + time.Now()), + }, + }, + false, + false, + ) + _, err := actions.Update(client, types.UpdateParams{Cleanup: true, MonitorOnly: true, LabelPrecedence: true}) + Expect(err).NotTo(HaveOccurred()) + Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0)) + }) + }) + }) }) When("watchtower has been instructed to run lifecycle hooks", func() { - When("prupddate script returns 1", func() { - BeforeEach(func() { - client = CreateMockClient( + When("pre-update script returns 1", func() { + It("should not update those containers", func() { + client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", @@ -177,9 +285,7 @@ var _ = Describe("the update action", func() { false, false, ) - }) - It("should not update those containers", func() { _, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0)) @@ -188,11 +294,11 @@ var _ = Describe("the update action", func() { }) When("prupddate script returns 75", func() { - BeforeEach(func() { - client = CreateMockClient( + It("should not update those containers", func() { + client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", @@ -212,9 +318,6 @@ var _ = Describe("the update action", func() { false, false, ) - }) - - It("should not update those containers", func() { _, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0)) @@ -223,11 +326,11 @@ var _ = Describe("the update action", func() { }) When("prupddate script returns 0", func() { - BeforeEach(func() { - client = CreateMockClient( + It("should update those containers", func() { + client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", @@ -247,9 +350,6 @@ var _ = Describe("the update action", func() { false, false, ) - }) - - It("should update those containers", func() { _, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) @@ -271,7 +371,7 @@ var _ = Describe("the update action", func() { ExposedPorts: map[nat.Port]struct{}{}, }) - provider.Stale = true + provider.SetStale(true) consumer := CreateMockContainerWithConfig( "test-container-consumer", @@ -287,7 +387,7 @@ var _ = Describe("the update action", func() { ExposedPorts: map[nat.Port]struct{}{}, }) - containers := []container.Container{ + containers := []types.Container{ provider, consumer, } @@ -305,11 +405,11 @@ var _ = Describe("the update action", func() { }) When("container is not running", func() { - BeforeEach(func() { - client = CreateMockClient( + It("skip running preupdate", func() { + client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", @@ -329,9 +429,6 @@ var _ = Describe("the update action", func() { false, false, ) - }) - - It("skip running preupdate", func() { _, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) @@ -340,11 +437,11 @@ var _ = Describe("the update action", func() { }) When("container is restarting", func() { - BeforeEach(func() { - client = CreateMockClient( + It("skip running preupdate", func() { + client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", @@ -364,9 +461,6 @@ var _ = Describe("the update action", func() { false, false, ) - }) - - It("skip running preupdate", func() { _, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) diff --git a/internal/flags/flags.go b/internal/flags/flags.go index a02dbd7..c11cdae 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -1,8 +1,11 @@ package flags import ( - "io/ioutil" + "bufio" + "errors" + "fmt" "os" + "regexp" "strings" "time" @@ -16,12 +19,14 @@ import ( // use watchtower const DockerAPIMinVersion string = "1.25" +var defaultInterval = int((time.Hour * 24).Seconds()) + // RegisterDockerFlags that are used directly by the docker api client func RegisterDockerFlags(rootCmd *cobra.Command) { flags := rootCmd.PersistentFlags() - flags.StringP("host", "H", viper.GetString("DOCKER_HOST"), "daemon socket to connect to") - flags.BoolP("tlsverify", "v", viper.GetBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote") - flags.StringP("api-version", "a", viper.GetString("DOCKER_API_VERSION"), "api version to use by docker client") + flags.StringP("host", "H", envString("DOCKER_HOST"), "daemon socket to connect to") + flags.BoolP("tlsverify", "v", envBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote") + flags.StringP("api-version", "a", envString("DOCKER_API_VERSION"), "api version to use by docker client") } // RegisterSystemFlags that are used by watchtower to modify the program flow @@ -30,143 +35,182 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { flags.IntP( "interval", "i", - viper.GetInt("WATCHTOWER_POLL_INTERVAL"), + envInt("WATCHTOWER_POLL_INTERVAL"), "Poll interval (in seconds)") flags.StringP( "schedule", "s", - viper.GetString("WATCHTOWER_SCHEDULE"), + envString("WATCHTOWER_SCHEDULE"), "The cron expression which defines when to update") flags.DurationP( "stop-timeout", "t", - viper.GetDuration("WATCHTOWER_TIMEOUT"), + envDuration("WATCHTOWER_TIMEOUT"), "Timeout before a container is forcefully stopped") flags.BoolP( "no-pull", "", - viper.GetBool("WATCHTOWER_NO_PULL"), + envBool("WATCHTOWER_NO_PULL"), "Do not pull any new images") flags.BoolP( "no-restart", "", - viper.GetBool("WATCHTOWER_NO_RESTART"), + envBool("WATCHTOWER_NO_RESTART"), "Do not restart any containers") flags.BoolP( "no-startup-message", "", - viper.GetBool("WATCHTOWER_NO_STARTUP_MESSAGE"), + envBool("WATCHTOWER_NO_STARTUP_MESSAGE"), "Prevents watchtower from sending a startup message") flags.BoolP( "cleanup", "c", - viper.GetBool("WATCHTOWER_CLEANUP"), + envBool("WATCHTOWER_CLEANUP"), "Remove previously used images after updating") flags.BoolP( "remove-volumes", "", - viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"), + envBool("WATCHTOWER_REMOVE_VOLUMES"), "Remove attached volumes before updating") flags.BoolP( "label-enable", "e", - viper.GetBool("WATCHTOWER_LABEL_ENABLE"), + envBool("WATCHTOWER_LABEL_ENABLE"), "Watch containers where the com.centurylinklabs.watchtower.enable label is true") + flags.StringSliceP( + "disable-containers", + "x", + // Due to issue spf13/viper#380, can't use viper.GetStringSlice: + regexp.MustCompile("[, ]+").Split(envString("WATCHTOWER_DISABLE_CONTAINERS"), -1), + "Comma-separated list of containers to explicitly exclude from watching.") + + flags.StringP( + "log-format", + "l", + viper.GetString("WATCHTOWER_LOG_FORMAT"), + "Sets what logging format to use for console output. Possible values: Auto, LogFmt, Pretty, JSON") + flags.BoolP( "debug", "d", - viper.GetBool("WATCHTOWER_DEBUG"), + envBool("WATCHTOWER_DEBUG"), "Enable debug mode with verbose logging") flags.BoolP( "trace", "", - viper.GetBool("WATCHTOWER_TRACE"), + envBool("WATCHTOWER_TRACE"), "Enable trace mode with very verbose logging - caution, exposes credentials") flags.BoolP( "monitor-only", "m", - viper.GetBool("WATCHTOWER_MONITOR_ONLY"), + envBool("WATCHTOWER_MONITOR_ONLY"), "Will only monitor for new images, not update the containers") flags.BoolP( "run-once", "R", - viper.GetBool("WATCHTOWER_RUN_ONCE"), + envBool("WATCHTOWER_RUN_ONCE"), "Run once now and exit") flags.BoolP( "include-restarting", "", - viper.GetBool("WATCHTOWER_INCLUDE_RESTARTING"), + envBool("WATCHTOWER_INCLUDE_RESTARTING"), "Will also include restarting containers") flags.BoolP( "include-stopped", "S", - viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"), + envBool("WATCHTOWER_INCLUDE_STOPPED"), "Will also include created and exited containers") flags.BoolP( "revive-stopped", "", - viper.GetBool("WATCHTOWER_REVIVE_STOPPED"), + envBool("WATCHTOWER_REVIVE_STOPPED"), "Will also start stopped containers that were updated, if include-stopped is active") flags.BoolP( "enable-lifecycle-hooks", "", - viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"), + envBool("WATCHTOWER_LIFECYCLE_HOOKS"), "Enable the execution of commands triggered by pre- and post-update lifecycle hooks") flags.BoolP( "rolling-restart", "", - viper.GetBool("WATCHTOWER_ROLLING_RESTART"), + envBool("WATCHTOWER_ROLLING_RESTART"), "Restart containers one at a time") flags.BoolP( "http-api-update", "", - viper.GetBool("WATCHTOWER_HTTP_API_UPDATE"), + envBool("WATCHTOWER_HTTP_API_UPDATE"), "Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request") flags.BoolP( "http-api-metrics", "", - viper.GetBool("WATCHTOWER_HTTP_API_METRICS"), + envBool("WATCHTOWER_HTTP_API_METRICS"), "Runs Watchtower with the Prometheus metrics API enabled") flags.StringP( "http-api-token", "", - viper.GetString("WATCHTOWER_HTTP_API_TOKEN"), + envString("WATCHTOWER_HTTP_API_TOKEN"), "Sets an authentication token to HTTP API requests.") + flags.BoolP( "http-api-periodic-polls", "", - viper.GetBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"), + envBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"), "Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled") + // https://no-color.org/ flags.BoolP( "no-color", "", viper.IsSet("NO_COLOR"), "Disable ANSI color escape codes in log output") + flags.StringP( "scope", "", - viper.GetString("WATCHTOWER_SCOPE"), + envString("WATCHTOWER_SCOPE"), "Defines a monitoring scope for the Watchtower instance.") + + flags.StringP( + "porcelain", + "P", + envString("WATCHTOWER_PORCELAIN"), + `Write session results to stdout using a stable versioned format. Supported values: "v1"`) + + flags.String( + "log-level", + envString("WATCHTOWER_LOG_LEVEL"), + "The maximum log level that will be written to STDERR. Possible values: panic, fatal, error, warn, info, debug or trace") + + flags.BoolP( + "health-check", + "", + false, + "Do health check and exit") + + flags.BoolP( + "label-take-precedence", + "", + envBool("WATCHTOWER_LABEL_TAKE_PRECEDENCE"), + "Label applied to containers take precedence over arguments") } // RegisterNotificationFlags that are used by watchtower to send notifications @@ -176,170 +220,216 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) { flags.StringSliceP( "notifications", "n", - viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"), + envStringSlice("WATCHTOWER_NOTIFICATIONS"), " Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)") flags.String( "notifications-level", - viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"), + envString("WATCHTOWER_NOTIFICATIONS_LEVEL"), "The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug") + flags.IntP( + "notifications-delay", + "", + envInt("WATCHTOWER_NOTIFICATIONS_DELAY"), + "Delay before sending notifications, expressed in seconds") + flags.StringP( "notifications-hostname", "", - viper.GetString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"), + envString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"), "Custom hostname for notification titles") flags.StringP( "notification-email-from", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"), + envString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"), "Address to send notification emails from") flags.StringP( "notification-email-to", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"), + envString("WATCHTOWER_NOTIFICATION_EMAIL_TO"), "Address to send notification emails to") flags.IntP( "notification-email-delay", "", - viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"), + envInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"), "Delay before sending notifications, expressed in seconds") flags.StringP( "notification-email-server", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"), + envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"), "SMTP server to send notification emails through") flags.IntP( "notification-email-server-port", "", - viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"), + envInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"), "SMTP server port to send notification emails through") flags.BoolP( "notification-email-server-tls-skip-verify", "", - viper.GetBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"), + envBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"), `Controls whether watchtower verifies the SMTP server's certificate chain and host name. Should only be used for testing.`) flags.StringP( "notification-email-server-user", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"), + envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"), "SMTP server user for sending notifications") flags.StringP( "notification-email-server-password", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"), + envString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"), "SMTP server password for sending notifications") flags.StringP( "notification-email-subjecttag", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"), + envString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"), "Subject prefix tag for notifications via mail") flags.StringP( "notification-slack-hook-url", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"), + envString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"), "The Slack Hook URL to send notifications to") flags.StringP( "notification-slack-identifier", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"), + envString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"), "A string which will be used to identify the messages coming from this watchtower instance") flags.StringP( "notification-slack-channel", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"), + envString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"), "A string which overrides the webhook's default channel. Example: #my-custom-channel") flags.StringP( "notification-slack-icon-emoji", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"), + envString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"), "An emoji code string to use in place of the default icon") flags.StringP( "notification-slack-icon-url", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"), + envString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"), "An icon image URL string to use in place of the default icon") flags.StringP( "notification-msteams-hook", "", - viper.GetString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"), + envString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"), "The MSTeams WebHook URL to send notifications to") flags.BoolP( "notification-msteams-data", "", - viper.GetBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"), + envBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"), "The MSTeams notifier will try to extract log entry fields as MSTeams message facts") flags.StringP( "notification-gotify-url", "", - viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"), + envString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"), "The Gotify URL to send notifications to") flags.StringP( "notification-gotify-token", "", - viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"), + envString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"), "The Gotify Application required to query the Gotify API") flags.BoolP( "notification-gotify-tls-skip-verify", "", - viper.GetBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"), + envBool("WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY"), `Controls whether watchtower verifies the Gotify server's certificate chain and host name. Should only be used for testing.`) flags.String( "notification-template", - viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"), + envString("WATCHTOWER_NOTIFICATION_TEMPLATE"), "The shoutrrr text/template for the messages") flags.StringArray( "notification-url", - viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"), + envStringSlice("WATCHTOWER_NOTIFICATION_URL"), "The shoutrrr URL to send notifications to") flags.Bool("notification-report", - viper.GetBool("WATCHTOWER_NOTIFICATION_REPORT"), + envBool("WATCHTOWER_NOTIFICATION_REPORT"), "Use the session report as the notification template data") + flags.StringP( + "notification-title-tag", + "", + envString("WATCHTOWER_NOTIFICATION_TITLE_TAG"), + "Title prefix tag for notifications") + + flags.Bool("notification-skip-title", + envBool("WATCHTOWER_NOTIFICATION_SKIP_TITLE"), + "Do not pass the title param to notifications") + flags.String( "warn-on-head-failure", - viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"), + envString("WATCHTOWER_WARN_ON_HEAD_FAILURE"), "When to warn about HEAD pull requests failing. Possible values: always, auto or never") + flags.Bool( + "notification-log-stdout", + envBool("WATCHTOWER_NOTIFICATION_LOG_STDOUT"), + "Write notification logs to stdout instead of logging (to stderr)") +} + +func envString(key string) string { + viper.MustBindEnv(key) + return viper.GetString(key) +} + +func envStringSlice(key string) []string { + viper.MustBindEnv(key) + return viper.GetStringSlice(key) +} + +func envInt(key string) int { + viper.MustBindEnv(key) + return viper.GetInt(key) +} + +func envBool(key string) bool { + viper.MustBindEnv(key) + return viper.GetBool(key) +} + +func envDuration(key string) time.Duration { + viper.MustBindEnv(key) + return viper.GetDuration(key) } // SetDefaults provides default values for environment variables func SetDefaults() { - day := (time.Hour * 24).Seconds() viper.AutomaticEnv() viper.SetDefault("DOCKER_HOST", "unix:///var/run/docker.sock") viper.SetDefault("DOCKER_API_VERSION", DockerAPIMinVersion) - viper.SetDefault("WATCHTOWER_POLL_INTERVAL", day) + viper.SetDefault("WATCHTOWER_POLL_INTERVAL", defaultInterval) viper.SetDefault("WATCHTOWER_TIMEOUT", time.Second*10) viper.SetDefault("WATCHTOWER_NOTIFICATIONS", []string{}) viper.SetDefault("WATCHTOWER_NOTIFICATIONS_LEVEL", "info") viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT", 25) viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "") viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower") + viper.SetDefault("WATCHTOWER_LOG_LEVEL", "info") + viper.SetDefault("WATCHTOWER_LOG_FORMAT", "auto") } // EnvConfig translates the command-line options into environment variables @@ -427,34 +517,191 @@ func GetSecretsFromFiles(rootCmd *cobra.Command) { "notification-slack-hook-url", "notification-msteams-hook", "notification-gotify-token", + "notification-url", + "http-api-token", } for _, secret := range secrets { - getSecretFromFile(flags, secret) + if err := getSecretFromFile(flags, secret); err != nil { + log.Fatalf("failed to get secret from flag %v: %s", secret, err) + } } } // getSecretFromFile will check if the flag contains a reference to a file; if it does, replaces the value of the flag with the contents of the file. -func getSecretFromFile(flags *pflag.FlagSet, secret string) { - value, err := flags.GetString(secret) - if err != nil { - log.Error(err) +func getSecretFromFile(flags *pflag.FlagSet, secret string) error { + flag := flags.Lookup(secret) + if sliceValue, ok := flag.Value.(pflag.SliceValue); ok { + oldValues := sliceValue.GetSlice() + values := make([]string, 0, len(oldValues)) + for _, value := range oldValues { + if value != "" && isFile(value) { + file, err := os.Open(value) + if err != nil { + return err + } + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if line == "" { + continue + } + values = append(values, line) + } + if err := file.Close(); err != nil { + return err + } + } else { + values = append(values, value) + } + } + return sliceValue.Replace(values) } + + value := flag.Value.String() if value != "" && isFile(value) { - file, err := ioutil.ReadFile(value) + content, err := os.ReadFile(value) if err != nil { - log.Fatal(err) - } - err = flags.Set(secret, strings.TrimSpace(string(file))) - if err != nil { - log.Error(err) + return err } + return flags.Set(secret, strings.TrimSpace(string(content))) } + + return nil } func isFile(s string) bool { - _, err := os.Stat(s) - if os.IsNotExist(err) { + firstColon := strings.IndexRune(s, ':') + if firstColon != 1 && firstColon != -1 { + // If the string contains a ':', but it's not the second character, it's probably not a file + // and will cause a fatal error on windows if stat'ed + // This still allows for paths that start with 'c:\' etc. return false } - return true + _, err := os.Stat(s) + return !errors.Is(err, os.ErrNotExist) +} + +// ProcessFlagAliases updates the value of flags that are being set by helper flags +func ProcessFlagAliases(flags *pflag.FlagSet) { + + porcelain, err := flags.GetString(`porcelain`) + if err != nil { + log.Fatalf(`Failed to get flag: %v`, err) + } + if porcelain != "" { + if porcelain != "v1" { + log.Fatalf(`Unknown porcelain version %q. Supported values: "v1"`, porcelain) + } + if err = appendFlagValue(flags, `notification-url`, `logger://`); err != nil { + log.Errorf(`Failed to set flag: %v`, err) + } + setFlagIfDefault(flags, `notification-log-stdout`, `true`) + setFlagIfDefault(flags, `notification-report`, `true`) + tpl := fmt.Sprintf(`porcelain.%s.summary-no-log`, porcelain) + setFlagIfDefault(flags, `notification-template`, tpl) + } + + scheduleChanged := flags.Changed(`schedule`) + intervalChanged := flags.Changed(`interval`) + // FIXME: snakeswap + // due to how viper is integrated by swapping the defaults for the flags, we need this hack: + if val, _ := flags.GetString(`schedule`); val != `` { + scheduleChanged = true + } + if val, _ := flags.GetInt(`interval`); val != defaultInterval { + intervalChanged = true + } + + if intervalChanged && scheduleChanged { + log.Fatal(`Only schedule or interval can be defined, not both.`) + } + + // update schedule flag to match interval if it's set, or to the default if none of them are + if intervalChanged || !scheduleChanged { + interval, _ := flags.GetInt(`interval`) + _ = flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval)) + } + + if flagIsEnabled(flags, `debug`) { + _ = flags.Set(`log-level`, `debug`) + } + + if flagIsEnabled(flags, `trace`) { + _ = flags.Set(`log-level`, `trace`) + } + +} + +// SetupLogging reads only the flags that is needed to set up logging and applies them to the global logger +func SetupLogging(f *pflag.FlagSet) error { + logFormat, _ := f.GetString(`log-format`) + noColor, _ := f.GetBool("no-color") + + switch strings.ToLower(logFormat) { + case "auto": + // This will either use the "pretty" or "logfmt" format, based on whether the standard out is connected to a TTY + log.SetFormatter(&log.TextFormatter{ + DisableColors: noColor, + // enable logrus built-in support for https://bixense.com/clicolors/ + EnvironmentOverrideColors: true, + }) + case "json": + log.SetFormatter(&log.JSONFormatter{}) + case "logfmt": + log.SetFormatter(&log.TextFormatter{ + DisableColors: true, + FullTimestamp: true, + }) + case "pretty": + log.SetFormatter(&log.TextFormatter{ + // "Pretty" format combined with `--no-color` will only change the timestamp to the time since start + ForceColors: !noColor, + FullTimestamp: false, + }) + default: + return fmt.Errorf("invalid log format: %s", logFormat) + } + + rawLogLevel, _ := f.GetString(`log-level`) + if logLevel, err := log.ParseLevel(rawLogLevel); err != nil { + return fmt.Errorf("invalid log level: %e", err) + } else { + log.SetLevel(logLevel) + } + + return nil +} + +func flagIsEnabled(flags *pflag.FlagSet, name string) bool { + value, err := flags.GetBool(name) + if err != nil { + log.Fatalf(`The flag %q is not defined`, name) + } + return value +} + +func appendFlagValue(flags *pflag.FlagSet, name string, values ...string) error { + flag := flags.Lookup(name) + if flag == nil { + return fmt.Errorf(`invalid flag name %q`, name) + } + + if flagValues, ok := flag.Value.(pflag.SliceValue); ok { + for _, value := range values { + _ = flagValues.Append(value) + } + } else { + return fmt.Errorf(`the value for flag %q is not a slice value`, name) + } + + return nil +} + +func setFlagIfDefault(flags *pflag.FlagSet, name string, value string) { + if flags.Changed(name) { + return + } + if err := flags.Set(name, value); err != nil { + log.Errorf(`Failed to set flag: %v`, err) + } } diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index e298622..2856456 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -1,16 +1,23 @@ package flags import ( - "io/ioutil" "os" + "strings" "testing" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestEnvConfig_Defaults(t *testing.T) { + // Unset testing environments own variables, since those are not what is under test + _ = os.Unsetenv("DOCKER_TLS_VERIFY") + _ = os.Unsetenv("DOCKER_HOST") + cmd := new(cobra.Command) SetDefaults() RegisterDockerFlags(cmd) @@ -43,9 +50,7 @@ func TestEnvConfig_Custom(t *testing.T) { func TestGetSecretsFromFilesWithString(t *testing.T) { value := "supersecretstring" - - err := os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value) - require.NoError(t, err) + t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value) testGetSecretsFromFiles(t, "notification-email-server-password", value) } @@ -54,28 +59,48 @@ func TestGetSecretsFromFilesWithFile(t *testing.T) { value := "megasecretstring" // Create the temporary file which will contain a secret. - file, err := ioutil.TempFile(os.TempDir(), "watchtower-") + file, err := os.CreateTemp(t.TempDir(), "watchtower-") require.NoError(t, err) - defer os.Remove(file.Name()) // Make sure to remove the temporary file later. // Write the secret to the temporary file. - secret := []byte(value) - _, err = file.Write(secret) + _, err = file.Write([]byte(value)) require.NoError(t, err) + require.NoError(t, file.Close()) - err = os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name()) - require.NoError(t, err) + t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name()) testGetSecretsFromFiles(t, "notification-email-server-password", value) } -func testGetSecretsFromFiles(t *testing.T, flagName string, expected string) { +func TestGetSliceSecretsFromFiles(t *testing.T) { + values := []string{"entry2", "", "entry3"} + + // Create the temporary file which will contain a secret. + file, err := os.CreateTemp(t.TempDir(), "watchtower-") + require.NoError(t, err) + + // Write the secret to the temporary file. + for _, value := range values { + _, err = file.WriteString("\n" + value) + require.NoError(t, err) + } + require.NoError(t, file.Close()) + + testGetSecretsFromFiles(t, "notification-url", `[entry1,entry2,entry3]`, + `--notification-url`, "entry1", + `--notification-url`, file.Name()) +} + +func testGetSecretsFromFiles(t *testing.T, flagName string, expected string, args ...string) { cmd := new(cobra.Command) SetDefaults() + RegisterSystemFlags(cmd) RegisterNotificationFlags(cmd) + require.NoError(t, cmd.ParseFlags(args)) GetSecretsFromFiles(cmd) - value, err := cmd.PersistentFlags().GetString(flagName) - require.NoError(t, err) + flag := cmd.PersistentFlags().Lookup(flagName) + require.NotNil(t, flag) + value := flag.Value.String() assert.Equal(t, expected, value) } @@ -94,3 +119,221 @@ func TestHTTPAPIPeriodicPollsFlag(t *testing.T) { assert.Equal(t, true, periodicPolls) } + +func TestIsFile(t *testing.T) { + assert.False(t, isFile("https://google.com"), "an URL should never be considered a file") + assert.True(t, isFile(os.Args[0]), "the currently running binary path should always be considered a file") +} + +func TestProcessFlagAliases(t *testing.T) { + logrus.StandardLogger().ExitFunc = func(_ int) { t.FailNow() } + cmd := new(cobra.Command) + SetDefaults() + RegisterDockerFlags(cmd) + RegisterSystemFlags(cmd) + RegisterNotificationFlags(cmd) + + require.NoError(t, cmd.ParseFlags([]string{ + `--porcelain`, `v1`, + `--interval`, `10`, + `--trace`, + })) + flags := cmd.Flags() + ProcessFlagAliases(flags) + + urls, _ := flags.GetStringArray(`notification-url`) + assert.Contains(t, urls, `logger://`) + + logStdout, _ := flags.GetBool(`notification-log-stdout`) + assert.True(t, logStdout) + + report, _ := flags.GetBool(`notification-report`) + assert.True(t, report) + + template, _ := flags.GetString(`notification-template`) + assert.Equal(t, `porcelain.v1.summary-no-log`, template) + + sched, _ := flags.GetString(`schedule`) + assert.Equal(t, `@every 10s`, sched) + + logLevel, _ := flags.GetString(`log-level`) + assert.Equal(t, `trace`, logLevel) +} + +func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) { + cmd := new(cobra.Command) + t.Setenv("WATCHTOWER_DEBUG", `true`) + + SetDefaults() + RegisterDockerFlags(cmd) + RegisterSystemFlags(cmd) + RegisterNotificationFlags(cmd) + + require.NoError(t, cmd.ParseFlags([]string{})) + flags := cmd.Flags() + ProcessFlagAliases(flags) + + logLevel, _ := flags.GetString(`log-level`) + assert.Equal(t, `debug`, logLevel) +} + +func TestLogFormatFlag(t *testing.T) { + cmd := new(cobra.Command) + + SetDefaults() + RegisterDockerFlags(cmd) + RegisterSystemFlags(cmd) + + // Ensure the default value is Auto + require.NoError(t, cmd.ParseFlags([]string{})) + require.NoError(t, SetupLogging(cmd.Flags())) + assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter) + + // Test JSON format + require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `JSON`})) + require.NoError(t, SetupLogging(cmd.Flags())) + assert.IsType(t, &logrus.JSONFormatter{}, logrus.StandardLogger().Formatter) + + // Test Pretty format + require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `pretty`})) + require.NoError(t, SetupLogging(cmd.Flags())) + assert.IsType(t, &logrus.TextFormatter{}, logrus.StandardLogger().Formatter) + textFormatter, ok := (logrus.StandardLogger().Formatter).(*logrus.TextFormatter) + assert.True(t, ok) + assert.True(t, textFormatter.ForceColors) + assert.False(t, textFormatter.FullTimestamp) + + // Test LogFmt format + require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `logfmt`})) + require.NoError(t, SetupLogging(cmd.Flags())) + textFormatter, ok = (logrus.StandardLogger().Formatter).(*logrus.TextFormatter) + assert.True(t, ok) + assert.True(t, textFormatter.DisableColors) + assert.True(t, textFormatter.FullTimestamp) + + // Test invalid format + require.NoError(t, cmd.ParseFlags([]string{`--log-format`, `cowsay`})) + require.Error(t, SetupLogging(cmd.Flags())) +} + +func TestLogLevelFlag(t *testing.T) { + cmd := new(cobra.Command) + + SetDefaults() + RegisterDockerFlags(cmd) + RegisterSystemFlags(cmd) + + // Test invalid format + require.NoError(t, cmd.ParseFlags([]string{`--log-level`, `gossip`})) + require.Error(t, SetupLogging(cmd.Flags())) +} + +func TestProcessFlagAliasesSchedAndInterval(t *testing.T) { + logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) } + cmd := new(cobra.Command) + SetDefaults() + RegisterDockerFlags(cmd) + RegisterSystemFlags(cmd) + RegisterNotificationFlags(cmd) + + require.NoError(t, cmd.ParseFlags([]string{`--schedule`, `@hourly`, `--interval`, `10`})) + flags := cmd.Flags() + + assert.PanicsWithValue(t, `FATAL`, func() { + ProcessFlagAliases(flags) + }) +} + +func TestProcessFlagAliasesScheduleFromEnvironment(t *testing.T) { + cmd := new(cobra.Command) + + t.Setenv("WATCHTOWER_SCHEDULE", `@hourly`) + + SetDefaults() + RegisterDockerFlags(cmd) + RegisterSystemFlags(cmd) + RegisterNotificationFlags(cmd) + + require.NoError(t, cmd.ParseFlags([]string{})) + flags := cmd.Flags() + ProcessFlagAliases(flags) + + sched, _ := flags.GetString(`schedule`) + assert.Equal(t, `@hourly`, sched) +} + +func TestProcessFlagAliasesInvalidPorcelaineVersion(t *testing.T) { + logrus.StandardLogger().ExitFunc = func(_ int) { panic(`FATAL`) } + cmd := new(cobra.Command) + SetDefaults() + RegisterDockerFlags(cmd) + RegisterSystemFlags(cmd) + RegisterNotificationFlags(cmd) + + require.NoError(t, cmd.ParseFlags([]string{`--porcelain`, `cowboy`})) + flags := cmd.Flags() + + assert.PanicsWithValue(t, `FATAL`, func() { + ProcessFlagAliases(flags) + }) +} + +func TestFlagsArePrecentInDocumentation(t *testing.T) { + + // Legacy notifcations are ignored, since they are (soft) deprecated + ignoredEnvs := map[string]string{ + "WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI": "legacy", + "WATCHTOWER_NOTIFICATION_SLACK_ICON_URL": "legacy", + } + + ignoredFlags := map[string]string{ + "notification-gotify-url": "legacy", + "notification-slack-icon-emoji": "legacy", + "notification-slack-icon-url": "legacy", + } + + cmd := new(cobra.Command) + SetDefaults() + RegisterDockerFlags(cmd) + RegisterSystemFlags(cmd) + RegisterNotificationFlags(cmd) + + flags := cmd.PersistentFlags() + + docFiles := []string{ + "../../docs/arguments.md", + "../../docs/lifecycle-hooks.md", + "../../docs/notifications.md", + } + allDocs := "" + for _, f := range docFiles { + bytes, err := os.ReadFile(f) + if err != nil { + t.Fatalf("Could not load docs file %q: %v", f, err) + } + allDocs += string(bytes) + } + + flags.VisitAll(func(f *pflag.Flag) { + if !strings.Contains(allDocs, "--"+f.Name) { + if _, found := ignoredFlags[f.Name]; !found { + t.Logf("Docs does not mention flag long name %q", f.Name) + t.Fail() + } + } + if !strings.Contains(allDocs, "-"+f.Shorthand) { + t.Logf("Docs does not mention flag shorthand %q (%q)", f.Shorthand, f.Name) + t.Fail() + } + }) + + for _, key := range viper.AllKeys() { + envKey := strings.ToUpper(key) + if !strings.Contains(allDocs, envKey) { + if _, found := ignoredEnvs[envKey]; !found { + t.Logf("Docs does not mention environment variable %q", envKey) + t.Fail() + } + } + } +} diff --git a/internal/util/rand_sha256.go b/internal/util/rand_sha256.go new file mode 100644 index 0000000..38a3736 --- /dev/null +++ b/internal/util/rand_sha256.go @@ -0,0 +1,24 @@ +package util + +import ( + "bytes" + "crypto/rand" + "fmt" +) + +// GenerateRandomSHA256 generates a random 64 character SHA 256 hash string +func GenerateRandomSHA256() string { + return GenerateRandomPrefixedSHA256()[7:] +} + +// GenerateRandomPrefixedSHA256 generates a random 64 character SHA 256 hash string, prefixed with `sha256:` +func GenerateRandomPrefixedSHA256() string { + hash := make([]byte, 32) + _, _ = rand.Read(hash) + sb := bytes.NewBufferString("sha256:") + sb.Grow(64) + for _, h := range hash { + _, _ = fmt.Fprintf(sb, "%02x", h) + } + return sb.String() +} diff --git a/internal/util/util_test.go b/internal/util/util_test.go index a6dd657..0b2c36c 100644 --- a/internal/util/util_test.go +++ b/internal/util/util_test.go @@ -1,8 +1,10 @@ package util import ( - "github.com/stretchr/testify/assert" + "regexp" "testing" + + "github.com/stretchr/testify/assert" ) func TestSliceEqual_True(t *testing.T) { @@ -62,3 +64,15 @@ func TestStructMapSubtract(t *testing.T) { assert.Equal(t, map[string]struct{}{"a": x, "b": x, "c": x}, m1) assert.Equal(t, map[string]struct{}{"a": x, "c": x}, m2) } + +// GenerateRandomSHA256 generates a random 64 character SHA 256 hash string +func TestGenerateRandomSHA256(t *testing.T) { + res := GenerateRandomSHA256() + assert.Len(t, res, 64) + assert.NotContains(t, res, "sha256:") +} + +func TestGenerateRandomPrefixedSHA256(t *testing.T) { + res := GenerateRandomPrefixedSHA256() + assert.Regexp(t, regexp.MustCompile("sha256:[0-9|a-f]{64}"), res) +} diff --git a/mkdocs.yml b/mkdocs.yml index 57b2f7b..5227004 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,7 +5,16 @@ edit_uri: edit/main/docs/ theme: name: 'material' palette: - scheme: containrrr + - media: "(prefers-color-scheme: light)" + scheme: containrrr + toggle: + icon: material/weather-night + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: containrrr-dark + toggle: + icon: material/weather-sunny + name: Switch to light mode logo: images/logo-450px.png favicon: images/favicon.ico extra_css: @@ -24,7 +33,7 @@ markdown_extensions: repo: watchtower - pymdownx.saneheaders - pymdownx.tabbed: - alternate_style: true + alternate_style: true nav: - 'Home': 'index.md' - 'Introduction': 'introduction.md' @@ -39,6 +48,7 @@ nav: - 'Stop signals': 'stop-signals.md' - 'Lifecycle hooks': 'lifecycle-hooks.md' - 'Running multiple instances': 'running-multiple-instances.md' + - 'HTTP API Mode': 'http-api-mode.md' - 'Metrics': 'metrics.md' plugins: - search diff --git a/oryxBuildBinary b/oryxBuildBinary new file mode 100755 index 0000000..86cbe57 Binary files /dev/null and b/oryxBuildBinary differ diff --git a/pkg/api/metrics/metrics_test.go b/pkg/api/metrics/metrics_test.go index 5120f8d..48b6dd7 100644 --- a/pkg/api/metrics/metrics_test.go +++ b/pkg/api/metrics/metrics_test.go @@ -2,18 +2,18 @@ package metrics_test import ( "fmt" - "io/ioutil" + "io" "net/http" "net/http/httptest" "strings" "testing" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/containrrr/watchtower/pkg/api" metricsAPI "github.com/containrrr/watchtower/pkg/api/metrics" "github.com/containrrr/watchtower/pkg/metrics" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" ) const ( @@ -36,7 +36,7 @@ func getWithToken(handler http.Handler) map[string]string { handler.ServeHTTP(respWriter, req) res := respWriter.Result() - body, _ := ioutil.ReadAll(res.Body) + body, _ := io.ReadAll(res.Body) for _, line := range strings.Split(string(body), "\n") { if len(line) < 1 || line[0] == '#' { diff --git a/pkg/api/update/update.go b/pkg/api/update/update.go index 4721e3e..ba044ab 100644 --- a/pkg/api/update/update.go +++ b/pkg/api/update/update.go @@ -4,6 +4,7 @@ import ( "io" "net/http" "os" + "strings" log "github.com/sirupsen/logrus" ) @@ -13,7 +14,7 @@ var ( ) // New is a factory function creating a new Handler instance -func New(updateFn func(), updateLock chan bool) *Handler { +func New(updateFn func(images []string), updateLock chan bool) *Handler { if updateLock != nil { lock = updateLock } else { @@ -29,7 +30,7 @@ func New(updateFn func(), updateLock chan bool) *Handler { // Handler is an API handler used for triggering container update scans type Handler struct { - fn func() + fn func(images []string) Path string } @@ -43,12 +44,29 @@ func (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) { return } - select { - case chanValue := <-lock: + var images []string + imageQueries, found := r.URL.Query()["image"] + if found { + for _, image := range imageQueries { + images = append(images, strings.Split(image, ",")...) + } + + } else { + images = nil + } + + if len(images) > 0 { + chanValue := <-lock defer func() { lock <- chanValue }() - handle.fn() - default: - log.Debug("Skipped. Another update already running.") + handle.fn(images) + } else { + select { + case chanValue := <-lock: + defer func() { lock <- chanValue }() + handle.fn(images) + default: + log.Debug("Skipped. Another update already running.") + } } } diff --git a/pkg/container/cgroup_id.go b/pkg/container/cgroup_id.go new file mode 100644 index 0000000..1da1dfe --- /dev/null +++ b/pkg/container/cgroup_id.go @@ -0,0 +1,29 @@ +package container + +import ( + "fmt" + "os" + "regexp" + + "github.com/containrrr/watchtower/pkg/types" +) + +var dockerContainerPattern = regexp.MustCompile(`[0-9]+:.*:/docker/([a-f|0-9]{64})`) + +// GetRunningContainerID tries to resolve the current container ID from the current process cgroup information +func GetRunningContainerID() (cid types.ContainerID, err error) { + file, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", os.Getpid())) + if err != nil { + return + } + + return getRunningContainerIDFromString(string(file)), nil +} + +func getRunningContainerIDFromString(s string) types.ContainerID { + matches := dockerContainerPattern.FindStringSubmatch(s) + if len(matches) < 2 { + return "" + } + return types.ContainerID(matches[1]) +} diff --git a/pkg/container/cgroup_id_test.go b/pkg/container/cgroup_id_test.go new file mode 100644 index 0000000..5f694e3 --- /dev/null +++ b/pkg/container/cgroup_id_test.go @@ -0,0 +1,40 @@ +package container + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("GetRunningContainerID", func() { + When("a matching container ID is found", func() { + It("should return that container ID", func() { + cid := getRunningContainerIDFromString(` +15:name=systemd:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +14:misc:/ +13:rdma:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +12:pids:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +11:hugetlb:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +10:net_prio:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +9:perf_event:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +8:net_cls:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +7:freezer:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +6:devices:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +5:blkio:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +4:cpuacct:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +3:cpu:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +2:cpuset:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +1:memory:/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 +0::/docker/991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377 + `) + Expect(cid).To(BeEquivalentTo(`991b6b42691449d3ce90192ff9f006863dcdafc6195e227aeefa298235004377`)) + }) + }) + When("no matching container ID could be found", func() { + It("should return that container ID", func() { + cid := getRunningContainerIDFromString(`14:misc:/`) + Expect(cid).To(BeEmpty()) + }) + }) +}) + +// diff --git a/pkg/container/client.go b/pkg/container/client.go index 371206b..c6c37de 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -3,14 +3,10 @@ package container import ( "bytes" "fmt" - "io/ioutil" + "io" "strings" "time" - "github.com/containrrr/watchtower/pkg/registry" - "github.com/containrrr/watchtower/pkg/registry/digest" - - t "github.com/containrrr/watchtower/pkg/types" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" @@ -18,6 +14,10 @@ import ( sdkClient "github.com/docker/docker/client" log "github.com/sirupsen/logrus" "golang.org/x/net/context" + + "github.com/containrrr/watchtower/pkg/registry" + "github.com/containrrr/watchtower/pkg/registry/digest" + t "github.com/containrrr/watchtower/pkg/types" ) const defaultStopSignal = "SIGTERM" @@ -25,24 +25,24 @@ const defaultStopSignal = "SIGTERM" // A Client is the interface through which watchtower interacts with the // Docker API. type Client interface { - ListContainers(t.Filter) ([]Container, error) - GetContainer(containerID t.ContainerID) (Container, error) - StopContainer(Container, time.Duration) error - StartContainer(Container) (t.ContainerID, error) - RenameContainer(Container, string) error - IsContainerStale(Container) (stale bool, latestImage t.ImageID, err error) + ListContainers(t.Filter) ([]t.Container, error) + GetContainer(containerID t.ContainerID) (t.Container, error) + StopContainer(t.Container, time.Duration) error + StartContainer(t.Container) (t.ContainerID, error) + RenameContainer(t.Container, string) error + IsContainerStale(t.Container, t.UpdateParams) (stale bool, latestImage t.ImageID, err error) ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error) RemoveImageByID(t.ImageID) error - WarnOnHeadPullFailed(container Container) bool + WarnOnHeadPullFailed(container t.Container) bool } // NewClient returns a new Client instance which can be used to interact with // the Docker API. // The client reads its configuration from the following environment variables: -// * DOCKER_HOST the docker-engine host to send api requests to -// * DOCKER_TLS_VERIFY whether to verify tls certificates -// * DOCKER_API_VERSION the minimum docker api version to work with -func NewClient(pullImages, includeStopped, reviveStopped, removeVolumes, includeRestarting bool, warnOnHeadFailed string) Client { +// - DOCKER_HOST the docker-engine host to send api requests to +// - DOCKER_TLS_VERIFY whether to verify tls certificates +// - DOCKER_API_VERSION the minimum docker api version to work with +func NewClient(opts ClientOptions) Client { cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv) if err != nil { @@ -50,46 +50,57 @@ func NewClient(pullImages, includeStopped, reviveStopped, removeVolumes, include } return dockerClient{ - api: cli, - pullImages: pullImages, - removeVolumes: removeVolumes, - includeStopped: includeStopped, - reviveStopped: reviveStopped, - includeRestarting: includeRestarting, - warnOnHeadFailed: warnOnHeadFailed, + api: cli, + ClientOptions: opts, } } +// ClientOptions contains the options for how the docker client wrapper should behave +type ClientOptions struct { + RemoveVolumes bool + IncludeStopped bool + ReviveStopped bool + IncludeRestarting bool + WarnOnHeadFailed WarningStrategy +} + +// WarningStrategy is a value determining when to show warnings +type WarningStrategy string + +const ( + // WarnAlways warns whenever the problem occurs + WarnAlways WarningStrategy = "always" + // WarnNever never warns when the problem occurs + WarnNever WarningStrategy = "never" + // WarnAuto skips warning when the problem was expected + WarnAuto WarningStrategy = "auto" +) + type dockerClient struct { - api sdkClient.CommonAPIClient - pullImages bool - removeVolumes bool - includeStopped bool - reviveStopped bool - includeRestarting bool - warnOnHeadFailed string + api sdkClient.CommonAPIClient + ClientOptions } -func (client dockerClient) WarnOnHeadPullFailed(container Container) bool { - if client.warnOnHeadFailed == "always" { +func (client dockerClient) WarnOnHeadPullFailed(container t.Container) bool { + if client.WarnOnHeadFailed == WarnAlways { return true } - if client.warnOnHeadFailed == "never" { + if client.WarnOnHeadFailed == WarnNever { return false } return registry.WarnOnAPIConsumption(container) } -func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) { - cs := []Container{} +func (client dockerClient) ListContainers(fn t.Filter) ([]t.Container, error) { + cs := []t.Container{} bg := context.Background() - if client.includeStopped && client.includeRestarting { + if client.IncludeStopped && client.IncludeRestarting { log.Debug("Retrieving running, stopped, restarting and exited containers") - } else if client.includeStopped { + } else if client.IncludeStopped { log.Debug("Retrieving running, stopped and exited containers") - } else if client.includeRestarting { + } else if client.IncludeRestarting { log.Debug("Retrieving running and restarting containers") } else { log.Debug("Retrieving running containers") @@ -125,36 +136,52 @@ func (client dockerClient) createListFilter() filters.Args { filterArgs := filters.NewArgs() filterArgs.Add("status", "running") - if client.includeStopped { + if client.IncludeStopped { filterArgs.Add("status", "created") filterArgs.Add("status", "exited") } - if client.includeRestarting { + if client.IncludeRestarting { filterArgs.Add("status", "restarting") } return filterArgs } -func (client dockerClient) GetContainer(containerID t.ContainerID) (Container, error) { +func (client dockerClient) GetContainer(containerID t.ContainerID) (t.Container, error) { bg := context.Background() containerInfo, err := client.api.ContainerInspect(bg, string(containerID)) if err != nil { - return Container{}, err + return &Container{}, err + } + + netType, netContainerId, found := strings.Cut(string(containerInfo.HostConfig.NetworkMode), ":") + if found && netType == "container" { + parentContainer, err := client.api.ContainerInspect(bg, netContainerId) + if err != nil { + log.WithFields(map[string]interface{}{ + "container": containerInfo.Name, + "error": err, + "network-container": netContainerId, + }).Warnf("Unable to resolve network container: %v", err) + + } else { + // Replace the container ID with a container name to allow it to reference the re-created network container + containerInfo.HostConfig.NetworkMode = container.NetworkMode(fmt.Sprintf("container:%s", parentContainer.Name)) + } } imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image) if err != nil { log.Warnf("Failed to retrieve container image info: %v", err) - return Container{containerInfo: &containerInfo, imageInfo: nil}, nil + return &Container{containerInfo: &containerInfo, imageInfo: nil}, nil } - return Container{containerInfo: &containerInfo, imageInfo: &imageInfo}, nil + return &Container{containerInfo: &containerInfo, imageInfo: &imageInfo}, nil } -func (client dockerClient) StopContainer(c Container, timeout time.Duration) error { +func (client dockerClient) StopContainer(c t.Container, timeout time.Duration) error { bg := context.Background() signal := c.StopSignal() if signal == "" { @@ -174,12 +201,16 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err // TODO: This should probably be checked. _ = client.waitForStopOrTimeout(c, timeout) - if c.containerInfo.HostConfig.AutoRemove { + if c.ContainerInfo().HostConfig.AutoRemove { log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", shortID) } else { log.Debugf("Removing container %s", shortID) - if err := client.api.ContainerRemove(bg, idStr, types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.removeVolumes}); err != nil { + if err := client.api.ContainerRemove(bg, idStr, types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.RemoveVolumes}); err != nil { + if sdkClient.IsErrNotFound(err) { + log.Debugf("Container %s not found, skipping removal.", shortID) + return nil + } return err } } @@ -192,11 +223,34 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err return nil } -func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) { +func (client dockerClient) GetNetworkConfig(c t.Container) *network.NetworkingConfig { + config := &network.NetworkingConfig{ + EndpointsConfig: c.ContainerInfo().NetworkSettings.Networks, + } + + for _, ep := range config.EndpointsConfig { + aliases := make([]string, 0, len(ep.Aliases)) + cidAlias := c.ID().ShortID() + + // Remove the old container ID alias from the network aliases, as it would accumulate across updates otherwise + for _, alias := range ep.Aliases { + if alias == cidAlias { + continue + } + aliases = append(aliases, alias) + } + + ep.Aliases = aliases + } + return config +} + +func (client dockerClient) StartContainer(c t.Container) (t.ContainerID, error) { bg := context.Background() - config := c.runtimeConfig() - hostConfig := c.hostConfig() - networkConfig := &network.NetworkingConfig{EndpointsConfig: c.containerInfo.NetworkSettings.Networks} + config := c.GetCreateConfig() + hostConfig := c.GetCreateHostConfig() + networkConfig := client.GetNetworkConfig(c) + // simpleNetworkConfig is a networkConfig with only 1 network. // see: https://github.com/docker/docker/issues/29265 simpleNetworkConfig := func() *network.NetworkingConfig { @@ -212,6 +266,7 @@ func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) { name := c.Name() log.Infof("Creating %s", name) + createdContainer, err := client.api.ContainerCreate(bg, config, hostConfig, simpleNetworkConfig, nil, name) if err != nil { return "", err @@ -236,7 +291,7 @@ func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) { } createdContainerID := t.ContainerID(createdContainer.ID) - if !c.IsRunning() && !client.reviveStopped { + if !c.IsRunning() && !client.ReviveStopped { return createdContainerID, nil } @@ -244,7 +299,7 @@ func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) { } -func (client dockerClient) doStartContainer(bg context.Context, c Container, creation container.ContainerCreateCreatedBody) error { +func (client dockerClient) doStartContainer(bg context.Context, c t.Container, creation container.CreateResponse) error { name := c.Name() log.Debugf("Starting container %s (%s)", name, t.ContainerID(creation.ID).ShortID()) @@ -255,16 +310,16 @@ func (client dockerClient) doStartContainer(bg context.Context, c Container, cre return nil } -func (client dockerClient) RenameContainer(c Container, newName string) error { +func (client dockerClient) RenameContainer(c t.Container, newName string) error { bg := context.Background() log.Debugf("Renaming container %s (%s) to %s", c.Name(), c.ID().ShortID(), newName) return client.api.ContainerRename(bg, string(c.ID()), newName) } -func (client dockerClient) IsContainerStale(container Container) (stale bool, latestImage t.ImageID, err error) { +func (client dockerClient) IsContainerStale(container t.Container, params t.UpdateParams) (stale bool, latestImage t.ImageID, err error) { ctx := context.Background() - if !client.pullImages { + if container.IsNoPull(params) { log.Debugf("Skipping image pull.") } else if err := client.PullImage(ctx, container); err != nil { return false, container.SafeImageID(), err @@ -273,8 +328,8 @@ func (client dockerClient) IsContainerStale(container Container) (stale bool, la return client.HasNewImage(ctx, container) } -func (client dockerClient) HasNewImage(ctx context.Context, container Container) (hasNew bool, latestImage t.ImageID, err error) { - currentImageID := t.ImageID(container.containerInfo.ContainerJSONBase.Image) +func (client dockerClient) HasNewImage(ctx context.Context, container t.Container) (hasNew bool, latestImage t.ImageID, err error) { + currentImageID := t.ImageID(container.ContainerInfo().ContainerJSONBase.Image) imageName := container.ImageName() newImageInfo, _, err := client.api.ImageInspectWithRaw(ctx, imageName) @@ -294,7 +349,7 @@ func (client dockerClient) HasNewImage(ctx context.Context, container Container) // PullImage pulls the latest image for the supplied container, optionally skipping if it's digest can be confirmed // to match the one that the registry reports via a HEAD request -func (client dockerClient) PullImage(ctx context.Context, container Container) error { +func (client dockerClient) PullImage(ctx context.Context, container t.Container) error { containerName := container.Name() imageName := container.ImageName() @@ -303,6 +358,10 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e "container": containerName, } + if strings.HasPrefix(imageName, "sha256:") { + return fmt.Errorf("container uses a pinned image, and cannot be updated by watchtower") + } + log.WithFields(fields).Debugf("Trying to load authentication credentials.") opts, err := registry.GetPullOptions(imageName) if err != nil { @@ -339,7 +398,7 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e defer response.Close() // the pull request will be aborted prematurely unless the response is read - if _, err = ioutil.ReadAll(response); err != nil { + if _, err = io.ReadAll(response); err != nil { log.Error(err) return err } @@ -349,13 +408,34 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e func (client dockerClient) RemoveImageByID(id t.ImageID) error { log.Infof("Removing image %s", id.ShortID()) - _, err := client.api.ImageRemove( + items, err := client.api.ImageRemove( context.Background(), string(id), types.ImageRemoveOptions{ Force: true, }) + if log.IsLevelEnabled(log.DebugLevel) { + deleted := strings.Builder{} + untagged := strings.Builder{} + for _, item := range items { + if item.Deleted != "" { + if deleted.Len() > 0 { + deleted.WriteString(`, `) + } + deleted.WriteString(t.ImageID(item.Deleted).ShortID()) + } + if item.Untagged != "" { + if untagged.Len() > 0 { + untagged.WriteString(`, `) + } + untagged.WriteString(t.ImageID(item.Untagged).ShortID()) + } + } + fields := log.Fields{`deleted`: deleted.String(), `untagged`: untagged.String()} + log.WithFields(fields).Debug("Image removal completed") + } + return err } @@ -438,7 +518,7 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e if err != nil { return false, err } - if execInspect.Running == true { + if execInspect.Running { time.Sleep(1 * time.Second) continue } @@ -451,14 +531,14 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e } if execInspect.ExitCode > 0 { - return false, fmt.Errorf("Command exited with code %v %s", execInspect.ExitCode, execOutput) + return false, fmt.Errorf("command exited with code %v %s", execInspect.ExitCode, execOutput) } break } return false, nil } -func (client dockerClient) waitForStopOrTimeout(c Container, waitTime time.Duration) error { +func (client dockerClient) waitForStopOrTimeout(c t.Container, waitTime time.Duration) error { bg := context.Background() timeout := time.After(waitTime) diff --git a/pkg/container/client_test.go b/pkg/container/client_test.go index 2f0157d..4e75409 100644 --- a/pkg/container/client_test.go +++ b/pkg/container/client_test.go @@ -1,6 +1,10 @@ package container import ( + "github.com/docker/docker/api/types/network" + "time" + + "github.com/containrrr/watchtower/internal/util" "github.com/containrrr/watchtower/pkg/container/mocks" "github.com/containrrr/watchtower/pkg/filters" t "github.com/containrrr/watchtower/pkg/types" @@ -8,14 +12,16 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/backend" cli "github.com/docker/docker/client" + "github.com/docker/docker/errdefs" "github.com/onsi/gomega/gbytes" "github.com/onsi/gomega/ghttp" "github.com/sirupsen/logrus" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - . "github.com/onsi/gomega/types" + gt "github.com/onsi/gomega/types" + "context" "net/http" ) @@ -32,41 +38,114 @@ var _ = Describe("the client", func() { mockServer.Close() }) Describe("WarnOnHeadPullFailed", func() { - containerUnknown := *mockContainerWithImageName("unknown.repo/prefix/imagename:latest") - containerKnown := *mockContainerWithImageName("docker.io/prefix/imagename:latest") + containerUnknown := MockContainer(WithImageName("unknown.repo/prefix/imagename:latest")) + containerKnown := MockContainer(WithImageName("docker.io/prefix/imagename:latest")) - When("warn on head failure is set to \"always\"", func() { - c := newClientNoAPI(false, false, false, false, false, "always") + When(`warn on head failure is set to "always"`, func() { + c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAlways}} It("should always return true", func() { Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeTrue()) Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue()) }) }) - When("warn on head failure is set to \"auto\"", func() { - c := newClientNoAPI(false, false, false, false, false, "auto") - It("should always return true", func() { + When(`warn on head failure is set to "auto"`, func() { + c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAuto}} + It("should return false for unknown repos", func() { Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse()) }) - It("should", func() { + It("should return true for known repos", func() { Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue()) }) }) - When("warn on head failure is set to \"never\"", func() { - c := newClientNoAPI(false, false, false, false, false, "never") + When(`warn on head failure is set to "never"`, func() { + c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnNever}} It("should never return true", func() { Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse()) Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeFalse()) }) }) }) + When("pulling the latest image", func() { + When("the image consist of a pinned hash", func() { + It("should gracefully fail with a useful message", func() { + c := dockerClient{} + pinnedContainer := MockContainer(WithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b")) + err := c.PullImage(context.Background(), pinnedContainer) + Expect(err).To(MatchError(`container uses a pinned image, and cannot be updated by watchtower`)) + }) + }) + }) + When("removing a running container", func() { + When("the container still exist after stopping", func() { + It("should attempt to remove the container", func() { + container := MockContainer(WithContainerState(types.ContainerState{Running: true})) + containerStopped := MockContainer(WithContainerState(types.ContainerState{Running: false})) + + cid := container.ContainerInfo().ID + mockServer.AppendHandlers( + mocks.KillContainerHandler(cid, mocks.Found), + mocks.GetContainerHandler(cid, containerStopped.ContainerInfo()), + mocks.RemoveContainerHandler(cid, mocks.Found), + mocks.GetContainerHandler(cid, nil), + ) + + Expect(dockerClient{api: docker}.StopContainer(container, time.Minute)).To(Succeed()) + }) + }) + When("the container does not exist after stopping", func() { + It("should not cause an error", func() { + container := MockContainer(WithContainerState(types.ContainerState{Running: true})) + + cid := container.ContainerInfo().ID + mockServer.AppendHandlers( + mocks.KillContainerHandler(cid, mocks.Found), + mocks.GetContainerHandler(cid, nil), + mocks.RemoveContainerHandler(cid, mocks.Missing), + ) + + Expect(dockerClient{api: docker}.StopContainer(container, time.Minute)).To(Succeed()) + }) + }) + }) + When("removing a image", func() { + When("debug logging is enabled", func() { + It("should log removed and untagged images", func() { + imageA := util.GenerateRandomSHA256() + imageAParent := util.GenerateRandomSHA256() + images := map[string][]string{imageA: {imageAParent}} + mockServer.AppendHandlers(mocks.RemoveImageHandler(images)) + c := dockerClient{api: docker} + + resetLogrus, logbuf := captureLogrus(logrus.DebugLevel) + defer resetLogrus() + + Expect(c.RemoveImageByID(t.ImageID(imageA))).To(Succeed()) + + shortA := t.ImageID(imageA).ShortID() + shortAParent := t.ImageID(imageAParent).ShortID() + + Eventually(logbuf).Should(gbytes.Say(`deleted="%v, %v" untagged="?%v"?`, shortA, shortAParent, shortA)) + }) + }) + When("image is not found", func() { + It("should return an error", func() { + image := util.GenerateRandomSHA256() + mockServer.AppendHandlers(mocks.RemoveImageHandler(nil)) + c := dockerClient{api: docker} + + err := c.RemoveImageByID(t.ImageID(image)) + Expect(errdefs.IsNotFound(err)).To(BeTrue()) + }) + }) + }) When("listing containers", func() { When("no filter is provided", func() { It("should return all available containers", func() { mockServer.AppendHandlers(mocks.ListContainersHandler("running")) - mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...) + mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...) client := dockerClient{ - api: docker, - pullImages: false, + api: docker, + ClientOptions: ClientOptions{}, } containers, err := client.ListContainers(filters.NoFilter) Expect(err).NotTo(HaveOccurred()) @@ -76,11 +155,11 @@ var _ = Describe("the client", func() { When("a filter matching nothing", func() { It("should return an empty array", func() { mockServer.AppendHandlers(mocks.ListContainersHandler("running")) - mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...) + mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...) filter := filters.FilterByNames([]string{"lollercoaster"}, filters.NoFilter) client := dockerClient{ - api: docker, - pullImages: false, + api: docker, + ClientOptions: ClientOptions{}, } containers, err := client.ListContainers(filter) Expect(err).NotTo(HaveOccurred()) @@ -90,10 +169,10 @@ var _ = Describe("the client", func() { When("a watchtower filter is provided", func() { It("should return only the watchtower container", func() { mockServer.AppendHandlers(mocks.ListContainersHandler("running")) - mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...) + mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...) client := dockerClient{ - api: docker, - pullImages: false, + api: docker, + ClientOptions: ClientOptions{}, } containers, err := client.ListContainers(filters.WatchtowerContainersFilter) Expect(err).NotTo(HaveOccurred()) @@ -103,11 +182,10 @@ var _ = Describe("the client", func() { When(`include stopped is enabled`, func() { It("should return both stopped and running containers", func() { mockServer.AppendHandlers(mocks.ListContainersHandler("running", "exited", "created")) - mockServer.AppendHandlers(mocks.GetContainerHandlers("stopped", "watchtower", "running")...) + mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Stopped, &mocks.Watchtower, &mocks.Running)...) client := dockerClient{ - api: docker, - pullImages: false, - includeStopped: true, + api: docker, + ClientOptions: ClientOptions{IncludeStopped: true}, } containers, err := client.ListContainers(filters.NoFilter) Expect(err).NotTo(HaveOccurred()) @@ -117,11 +195,10 @@ var _ = Describe("the client", func() { When(`include restarting is enabled`, func() { It("should return both restarting and running containers", func() { mockServer.AppendHandlers(mocks.ListContainersHandler("running", "restarting")) - mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running", "restarting")...) + mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running, &mocks.Restarting)...) client := dockerClient{ - api: docker, - pullImages: false, - includeRestarting: true, + api: docker, + ClientOptions: ClientOptions{IncludeRestarting: true}, } containers, err := client.ListContainers(filters.NoFilter) Expect(err).NotTo(HaveOccurred()) @@ -131,31 +208,58 @@ var _ = Describe("the client", func() { When(`include restarting is disabled`, func() { It("should not return restarting containers", func() { mockServer.AppendHandlers(mocks.ListContainersHandler("running")) - mockServer.AppendHandlers(mocks.GetContainerHandlers("watchtower", "running")...) + mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...) client := dockerClient{ - api: docker, - pullImages: false, - includeRestarting: false, + api: docker, + ClientOptions: ClientOptions{IncludeRestarting: false}, } containers, err := client.ListContainers(filters.NoFilter) Expect(err).NotTo(HaveOccurred()) Expect(containers).NotTo(ContainElement(havingRestartingState(true))) }) }) + When(`a container uses container network mode`, func() { + When(`the network container can be resolved`, func() { + It("should return the container name instead of the ID", func() { + consumerContainerRef := mocks.NetConsumerOK + mockServer.AppendHandlers(mocks.GetContainerHandlers(&consumerContainerRef)...) + client := dockerClient{ + api: docker, + ClientOptions: ClientOptions{}, + } + container, err := client.GetContainer(consumerContainerRef.ContainerID()) + Expect(err).NotTo(HaveOccurred()) + networkMode := container.ContainerInfo().HostConfig.NetworkMode + Expect(networkMode.ConnectedContainer()).To(Equal(mocks.NetSupplierContainerName)) + }) + }) + When(`the network container cannot be resolved`, func() { + It("should still return the container ID", func() { + consumerContainerRef := mocks.NetConsumerInvalidSupplier + mockServer.AppendHandlers(mocks.GetContainerHandlers(&consumerContainerRef)...) + client := dockerClient{ + api: docker, + ClientOptions: ClientOptions{}, + } + container, err := client.GetContainer(consumerContainerRef.ContainerID()) + Expect(err).NotTo(HaveOccurred()) + networkMode := container.ContainerInfo().HostConfig.NetworkMode + Expect(networkMode.ConnectedContainer()).To(Equal(mocks.NetSupplierNotFoundID)) + }) + }) + }) }) Describe(`ExecuteCommand`, func() { When(`logging`, func() { It("should include container id field", func() { client := dockerClient{ - api: docker, - pullImages: false, + api: docker, + ClientOptions: ClientOptions{}, } // Capture logrus output in buffer - logbuf := gbytes.NewBuffer() - origOut := logrus.StandardLogger().Out - defer logrus.SetOutput(origOut) - logrus.SetOutput(logbuf) + resetLogrus, logbuf := captureLogrus(logrus.DebugLevel) + defer resetLogrus() user := "" containerID := t.ContainerID("ex-cont-id") @@ -212,26 +316,62 @@ var _ = Describe("the client", func() { }) }) }) + Describe(`GetNetworkConfig`, func() { + When(`providing a container with network aliases`, func() { + It(`should omit the container ID alias`, func() { + client := dockerClient{ + api: docker, + ClientOptions: ClientOptions{IncludeRestarting: false}, + } + container := MockContainer(WithImageName("docker.io/prefix/imagename:latest")) + + aliases := []string{"One", "Two", container.ID().ShortID(), "Four"} + endpoints := map[string]*network.EndpointSettings{ + `test`: {Aliases: aliases}, + } + container.containerInfo.NetworkSettings = &types.NetworkSettings{Networks: endpoints} + Expect(container.ContainerInfo().NetworkSettings.Networks[`test`].Aliases).To(Equal(aliases)) + Expect(client.GetNetworkConfig(container).EndpointsConfig[`test`].Aliases).To(Equal([]string{"One", "Two", "Four"})) + }) + }) + }) }) +// Capture logrus output in buffer +func captureLogrus(level logrus.Level) (func(), *gbytes.Buffer) { + + logbuf := gbytes.NewBuffer() + + origOut := logrus.StandardLogger().Out + logrus.SetOutput(logbuf) + + origLev := logrus.StandardLogger().Level + logrus.SetLevel(level) + + return func() { + logrus.SetOutput(origOut) + logrus.SetLevel(origLev) + }, logbuf +} + // Gomega matcher helpers -func withContainerImageName(matcher GomegaMatcher) GomegaMatcher { +func withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher { return WithTransform(containerImageName, matcher) } -func containerImageName(container Container) string { +func containerImageName(container t.Container) string { return container.ImageName() } -func havingRestartingState(expected bool) GomegaMatcher { - return WithTransform(func(container Container) bool { - return container.containerInfo.State.Restarting +func havingRestartingState(expected bool) gt.GomegaMatcher { + return WithTransform(func(container t.Container) bool { + return container.ContainerInfo().State.Restarting }, Equal(expected)) } -func havingRunningState(expected bool) GomegaMatcher { - return WithTransform(func(container Container) bool { - return container.containerInfo.State.Running +func havingRunningState(expected bool) gt.GomegaMatcher { + return WithTransform(func(container t.Container) bool { + return container.ContainerInfo().State.Running }, Equal(expected)) } diff --git a/pkg/container/container.go b/pkg/container/container.go index 82ae205..10ed677 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -1,12 +1,15 @@ +// Package container contains code related to dealing with docker containers package container import ( + "errors" "fmt" "strconv" "strings" "github.com/containrrr/watchtower/internal/util" wt "github.com/containrrr/watchtower/pkg/types" + "github.com/sirupsen/logrus" "github.com/docker/docker/api/types" dockercontainer "github.com/docker/docker/api/types/container" @@ -31,6 +34,26 @@ type Container struct { imageInfo *types.ImageInspect } +// IsLinkedToRestarting returns the current value of the LinkedToRestarting field for the container +func (c *Container) IsLinkedToRestarting() bool { + return c.LinkedToRestarting +} + +// IsStale returns the current value of the Stale field for the container +func (c *Container) IsStale() bool { + return c.Stale +} + +// SetLinkedToRestarting sets the LinkedToRestarting field for the container +func (c *Container) SetLinkedToRestarting(value bool) { + c.LinkedToRestarting = value +} + +// SetStale implements sets the Stale field for the container +func (c *Container) SetStale(value bool) { + c.Stale = value +} + // ContainerInfo fetches JSON info for the container func (c Container) ContainerInfo() *types.ContainerJSON { return c.containerInfo @@ -108,20 +131,31 @@ func (c Container) Enabled() (bool, bool) { return parsedBool, true } -// IsMonitorOnly returns the value of the monitor-only label. If the label -// is not set then false is returned. -func (c Container) IsMonitorOnly() bool { - rawBool, ok := c.getLabelValue(monitorOnlyLabel) - if !ok { - return false - } +// IsMonitorOnly returns whether the container should only be monitored based on values of +// the monitor-only label, the monitor-only argument and the label-take-precedence argument. +func (c Container) IsMonitorOnly(params wt.UpdateParams) bool { + return c.getContainerOrGlobalBool(params.MonitorOnly, monitorOnlyLabel, params.LabelPrecedence) +} - parsedBool, err := strconv.ParseBool(rawBool) - if err != nil { - return false - } +// IsNoPull returns whether the image should be pulled based on values of +// the no-pull label, the no-pull argument and the label-take-precedence argument. +func (c Container) IsNoPull(params wt.UpdateParams) bool { + return c.getContainerOrGlobalBool(params.NoPull, noPullLabel, params.LabelPrecedence) +} - return parsedBool +func (c Container) getContainerOrGlobalBool(globalVal bool, label string, contPrecedence bool) bool { + if contVal, err := c.getBoolLabelValue(label); err != nil { + if !errors.Is(err, errorLabelNotFound) { + logrus.WithField("error", err).WithField("label", label).Warn("Failed to parse label value") + } + return globalVal + } else { + if contPrecedence { + return contVal + } else { + return contVal || globalVal + } + } } // Scope returns the value of the scope UID label and if the label @@ -143,7 +177,14 @@ func (c Container) Links() []string { dependsOnLabelValue := c.getLabelValueOrEmpty(dependsOnLabel) if dependsOnLabelValue != "" { - links := strings.Split(dependsOnLabelValue, ",") + for _, link := range strings.Split(dependsOnLabelValue, ",") { + // Since the container names need to start with '/', let's prepend it if it's missing + if !strings.HasPrefix(link, "/") { + link = "/" + link + } + links = append(links, link) + } + return links } @@ -152,6 +193,13 @@ func (c Container) Links() []string { name := strings.Split(link, ":")[0] links = append(links, name) } + + // If the container uses another container for networking, it can be considered an implicit link + // since the container would stop working if the network supplier were to be recreated + networkMode := c.containerInfo.HostConfig.NetworkMode + if networkMode.IsContainer() { + links = append(links, networkMode.ConnectedContainer()) + } } return links @@ -216,18 +264,23 @@ func (c Container) StopSignal() string { return c.getLabelValueOrEmpty(signalLabel) } +// GetCreateConfig returns the container's current Config converted into a format +// that can be re-submitted to the Docker create API. +// // Ideally, we'd just be able to take the ContainerConfig from the old container // and use it as the starting point for creating the new container; however, // the ContainerConfig that comes back from the Inspect call merges the default // configuration (the stuff specified in the metadata for the image itself) // with the overridden configuration (the stuff that you might specify as part -// of the "docker run"). In order to avoid unintentionally overriding the +// of the "docker run"). +// +// In order to avoid unintentionally overriding the // defaults in the new image we need to separate the override options from the // default options. To do this we have to compare the ContainerConfig for the // running container with the ContainerConfig from the image that container was // started from. This function returns a ContainerConfig which contains just // the options overridden at runtime. -func (c Container) runtimeConfig() *dockercontainer.Config { +func (c Container) GetCreateConfig() *dockercontainer.Config { config := c.containerInfo.Config hostConfig := c.containerInfo.HostConfig imageConfig := c.imageInfo.Config @@ -251,6 +304,29 @@ func (c Container) runtimeConfig() *dockercontainer.Config { } } + // Clear HEALTHCHECK configuration (if default) + if config.Healthcheck != nil && imageConfig.Healthcheck != nil { + if util.SliceEqual(config.Healthcheck.Test, imageConfig.Healthcheck.Test) { + config.Healthcheck.Test = nil + } + + if config.Healthcheck.Retries == imageConfig.Healthcheck.Retries { + config.Healthcheck.Retries = 0 + } + + if config.Healthcheck.Interval == imageConfig.Healthcheck.Interval { + config.Healthcheck.Interval = 0 + } + + if config.Healthcheck.Timeout == imageConfig.Healthcheck.Timeout { + config.Healthcheck.Timeout = 0 + } + + if config.Healthcheck.StartPeriod == imageConfig.Healthcheck.StartPeriod { + config.Healthcheck.StartPeriod = 0 + } + } + config.Env = util.SliceSubtract(config.Env, imageConfig.Env) config.Labels = util.StringMapSubtract(config.Labels, imageConfig.Labels) @@ -271,9 +347,9 @@ func (c Container) runtimeConfig() *dockercontainer.Config { return config } -// Any links in the HostConfig need to be re-written before they can be -// re-submitted to the Docker create API. -func (c Container) hostConfig() *dockercontainer.HostConfig { +// GetCreateHostConfig returns the container's current HostConfig with any links +// re-written so that they can be re-submitted to the Docker create API. +func (c Container) GetCreateHostConfig() *dockercontainer.HostConfig { hostConfig := c.containerInfo.HostConfig for i, link := range hostConfig.Links { diff --git a/pkg/container/container_mock_test.go b/pkg/container/container_mock_test.go new file mode 100644 index 0000000..8aa1470 --- /dev/null +++ b/pkg/container/container_mock_test.go @@ -0,0 +1,79 @@ +package container + +import ( + "github.com/docker/docker/api/types" + dockerContainer "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" +) + +type MockContainerUpdate func(*types.ContainerJSON, *types.ImageInspect) + +func MockContainer(updates ...MockContainerUpdate) *Container { + containerInfo := types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + ID: "container_id", + Image: "image", + Name: "test-containrrr", + HostConfig: &dockerContainer.HostConfig{}, + }, + Config: &dockerContainer.Config{ + Labels: map[string]string{}, + }, + } + image := types.ImageInspect{ + ID: "image_id", + Config: &dockerContainer.Config{}, + } + + for _, update := range updates { + update(&containerInfo, &image) + } + return NewContainer(&containerInfo, &image) +} + +func WithPortBindings(portBindingSources ...string) MockContainerUpdate { + return func(c *types.ContainerJSON, i *types.ImageInspect) { + portBindings := nat.PortMap{} + for _, pbs := range portBindingSources { + portBindings[nat.Port(pbs)] = []nat.PortBinding{} + } + c.HostConfig.PortBindings = portBindings + } +} + +func WithImageName(name string) MockContainerUpdate { + return func(c *types.ContainerJSON, i *types.ImageInspect) { + c.Config.Image = name + i.RepoTags = append(i.RepoTags, name) + } +} + +func WithLinks(links []string) MockContainerUpdate { + return func(c *types.ContainerJSON, i *types.ImageInspect) { + c.HostConfig.Links = links + } +} + +func WithLabels(labels map[string]string) MockContainerUpdate { + return func(c *types.ContainerJSON, i *types.ImageInspect) { + c.Config.Labels = labels + } +} + +func WithContainerState(state types.ContainerState) MockContainerUpdate { + return func(cnt *types.ContainerJSON, img *types.ImageInspect) { + cnt.State = &state + } +} + +func WithHealthcheck(healthConfig dockerContainer.HealthConfig) MockContainerUpdate { + return func(cnt *types.ContainerJSON, img *types.ImageInspect) { + cnt.Config.Healthcheck = &healthConfig + } +} + +func WithImageHealthcheck(healthConfig dockerContainer.HealthConfig) MockContainerUpdate { + return func(cnt *types.ContainerJSON, img *types.ImageInspect) { + img.Config.Healthcheck = &healthConfig + } +} diff --git a/pkg/container/container_test.go b/pkg/container/container_test.go index 1a4e956..a129afe 100644 --- a/pkg/container/container_test.go +++ b/pkg/container/container_test.go @@ -1,8 +1,8 @@ package container import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" + "github.com/containrrr/watchtower/pkg/types" + dc "github.com/docker/docker/api/types/container" "github.com/docker/go-connections/nat" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -12,7 +12,7 @@ var _ = Describe("the container", func() { Describe("VerifyConfiguration", func() { When("verifying a container with no image info", func() { It("should return an error", func() { - c := mockContainerWithPortBindings() + c := MockContainer(WithPortBindings()) c.imageInfo = nil err := c.VerifyConfiguration() Expect(err).To(Equal(errorNoImageInfo)) @@ -20,7 +20,7 @@ var _ = Describe("the container", func() { }) When("verifying a container with no container info", func() { It("should return an error", func() { - c := mockContainerWithPortBindings() + c := MockContainer(WithPortBindings()) c.containerInfo = nil err := c.VerifyConfiguration() Expect(err).To(Equal(errorNoContainerInfo)) @@ -28,7 +28,7 @@ var _ = Describe("the container", func() { }) When("verifying a container with no config", func() { It("should return an error", func() { - c := mockContainerWithPortBindings() + c := MockContainer(WithPortBindings()) c.containerInfo.Config = nil err := c.VerifyConfiguration() Expect(err).To(Equal(errorInvalidConfig)) @@ -36,7 +36,7 @@ var _ = Describe("the container", func() { }) When("verifying a container with no host config", func() { It("should return an error", func() { - c := mockContainerWithPortBindings() + c := MockContainer(WithPortBindings()) c.containerInfo.HostConfig = nil err := c.VerifyConfiguration() Expect(err).To(Equal(errorInvalidConfig)) @@ -44,14 +44,14 @@ var _ = Describe("the container", func() { }) When("verifying a container with no port bindings", func() { It("should not return an error", func() { - c := mockContainerWithPortBindings() + c := MockContainer(WithPortBindings()) err := c.VerifyConfiguration() Expect(err).ToNot(HaveOccurred()) }) }) When("verifying a container with port bindings, but no exposed ports", func() { It("should make the config compatible with updating", func() { - c := mockContainerWithPortBindings("80/tcp") + c := MockContainer(WithPortBindings("80/tcp")) c.containerInfo.Config.ExposedPorts = nil Expect(c.VerifyConfiguration()).To(Succeed()) @@ -61,20 +61,107 @@ var _ = Describe("the container", func() { }) When("verifying a container with port bindings and exposed ports is non-nil", func() { It("should return an error", func() { - c := mockContainerWithPortBindings("80/tcp") + c := MockContainer(WithPortBindings("80/tcp")) c.containerInfo.Config.ExposedPorts = map[nat.Port]struct{}{"80/tcp": {}} err := c.VerifyConfiguration() Expect(err).ToNot(HaveOccurred()) }) }) }) + Describe("GetCreateConfig", func() { + When("container healthcheck config is equal to image config", func() { + It("should return empty healthcheck values", func() { + c := MockContainer(WithHealthcheck(dc.HealthConfig{ + Test: []string{"/usr/bin/sleep", "1s"}, + }), WithImageHealthcheck(dc.HealthConfig{ + Test: []string{"/usr/bin/sleep", "1s"}, + })) + Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{})) + + c = MockContainer(WithHealthcheck(dc.HealthConfig{ + Timeout: 30, + }), WithImageHealthcheck(dc.HealthConfig{ + Timeout: 30, + })) + Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{})) + + c = MockContainer(WithHealthcheck(dc.HealthConfig{ + StartPeriod: 30, + }), WithImageHealthcheck(dc.HealthConfig{ + StartPeriod: 30, + })) + Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{})) + + c = MockContainer(WithHealthcheck(dc.HealthConfig{ + Retries: 30, + }), WithImageHealthcheck(dc.HealthConfig{ + Retries: 30, + })) + Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{})) + }) + }) + When("container healthcheck config is different to image config", func() { + It("should return the container healthcheck values", func() { + c := MockContainer(WithHealthcheck(dc.HealthConfig{ + Test: []string{"/usr/bin/sleep", "1s"}, + Interval: 30, + Timeout: 30, + StartPeriod: 10, + Retries: 2, + }), WithImageHealthcheck(dc.HealthConfig{ + Test: []string{"/usr/bin/sleep", "10s"}, + Interval: 10, + Timeout: 60, + StartPeriod: 30, + Retries: 10, + })) + Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{ + Test: []string{"/usr/bin/sleep", "1s"}, + Interval: 30, + Timeout: 30, + StartPeriod: 10, + Retries: 2, + })) + }) + }) + When("container healthcheck config is empty", func() { + It("should not panic", func() { + c := MockContainer(WithImageHealthcheck(dc.HealthConfig{ + Test: []string{"/usr/bin/sleep", "10s"}, + Interval: 10, + Timeout: 60, + StartPeriod: 30, + Retries: 10, + })) + Expect(c.GetCreateConfig().Healthcheck).To(BeNil()) + }) + }) + When("container image healthcheck config is empty", func() { + It("should not panic", func() { + c := MockContainer(WithHealthcheck(dc.HealthConfig{ + Test: []string{"/usr/bin/sleep", "1s"}, + Interval: 30, + Timeout: 30, + StartPeriod: 10, + Retries: 2, + })) + Expect(c.GetCreateConfig().Healthcheck).To(Equal(&dc.HealthConfig{ + Test: []string{"/usr/bin/sleep", "1s"}, + Interval: 30, + Timeout: 30, + StartPeriod: 10, + Retries: 2, + })) + }) + }) + }) When("asked for metadata", func() { var c *Container BeforeEach(func() { - c = mockContainerWithLabels(map[string]string{ + c = MockContainer(WithLabels(map[string]string{ "com.centurylinklabs.watchtower.enable": "true", "com.centurylinklabs.watchtower": "true", - }) + })) }) It("should return its name on calls to .Name()", func() { name := c.Name() @@ -91,36 +178,28 @@ var _ = Describe("the container", func() { enabled, exists := c.Enabled() Expect(enabled).To(BeTrue()) - Expect(enabled).NotTo(BeFalse()) Expect(exists).To(BeTrue()) - Expect(exists).NotTo(BeFalse()) }) It("should return false, true if present but not true on calls to .Enabled()", func() { - c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "false"}) + c = MockContainer(WithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "false"})) enabled, exists := c.Enabled() Expect(enabled).To(BeFalse()) - Expect(enabled).NotTo(BeTrue()) Expect(exists).To(BeTrue()) - Expect(exists).NotTo(BeFalse()) }) It("should return false, false if not present on calls to .Enabled()", func() { - c = mockContainerWithLabels(map[string]string{"lol": "false"}) + c = MockContainer(WithLabels(map[string]string{"lol": "false"})) enabled, exists := c.Enabled() Expect(enabled).To(BeFalse()) - Expect(enabled).NotTo(BeTrue()) Expect(exists).To(BeFalse()) - Expect(exists).NotTo(BeTrue()) }) It("should return false, false if present but not parsable .Enabled()", func() { - c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "falsy"}) + c = MockContainer(WithLabels(map[string]string{"com.centurylinklabs.watchtower.enable": "falsy"})) enabled, exists := c.Enabled() Expect(enabled).To(BeFalse()) - Expect(enabled).NotTo(BeTrue()) Expect(exists).To(BeFalse()) - Expect(exists).NotTo(BeTrue()) }) When("checking if its a watchtower instance", func() { It("should return true if the label is set to true", func() { @@ -128,31 +207,31 @@ var _ = Describe("the container", func() { Expect(isWatchtower).To(BeTrue()) }) It("should return false if the label is present but set to false", func() { - c = mockContainerWithLabels(map[string]string{"com.centurylinklabs.watchtower": "false"}) + c = MockContainer(WithLabels(map[string]string{"com.centurylinklabs.watchtower": "false"})) isWatchtower := c.IsWatchtower() Expect(isWatchtower).To(BeFalse()) }) It("should return false if the label is not present", func() { - c = mockContainerWithLabels(map[string]string{"funny.label": "false"}) + c = MockContainer(WithLabels(map[string]string{"funny.label": "false"})) isWatchtower := c.IsWatchtower() Expect(isWatchtower).To(BeFalse()) }) It("should return false if there are no labels", func() { - c = mockContainerWithLabels(map[string]string{}) + c = MockContainer(WithLabels(map[string]string{})) isWatchtower := c.IsWatchtower() Expect(isWatchtower).To(BeFalse()) }) }) When("fetching the custom stop signal", func() { It("should return the signal if its set", func() { - c = mockContainerWithLabels(map[string]string{ + c = MockContainer(WithLabels(map[string]string{ "com.centurylinklabs.watchtower.stop-signal": "SIGKILL", - }) + })) stopSignal := c.StopSignal() Expect(stopSignal).To(Equal("SIGKILL")) }) It("should return an empty string if its not set", func() { - c = mockContainerWithLabels(map[string]string{}) + c = MockContainer(WithLabels(map[string]string{})) stopSignal := c.StopSignal() Expect(stopSignal).To(Equal("")) }) @@ -160,22 +239,22 @@ var _ = Describe("the container", func() { When("fetching the image name", func() { When("the zodiac label is present", func() { It("should fetch the image name from it", func() { - c = mockContainerWithLabels(map[string]string{ + c = MockContainer(WithLabels(map[string]string{ "com.centurylinklabs.zodiac.original-image": "the-original-image", - }) + })) imageName := c.ImageName() Expect(imageName).To(Equal(imageName)) }) }) It("should return the image name", func() { name := "image-name:3" - c = mockContainerWithImageName(name) + c = MockContainer(WithImageName(name)) imageName := c.ImageName() Expect(imageName).To(Equal(name)) }) It("should assume latest if no tag is supplied", func() { name := "image-name" - c = mockContainerWithImageName(name) + c = MockContainer(WithImageName(name)) imageName := c.ImageName() Expect(imageName).To(Equal(name + ":latest")) }) @@ -184,113 +263,129 @@ var _ = Describe("the container", func() { When("fetching container links", func() { When("the depends on label is present", func() { It("should fetch depending containers from it", func() { - c = mockContainerWithLabels(map[string]string{ + c = MockContainer(WithLabels(map[string]string{ "com.centurylinklabs.watchtower.depends-on": "postgres", - }) + })) links := c.Links() - Expect(links).To(SatisfyAll(ContainElement("postgres"), HaveLen(1))) + Expect(links).To(SatisfyAll(ContainElement("/postgres"), HaveLen(1))) }) It("should fetch depending containers if there are many", func() { - c = mockContainerWithLabels(map[string]string{ + c = MockContainer(WithLabels(map[string]string{ "com.centurylinklabs.watchtower.depends-on": "postgres,redis", - }) + })) links := c.Links() - Expect(links).To(SatisfyAll(ContainElement("postgres"), ContainElement("redis"), HaveLen(2))) + Expect(links).To(SatisfyAll(ContainElement("/postgres"), ContainElement("/redis"), HaveLen(2))) + }) + It("should only add slashes to names when they are missing", func() { + c = MockContainer(WithLabels(map[string]string{ + "com.centurylinklabs.watchtower.depends-on": "/postgres,redis", + })) + links := c.Links() + Expect(links).To(SatisfyAll(ContainElement("/postgres"), ContainElement("/redis"))) }) It("should fetch depending containers if label is blank", func() { - c = mockContainerWithLabels(map[string]string{ + c = MockContainer(WithLabels(map[string]string{ "com.centurylinklabs.watchtower.depends-on": "", - }) + })) links := c.Links() Expect(links).To(HaveLen(0)) }) }) When("the depends on label is not present", func() { It("should fetch depending containers from host config links", func() { - c = mockContainerWithLinks([]string{ + c = MockContainer(WithLinks([]string{ "redis:test-containrrr", "postgres:test-containrrr", - }) + })) links := c.Links() Expect(links).To(SatisfyAll(ContainElement("redis"), ContainElement("postgres"), HaveLen(2))) }) }) }) - + + When("checking no-pull label", func() { + When("no-pull argument is not set", func() { + When("no-pull label is true", func() { + c := MockContainer(WithLabels(map[string]string{ + "com.centurylinklabs.watchtower.no-pull": "true", + })) + It("should return true", func() { + Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(true)) + }) + }) + When("no-pull label is false", func() { + c := MockContainer(WithLabels(map[string]string{ + "com.centurylinklabs.watchtower.no-pull": "false", + })) + It("should return false", func() { + Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(false)) + }) + }) + When("no-pull label is set to an invalid value", func() { + c := MockContainer(WithLabels(map[string]string{ + "com.centurylinklabs.watchtower.no-pull": "maybe", + })) + It("should return false", func() { + Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(false)) + }) + }) + When("no-pull label is unset", func() { + c = MockContainer(WithLabels(map[string]string{})) + It("should return false", func() { + Expect(c.IsNoPull(types.UpdateParams{})).To(Equal(false)) + }) + }) + }) + When("no-pull argument is set to true", func() { + When("no-pull label is true", func() { + c := MockContainer(WithLabels(map[string]string{ + "com.centurylinklabs.watchtower.no-pull": "true", + })) + It("should return true", func() { + Expect(c.IsNoPull(types.UpdateParams{NoPull: true})).To(Equal(true)) + }) + }) + When("no-pull label is false", func() { + c := MockContainer(WithLabels(map[string]string{ + "com.centurylinklabs.watchtower.no-pull": "false", + })) + It("should return true", func() { + Expect(c.IsNoPull(types.UpdateParams{NoPull: true})).To(Equal(true)) + }) + }) + When("label-take-precedence argument is set to true", func() { + When("no-pull label is true", func() { + c := MockContainer(WithLabels(map[string]string{ + "com.centurylinklabs.watchtower.no-pull": "true", + })) + It("should return true", func() { + Expect(c.IsNoPull(types.UpdateParams{LabelPrecedence: true, NoPull: true})).To(Equal(true)) + }) + }) + When("no-pull label is false", func() { + c := MockContainer(WithLabels(map[string]string{ + "com.centurylinklabs.watchtower.no-pull": "false", + })) + It("should return false", func() { + Expect(c.IsNoPull(types.UpdateParams{LabelPrecedence: true, NoPull: true})).To(Equal(false)) + }) + }) + }) + }) + }) + When("there is a pre or post update timeout", func() { - It("should return minute values", func() { - c = mockContainerWithLabels(map[string]string{ - "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout": "3", - "com.centurylinklabs.watchtower.lifecycle.post-update-timeout": "5", - }) - preTimeout := c.PreUpdateTimeout() - Expect(preTimeout).To(Equal(3)) - postTimeout := c.PostUpdateTimeout() - Expect(postTimeout).To(Equal(5)) - }) - }) - + It("should return minute values", func() { + c = MockContainer(WithLabels(map[string]string{ + "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout": "3", + "com.centurylinklabs.watchtower.lifecycle.post-update-timeout": "5", + })) + preTimeout := c.PreUpdateTimeout() + Expect(preTimeout).To(Equal(3)) + postTimeout := c.PostUpdateTimeout() + Expect(postTimeout).To(Equal(5)) + }) + }) + }) }) - -func mockContainerWithPortBindings(portBindingSources ...string) *Container { - mockContainer := mockContainerWithLabels(nil) - mockContainer.imageInfo = &types.ImageInspect{} - hostConfig := &container.HostConfig{ - PortBindings: nat.PortMap{}, - } - for _, pbs := range portBindingSources { - hostConfig.PortBindings[nat.Port(pbs)] = []nat.PortBinding{} - } - mockContainer.containerInfo.HostConfig = hostConfig - return mockContainer -} - -func mockContainerWithImageName(name string) *Container { - mockContainer := mockContainerWithLabels(nil) - mockContainer.containerInfo.Config.Image = name - return mockContainer -} - -func mockContainerWithLinks(links []string) *Container { - content := types.ContainerJSON{ - ContainerJSONBase: &types.ContainerJSONBase{ - ID: "container_id", - Image: "image", - Name: "test-containrrr", - HostConfig: &container.HostConfig{ - Links: links, - }, - }, - Config: &container.Config{ - Labels: map[string]string{}, - }, - } - return NewContainer(&content, nil) -} - -func mockContainerWithLabels(labels map[string]string) *Container { - content := types.ContainerJSON{ - ContainerJSONBase: &types.ContainerJSONBase{ - ID: "container_id", - Image: "image", - Name: "test-containrrr", - }, - Config: &container.Config{ - Labels: labels, - }, - } - return NewContainer(&content, nil) -} - -func newClientNoAPI(pullImages, includeStopped, reviveStopped, removeVolumes, includeRestarting bool, warnOnHeadFailed string) Client { - return dockerClient{ - api: nil, - pullImages: pullImages, - removeVolumes: removeVolumes, - includeStopped: includeStopped, - reviveStopped: reviveStopped, - includeRestarting: includeRestarting, - warnOnHeadFailed: warnOnHeadFailed, - } -} diff --git a/pkg/container/errors.go b/pkg/container/errors.go index 1937430..05dc722 100644 --- a/pkg/container/errors.go +++ b/pkg/container/errors.go @@ -4,5 +4,5 @@ import "errors" var errorNoImageInfo = errors.New("no available image info") var errorNoContainerInfo = errors.New("no available container info") -var errorNoExposedPorts = errors.New("exposed ports does not match port bindings") var errorInvalidConfig = errors.New("container configuration missing or invalid") +var errorLabelNotFound = errors.New("label was not found in container") diff --git a/pkg/container/metadata.go b/pkg/container/metadata.go index ee9fddf..8ac5f34 100644 --- a/pkg/container/metadata.go +++ b/pkg/container/metadata.go @@ -1,18 +1,21 @@ package container +import "strconv" + const ( - watchtowerLabel = "com.centurylinklabs.watchtower" - signalLabel = "com.centurylinklabs.watchtower.stop-signal" - enableLabel = "com.centurylinklabs.watchtower.enable" - monitorOnlyLabel = "com.centurylinklabs.watchtower.monitor-only" - dependsOnLabel = "com.centurylinklabs.watchtower.depends-on" - zodiacLabel = "com.centurylinklabs.zodiac.original-image" - scope = "com.centurylinklabs.watchtower.scope" - preCheckLabel = "com.centurylinklabs.watchtower.lifecycle.pre-check" - postCheckLabel = "com.centurylinklabs.watchtower.lifecycle.post-check" - preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update" - postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update" - preUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout" + watchtowerLabel = "com.centurylinklabs.watchtower" + signalLabel = "com.centurylinklabs.watchtower.stop-signal" + enableLabel = "com.centurylinklabs.watchtower.enable" + monitorOnlyLabel = "com.centurylinklabs.watchtower.monitor-only" + noPullLabel = "com.centurylinklabs.watchtower.no-pull" + dependsOnLabel = "com.centurylinklabs.watchtower.depends-on" + zodiacLabel = "com.centurylinklabs.zodiac.original-image" + scope = "com.centurylinklabs.watchtower.scope" + preCheckLabel = "com.centurylinklabs.watchtower.lifecycle.pre-check" + postCheckLabel = "com.centurylinklabs.watchtower.lifecycle.post-check" + preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update" + postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update" + preUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout" postUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.post-update-timeout" ) @@ -54,3 +57,11 @@ func (c Container) getLabelValue(label string) (string, bool) { val, ok := c.containerInfo.Config.Labels[label] return val, ok } + +func (c Container) getBoolLabelValue(label string) (bool, error) { + if strVal, ok := c.containerInfo.Config.Labels[label]; ok { + value, err := strconv.ParseBool(strVal) + return value, err + } + return false, errorLabelNotFound +} diff --git a/pkg/container/mocks/ApiServer.go b/pkg/container/mocks/ApiServer.go index 20610cd..84756f0 100644 --- a/pkg/container/mocks/ApiServer.go +++ b/pkg/container/mocks/ApiServer.go @@ -3,22 +3,26 @@ package mocks import ( "encoding/json" "fmt" + "github.com/onsi/ginkgo" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + + t "github.com/containrrr/watchtower/pkg/types" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" O "github.com/onsi/gomega" "github.com/onsi/gomega/ghttp" - "io/ioutil" - "net/http" - "net/url" - "path/filepath" ) func getMockJSONFile(relPath string) ([]byte, error) { absPath, _ := filepath.Abs(relPath) - buf, err := ioutil.ReadFile(absPath) + buf, err := os.ReadFile(absPath) if err != nil { - // logrus.WithError(err).WithField("file", absPath).Error(err) - return nil, err + return nil, fmt.Errorf("mock JSON file %q not found: %e", absPath, err) } return buf, nil } @@ -39,19 +43,22 @@ func respondWithJSONFile(relPath string, statusCode int, optionalHeader ...http. } // GetContainerHandlers returns the handlers serving lookups for the supplied container mock files -func GetContainerHandlers(containerFiles ...string) []http.HandlerFunc { - handlers := make([]http.HandlerFunc, 0, len(containerFiles)*2) - for _, file := range containerFiles { - handlers = append(handlers, getContainerHandler(file)) +func GetContainerHandlers(containerRefs ...*ContainerRef) []http.HandlerFunc { + handlers := make([]http.HandlerFunc, 0, len(containerRefs)*3) + for _, containerRef := range containerRefs { + handlers = append(handlers, getContainerFileHandler(containerRef)) + + // Also append any containers that the container references, if any + for _, ref := range containerRef.references { + handlers = append(handlers, getContainerFileHandler(ref)) + } // Also append the image request since that will be called for every container - if file == "running" { - // The "running" container is the only one using image02 - handlers = append(handlers, getImageHandler(1)) - } else { - handlers = append(handlers, getImageHandler(0)) - } + handlers = append(handlers, getImageHandler(containerRef.image.id, + RespondWithJSONFile(containerRef.image.getFileName(), http.StatusOK), + )) } + return handlers } @@ -63,34 +70,120 @@ func createFilterArgs(statuses []string) filters.Args { return args } -var containerFileIds = map[string]string{ - "stopped": "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65", - "watchtower": "3d88e0e3543281c747d88b27e246578b65ae8964ba86c7cd7522cf84e0978134", - "running": "b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008", - "restarting": "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b67", +var defaultImage = imageRef{ + // watchtower + id: t.ImageID("sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa"), + file: "default", } -var imageIds = []string{ - "sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa", - "sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd", +var Watchtower = ContainerRef{ + name: "watchtower", + id: "3d88e0e3543281c747d88b27e246578b65ae8964ba86c7cd7522cf84e0978134", + image: &defaultImage, +} +var Stopped = ContainerRef{ + name: "stopped", + id: "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65", + image: &defaultImage, +} +var Running = ContainerRef{ + name: "running", + id: "b978af0b858aa8855cce46b628817d4ed58e58f2c4f66c9b9c5449134ed4c008", + image: &imageRef{ + // portainer + id: t.ImageID("sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd"), + file: "running", + }, +} +var Restarting = ContainerRef{ + name: "restarting", + id: "ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b67", + image: &defaultImage, } -func getContainerHandler(file string) http.HandlerFunc { - id, ok := containerFileIds[file] - failTestUnless(ok) - return ghttp.CombineHandlers( - ghttp.VerifyRequest("GET", O.HaveSuffix("/containers/%v/json", id)), - RespondWithJSONFile(fmt.Sprintf("./mocks/data/container_%v.json", file), http.StatusOK), +var netSupplierOK = ContainerRef{ + id: "25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2", + name: "net_supplier", + image: &imageRef{ + // gluetun + id: t.ImageID("sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51"), + file: "net_producer", + }, +} +var netSupplierNotFound = ContainerRef{ + id: NetSupplierNotFoundID, + name: netSupplierOK.name, + isMissing: true, +} + +// NetConsumerOK is used for testing `container` networking mode +// returns a container that consumes an existing supplier container +var NetConsumerOK = ContainerRef{ + id: "1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6", + name: "net_consumer", + image: &imageRef{ + id: t.ImageID("sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8"), // nginx + file: "net_consumer", + }, + references: []*ContainerRef{&netSupplierOK}, +} + +// NetConsumerInvalidSupplier is used for testing `container` networking mode +// returns a container that references a supplying container that does not exist +var NetConsumerInvalidSupplier = ContainerRef{ + id: NetConsumerOK.id, + name: "net_consumer-missing_supplier", + image: NetConsumerOK.image, + references: []*ContainerRef{&netSupplierNotFound}, +} + +const NetSupplierNotFoundID = "badc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc" +const NetSupplierContainerName = "/wt-contnet-producer-1" + +func getContainerFileHandler(cr *ContainerRef) http.HandlerFunc { + + if cr.isMissing { + return containerNotFoundResponse(string(cr.id)) + } + + containerFile, err := cr.getContainerFile() + if err != nil { + ginkgo.Fail(fmt.Sprintf("Failed to get container mock file: %v", err)) + } + + return getContainerHandler( + string(cr.id), + RespondWithJSONFile(containerFile, http.StatusOK), ) } +func getContainerHandler(containerId string, responseHandler http.HandlerFunc) http.HandlerFunc { + return ghttp.CombineHandlers( + ghttp.VerifyRequest("GET", O.HaveSuffix("/containers/%v/json", containerId)), + responseHandler, + ) +} + +// GetContainerHandler mocks the GET containers/{id}/json endpoint +func GetContainerHandler(containerID string, containerInfo *types.ContainerJSON) http.HandlerFunc { + responseHandler := containerNotFoundResponse(containerID) + if containerInfo != nil { + responseHandler = ghttp.RespondWithJSONEncoded(http.StatusOK, containerInfo) + } + return getContainerHandler(containerID, responseHandler) +} + +// GetImageHandler mocks the GET images/{id}/json endpoint +func GetImageHandler(imageInfo *types.ImageInspect) http.HandlerFunc { + return getImageHandler(t.ImageID(imageInfo.ID), ghttp.RespondWithJSONEncoded(http.StatusOK, imageInfo)) +} + // ListContainersHandler mocks the GET containers/json endpoint, filtering the returned containers based on statuses func ListContainersHandler(statuses ...string) http.HandlerFunc { filterArgs := createFilterArgs(statuses) bytes, err := filterArgs.MarshalJSON() O.ExpectWithOffset(1, err).ShouldNot(O.HaveOccurred()) query := url.Values{ - "limit": []string{"0"}, "filters": []string{string(bytes)}, } return ghttp.CombineHandlers( @@ -116,13 +209,72 @@ func respondWithFilteredContainers(filters filters.Args) http.HandlerFunc { return ghttp.RespondWithJSONEncoded(http.StatusOK, filteredContainers) } -func getImageHandler(index int) http.HandlerFunc { +func getImageHandler(imageId t.ImageID, responseHandler http.HandlerFunc) http.HandlerFunc { return ghttp.CombineHandlers( - ghttp.VerifyRequest("GET", O.HaveSuffix("/images/%v/json", imageIds[index])), - RespondWithJSONFile(fmt.Sprintf("./mocks/data/image%02d.json", index+1), http.StatusOK), + ghttp.VerifyRequest("GET", O.HaveSuffix("/images/%s/json", imageId)), + responseHandler, ) } -func failTestUnless(ok bool) { - O.ExpectWithOffset(2, ok).To(O.BeTrue(), "test setup failed") +// KillContainerHandler mocks the POST containers/{id}/kill endpoint +func KillContainerHandler(containerID string, found FoundStatus) http.HandlerFunc { + responseHandler := noContentStatusResponse + if !found { + responseHandler = containerNotFoundResponse(containerID) + } + return ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", O.HaveSuffix("containers/%s/kill", containerID)), + responseHandler, + ) +} + +// RemoveContainerHandler mocks the DELETE containers/{id} endpoint +func RemoveContainerHandler(containerID string, found FoundStatus) http.HandlerFunc { + responseHandler := noContentStatusResponse + if !found { + responseHandler = containerNotFoundResponse(containerID) + } + return ghttp.CombineHandlers( + ghttp.VerifyRequest("DELETE", O.HaveSuffix("containers/%s", containerID)), + responseHandler, + ) +} + +func containerNotFoundResponse(containerID string) http.HandlerFunc { + return ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{message: "No such container: " + string(containerID)}) +} + +var noContentStatusResponse = ghttp.RespondWith(http.StatusNoContent, nil) + +type FoundStatus bool + +const ( + Found FoundStatus = true + Missing FoundStatus = false +) + +// RemoveImageHandler mocks the DELETE images/ID endpoint, simulating removal of the given imagesWithParents +func RemoveImageHandler(imagesWithParents map[string][]string) http.HandlerFunc { + return ghttp.CombineHandlers( + ghttp.VerifyRequest("DELETE", O.MatchRegexp("/images/.*")), + func(w http.ResponseWriter, r *http.Request) { + parts := strings.Split(r.URL.Path, `/`) + image := parts[len(parts)-1] + + if parents, found := imagesWithParents[image]; found { + items := []types.ImageDeleteResponseItem{ + {Untagged: image}, + {Deleted: image}, + } + for _, parent := range parents { + items = append(items, types.ImageDeleteResponseItem{Deleted: parent}) + } + ghttp.RespondWithJSONEncoded(http.StatusOK, items)(w, r) + } else { + ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{ + message: "Something went wrong.", + })(w, r) + } + }, + ) } diff --git a/pkg/container/mocks/FilterableContainer.go b/pkg/container/mocks/FilterableContainer.go index 1ae8125..fa863b5 100644 --- a/pkg/container/mocks/FilterableContainer.go +++ b/pkg/container/mocks/FilterableContainer.go @@ -78,3 +78,17 @@ func (_m *FilterableContainer) Scope() (string, bool) { return r0, r1 } + +// ImageName provides a mock function with given fields: +func (_m *FilterableContainer) ImageName() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} diff --git a/pkg/container/mocks/container_ref.go b/pkg/container/mocks/container_ref.go new file mode 100644 index 0000000..c46eb93 --- /dev/null +++ b/pkg/container/mocks/container_ref.go @@ -0,0 +1,42 @@ +package mocks + +import ( + "fmt" + "os" + + t "github.com/containrrr/watchtower/pkg/types" +) + +type imageRef struct { + id t.ImageID + file string +} + +func (ir *imageRef) getFileName() string { + return fmt.Sprintf("./mocks/data/image_%v.json", ir.file) +} + +type ContainerRef struct { + name string + id t.ContainerID + image *imageRef + file string + references []*ContainerRef + isMissing bool +} + +func (cr *ContainerRef) getContainerFile() (containerFile string, err error) { + file := cr.file + if file == "" { + file = cr.name + } + + containerFile = fmt.Sprintf("./mocks/data/container_%v.json", file) + _, err = os.Stat(containerFile) + + return containerFile, err +} + +func (cr *ContainerRef) ContainerID() t.ContainerID { + return cr.id +} diff --git a/pkg/container/mocks/data/container_net_consumer-missing_supplier.json b/pkg/container/mocks/data/container_net_consumer-missing_supplier.json new file mode 100644 index 0000000..c1a233b --- /dev/null +++ b/pkg/container/mocks/data/container_net_consumer-missing_supplier.json @@ -0,0 +1,205 @@ +{ + "Id": "1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6", + "Created": "2023-07-25T14:55:14.69155887Z", + "Path": "/docker-entrypoint.sh", + "Args": [ + "nginx", + "-g", + "daemon off;" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 3743, + "ExitCode": 0, + "Error": "", + "StartedAt": "2023-07-25T14:55:15.299654437Z", + "FinishedAt": "0001-01-01T00:00:00Z" + }, + "Image": "sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8", + "ResolvConfPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hostname", + "HostsPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hosts", + "LogPath": "/var/lib/docker/containers/1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6/1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6-json.log", + "Name": "/wt-contnet-consumer-1", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "NetworkMode": "container:badc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc1dbadc", + "PortBindings": {}, + "RestartPolicy": { + "Name": "", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 0, + 0 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "host", + "Dns": null, + "DnsOptions": null, + "DnsSearch": null, + "ExtraHosts": [], + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 0, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": null, + "BlkioDeviceReadBps": null, + "BlkioDeviceWriteBps": null, + "BlkioDeviceReadIOps": null, + "BlkioDeviceWriteIOps": null, + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": null, + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": false, + "PidsLimit": null, + "Ulimits": null, + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2-init/diff:/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/diff:/var/lib/docker/overlay2/09785ba17f27c783ef8b44f369f9aac0ca936000b57abf22b3c54d1e6eb8e27b/diff:/var/lib/docker/overlay2/6f8acd64ae44fd4d14bcb90c105eceba46854aa3985b5b6b317bcc5692cfc286/diff:/var/lib/docker/overlay2/73d41c15edb21c5f12cf53e313f48b5da55283aafc77d35b7bc662241879d7e7/diff:/var/lib/docker/overlay2/d97b55f3d966ae031492369a98e9e00d2bd31e520290fe2034e0a2b1ed77c91e/diff:/var/lib/docker/overlay2/053e9ca65c6b64cb9d98a812ff7488c7e77938b4fb8e0c4d2ad7f8ec235f0f20/diff", + "MergedDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/merged", + "UpperDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/diff", + "WorkDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/work" + }, + "Name": "overlay2" + }, + "Mounts": [], + "Config": { + "Hostname": "25e75393800b", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "ExposedPorts": { + "80/tcp": {} + }, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "NGINX_VERSION=1.23.3", + "NJS_VERSION=0.7.9", + "PKG_RELEASE=1~bullseye" + ], + "Cmd": [ + "nginx", + "-g", + "daemon off;" + ], + "Image": "nginx", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [ + "/docker-entrypoint.sh" + ], + "OnBuild": null, + "Labels": { + "com.docker.compose.config-hash": "8bb0e1c8c61f6d495840ba9133ebfb1e4ffda3e1adb701a011b03951848bb9fa", + "com.docker.compose.container-number": "1", + "com.docker.compose.depends_on": "producer:service_started:false", + "com.docker.compose.image": "sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8", + "com.docker.compose.oneoff": "False", + "com.docker.compose.project": "wt-contnet", + "com.docker.compose.project.config_files": "/tmp/wt-contnet/docker-compose.yaml", + "com.docker.compose.project.working_dir": "/tmp/wt-contnet", + "com.docker.compose.replace": "07bb70608f96f577aa02b9f317500e23e691c94eb099f6fb52301dfb031d0668", + "com.docker.compose.service": "consumer", + "com.docker.compose.version": "2.19.1", + "desktop.docker.io/wsl-distro": "Ubuntu", + "maintainer": "NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e" + }, + "StopSignal": "SIGQUIT" + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": {}, + "SandboxKey": "", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "", + "Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "MacAddress": "", + "Networks": {} + } +} diff --git a/pkg/container/mocks/data/container_net_consumer.json b/pkg/container/mocks/data/container_net_consumer.json new file mode 100644 index 0000000..2e64f89 --- /dev/null +++ b/pkg/container/mocks/data/container_net_consumer.json @@ -0,0 +1,205 @@ +{ + "Id": "1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6", + "Created": "2023-07-25T14:55:14.69155887Z", + "Path": "/docker-entrypoint.sh", + "Args": [ + "nginx", + "-g", + "daemon off;" + ], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 3743, + "ExitCode": 0, + "Error": "", + "StartedAt": "2023-07-25T14:55:15.299654437Z", + "FinishedAt": "0001-01-01T00:00:00Z" + }, + "Image": "sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8", + "ResolvConfPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hostname", + "HostsPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hosts", + "LogPath": "/var/lib/docker/containers/1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6/1f6b79d2aff23244382026c76f4995851322bed5f9c50631620162f6f9aafbd6-json.log", + "Name": "/wt-contnet-consumer-1", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "NetworkMode": "container:25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2", + "PortBindings": {}, + "RestartPolicy": { + "Name": "", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 0, + 0 + ], + "CapAdd": null, + "CapDrop": null, + "CgroupnsMode": "host", + "Dns": null, + "DnsOptions": null, + "DnsSearch": null, + "ExtraHosts": [], + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 0, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": null, + "BlkioDeviceReadBps": null, + "BlkioDeviceWriteBps": null, + "BlkioDeviceReadIOps": null, + "BlkioDeviceWriteIOps": null, + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": null, + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": false, + "PidsLimit": null, + "Ulimits": null, + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2-init/diff:/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/diff:/var/lib/docker/overlay2/09785ba17f27c783ef8b44f369f9aac0ca936000b57abf22b3c54d1e6eb8e27b/diff:/var/lib/docker/overlay2/6f8acd64ae44fd4d14bcb90c105eceba46854aa3985b5b6b317bcc5692cfc286/diff:/var/lib/docker/overlay2/73d41c15edb21c5f12cf53e313f48b5da55283aafc77d35b7bc662241879d7e7/diff:/var/lib/docker/overlay2/d97b55f3d966ae031492369a98e9e00d2bd31e520290fe2034e0a2b1ed77c91e/diff:/var/lib/docker/overlay2/053e9ca65c6b64cb9d98a812ff7488c7e77938b4fb8e0c4d2ad7f8ec235f0f20/diff", + "MergedDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/merged", + "UpperDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/diff", + "WorkDir": "/var/lib/docker/overlay2/05501c86219af9f713c74c129426cf5a17dc5e42f96f7f881f443cab100280e2/work" + }, + "Name": "overlay2" + }, + "Mounts": [], + "Config": { + "Hostname": "25e75393800b", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "ExposedPorts": { + "80/tcp": {} + }, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "NGINX_VERSION=1.23.3", + "NJS_VERSION=0.7.9", + "PKG_RELEASE=1~bullseye" + ], + "Cmd": [ + "nginx", + "-g", + "daemon off;" + ], + "Image": "nginx", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [ + "/docker-entrypoint.sh" + ], + "OnBuild": null, + "Labels": { + "com.docker.compose.config-hash": "8bb0e1c8c61f6d495840ba9133ebfb1e4ffda3e1adb701a011b03951848bb9fa", + "com.docker.compose.container-number": "1", + "com.docker.compose.depends_on": "producer:service_started:false", + "com.docker.compose.image": "sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8", + "com.docker.compose.oneoff": "False", + "com.docker.compose.project": "wt-contnet", + "com.docker.compose.project.config_files": "/tmp/wt-contnet/docker-compose.yaml", + "com.docker.compose.project.working_dir": "/tmp/wt-contnet", + "com.docker.compose.replace": "07bb70608f96f577aa02b9f317500e23e691c94eb099f6fb52301dfb031d0668", + "com.docker.compose.service": "consumer", + "com.docker.compose.version": "2.19.1", + "desktop.docker.io/wsl-distro": "Ubuntu", + "maintainer": "NGINX Docker Maintainers \u003cdocker-maint@nginx.com\u003e" + }, + "StopSignal": "SIGQUIT" + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": {}, + "SandboxKey": "", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "", + "Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "MacAddress": "", + "Networks": {} + } +} diff --git a/pkg/container/mocks/data/container_net_supplier.json b/pkg/container/mocks/data/container_net_supplier.json new file mode 100644 index 0000000..24db841 --- /dev/null +++ b/pkg/container/mocks/data/container_net_supplier.json @@ -0,0 +1,380 @@ +{ + "Id": "25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2", + "Created": "2023-07-25T14:55:14.595662628Z", + "Path": "/gluetun-entrypoint", + "Args": [], + "State": { + "Status": "running", + "Running": true, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 3648, + "ExitCode": 0, + "Error": "", + "StartedAt": "2023-07-25T14:55:15.193430103Z", + "FinishedAt": "0001-01-01T00:00:00Z", + "Health": { + "Status": "healthy", + "FailingStreak": 0, + "Log": [ + { + "Start": "2023-07-25T15:00:32.078491228Z", + "End": "2023-07-25T15:00:32.194554876Z", + "ExitCode": 0, + "Output": "" + }, + { + "Start": "2023-07-25T15:00:37.199245496Z", + "End": "2023-07-25T15:00:37.294845687Z", + "ExitCode": 0, + "Output": "" + }, + { + "Start": "2023-07-25T15:00:42.299676089Z", + "End": "2023-07-25T15:00:42.384213818Z", + "ExitCode": 0, + "Output": "" + }, + { + "Start": "2023-07-25T15:00:47.389142447Z", + "End": "2023-07-25T15:00:47.514483294Z", + "ExitCode": 0, + "Output": "" + }, + { + "Start": "2023-07-25T15:00:52.518770886Z", + "End": "2023-07-25T15:00:52.644288742Z", + "ExitCode": 0, + "Output": "" + } + ] + } + }, + "Image": "sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51", + "ResolvConfPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hostname", + "HostsPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/hosts", + "LogPath": "/var/lib/docker/containers/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2/25e75393800b5c450a6841212a3b92ed28fa35414a586dec9f2c8a520d4910c2-json.log", + "Name": "/wt-contnet-producer-1", + "RestartCount": 0, + "Driver": "overlay2", + "Platform": "linux", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "NetworkMode": "wt-contnet_default", + "PortBindings": {}, + "RestartPolicy": { + "Name": "", + "MaximumRetryCount": 0 + }, + "AutoRemove": false, + "VolumeDriver": "", + "VolumesFrom": null, + "ConsoleSize": [ + 0, + 0 + ], + "CapAdd": [ + "NET_ADMIN" + ], + "CapDrop": null, + "CgroupnsMode": "host", + "Dns": null, + "DnsOptions": null, + "DnsSearch": null, + "ExtraHosts": [], + "GroupAdd": null, + "IpcMode": "private", + "Cgroup": "", + "Links": null, + "OomScoreAdj": 0, + "PidMode": "", + "Privileged": false, + "PublishAllPorts": false, + "ReadonlyRootfs": false, + "SecurityOpt": null, + "UTSMode": "", + "UsernsMode": "", + "ShmSize": 67108864, + "Runtime": "runc", + "Isolation": "", + "CpuShares": 0, + "Memory": 0, + "NanoCpus": 0, + "CgroupParent": "", + "BlkioWeight": 0, + "BlkioWeightDevice": null, + "BlkioDeviceReadBps": null, + "BlkioDeviceWriteBps": null, + "BlkioDeviceReadIOps": null, + "BlkioDeviceWriteIOps": null, + "CpuPeriod": 0, + "CpuQuota": 0, + "CpuRealtimePeriod": 0, + "CpuRealtimeRuntime": 0, + "CpusetCpus": "", + "CpusetMems": "", + "Devices": null, + "DeviceCgroupRules": null, + "DeviceRequests": null, + "MemoryReservation": 0, + "MemorySwap": 0, + "MemorySwappiness": null, + "OomKillDisable": false, + "PidsLimit": null, + "Ulimits": null, + "CpuCount": 0, + "CpuPercent": 0, + "IOMaximumIOps": 0, + "IOMaximumBandwidth": 0, + "MaskedPaths": [ + "/proc/asound", + "/proc/acpi", + "/proc/kcore", + "/proc/keys", + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + "/sys/firmware" + ], + "ReadonlyPaths": [ + "/proc/bus", + "/proc/fs", + "/proc/irq", + "/proc/sys", + "/proc/sysrq-trigger" + ] + }, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/618bd1e7a13880c07ec7f5bfc45012a9f81d5de452f942b49d8f49b3c67a19a2-init/diff:/var/lib/docker/overlay2/0d222a3aa067159831c4111a408e40325be1085b935c98d39c2e9a01ff50b224/diff:/var/lib/docker/overlay2/a20c9490a23ee8af51898892d9bf32258d44e0e07f3799475be8e8f273a50f73/diff:/var/lib/docker/overlay2/d4c97f367c37c6ada9de57f438a3e19cc714be2a54a6f582a03de9e42d88b344/diff", + "MergedDir": "/var/lib/docker/overlay2/618bd1e7a13880c07ec7f5bfc45012a9f81d5de452f942b49d8f49b3c67a19a2/merged", + "UpperDir": "/var/lib/docker/overlay2/618bd1e7a13880c07ec7f5bfc45012a9f81d5de452f942b49d8f49b3c67a19a2/diff", + "WorkDir": "/var/lib/docker/overlay2/618bd1e7a13880c07ec7f5bfc45012a9f81d5de452f942b49d8f49b3c67a19a2/work" + }, + "Name": "overlay2" + }, + "Mounts": [], + "Config": { + "Hostname": "25e75393800b", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "ExposedPorts": { + "8000/tcp": {}, + "8388/tcp": {}, + "8388/udp": {}, + "8888/tcp": {} + }, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "OPENVPN_PASSWORD=", + "SERVER_COUNTRIES=Sweden", + "VPN_SERVICE_PROVIDER=nordvpn", + "OPENVPN_USER=", + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "VPN_TYPE=openvpn", + "VPN_ENDPOINT_IP=", + "VPN_ENDPOINT_PORT=", + "VPN_INTERFACE=tun0", + "OPENVPN_PROTOCOL=udp", + "OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user", + "OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password", + "OPENVPN_VERSION=2.5", + "OPENVPN_VERBOSITY=1", + "OPENVPN_FLAGS=", + "OPENVPN_CIPHERS=", + "OPENVPN_AUTH=", + "OPENVPN_PROCESS_USER=root", + "OPENVPN_CUSTOM_CONFIG=", + "WIREGUARD_PRIVATE_KEY=", + "WIREGUARD_PRESHARED_KEY=", + "WIREGUARD_PUBLIC_KEY=", + "WIREGUARD_ALLOWED_IPS=", + "WIREGUARD_ADDRESSES=", + "WIREGUARD_MTU=1400", + "WIREGUARD_IMPLEMENTATION=auto", + "SERVER_REGIONS=", + "SERVER_CITIES=", + "SERVER_HOSTNAMES=", + "ISP=", + "OWNED_ONLY=no", + "PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET=", + "VPN_PORT_FORWARDING=off", + "VPN_PORT_FORWARDING_PROVIDER=", + "VPN_PORT_FORWARDING_STATUS_FILE=/tmp/gluetun/forwarded_port", + "OPENVPN_CERT=", + "OPENVPN_KEY=", + "OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt", + "OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey", + "OPENVPN_ENCRYPTED_KEY=", + "OPENVPN_ENCRYPTED_KEY_SECRETFILE=/run/secrets/openvpn_encrypted_key", + "OPENVPN_KEY_PASSPHRASE=", + "OPENVPN_KEY_PASSPHRASE_SECRETFILE=/run/secrets/openvpn_key_passphrase", + "SERVER_NUMBER=", + "SERVER_NAMES=", + "FREE_ONLY=", + "MULTIHOP_ONLY=", + "PREMIUM_ONLY=", + "FIREWALL=on", + "FIREWALL_VPN_INPUT_PORTS=", + "FIREWALL_INPUT_PORTS=", + "FIREWALL_OUTBOUND_SUBNETS=", + "FIREWALL_DEBUG=off", + "LOG_LEVEL=info", + "HEALTH_SERVER_ADDRESS=127.0.0.1:9999", + "HEALTH_TARGET_ADDRESS=cloudflare.com:443", + "HEALTH_SUCCESS_WAIT_DURATION=5s", + "HEALTH_VPN_DURATION_INITIAL=6s", + "HEALTH_VPN_DURATION_ADDITION=5s", + "DOT=on", + "DOT_PROVIDERS=cloudflare", + "DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112", + "DOT_VERBOSITY=1", + "DOT_VERBOSITY_DETAILS=0", + "DOT_VALIDATION_LOGLEVEL=0", + "DOT_CACHING=on", + "DOT_IPV6=off", + "BLOCK_MALICIOUS=on", + "BLOCK_SURVEILLANCE=off", + "BLOCK_ADS=off", + "UNBLOCK=", + "DNS_UPDATE_PERIOD=24h", + "DNS_ADDRESS=127.0.0.1", + "DNS_KEEP_NAMESERVER=off", + "HTTPPROXY=", + "HTTPPROXY_LOG=off", + "HTTPPROXY_LISTENING_ADDRESS=:8888", + "HTTPPROXY_STEALTH=off", + "HTTPPROXY_USER=", + "HTTPPROXY_PASSWORD=", + "HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user", + "HTTPPROXY_PASSWORD_SECRETFILE=/run/secrets/httpproxy_password", + "SHADOWSOCKS=off", + "SHADOWSOCKS_LOG=off", + "SHADOWSOCKS_LISTENING_ADDRESS=:8388", + "SHADOWSOCKS_PASSWORD=", + "SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password", + "SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305", + "HTTP_CONTROL_SERVER_LOG=on", + "HTTP_CONTROL_SERVER_ADDRESS=:8000", + "UPDATER_PERIOD=0", + "UPDATER_MIN_RATIO=0.8", + "UPDATER_VPN_SERVICE_PROVIDERS=", + "PUBLICIP_FILE=/tmp/gluetun/ip", + "PUBLICIP_PERIOD=12h", + "PPROF_ENABLED=no", + "PPROF_BLOCK_PROFILE_RATE=0", + "PPROF_MUTEX_PROFILE_RATE=0", + "PPROF_HTTP_SERVER_ADDRESS=:6060", + "VERSION_INFORMATION=on", + "TZ=", + "PUID=", + "PGID=" + ], + "Cmd": null, + "Healthcheck": { + "Test": [ + "CMD-SHELL", + "/gluetun-entrypoint healthcheck" + ], + "Interval": 5000000000, + "Timeout": 5000000000, + "StartPeriod": 10000000000, + "Retries": 1 + }, + "Image": "qmcgaw/gluetun", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [ + "/gluetun-entrypoint" + ], + "OnBuild": null, + "Labels": { + "com.docker.compose.config-hash": "6dc7dc42a86edb47039de3650a9cb9bdcf4866c113b8f9d797722c9dfd20428b", + "com.docker.compose.container-number": "1", + "com.docker.compose.depends_on": "", + "com.docker.compose.image": "sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51", + "com.docker.compose.oneoff": "False", + "com.docker.compose.project": "wt-contnet", + "com.docker.compose.project.config_files": "/tmp/wt-contnet/docker-compose.yaml", + "com.docker.compose.project.working_dir": "/tmp/wt-contnet", + "com.docker.compose.replace": "9bd1ce000be81819fc915aa60a1674c7573b59a26ac4643ecf427a5732b9785f", + "com.docker.compose.service": "producer", + "com.docker.compose.version": "2.19.1", + "desktop.docker.io/wsl-distro": "Ubuntu", + "org.opencontainers.image.authors": "quentin.mcgaw@gmail.com", + "org.opencontainers.image.created": "2023-07-22T16:07:05.641Z", + "org.opencontainers.image.description": "VPN client in a thin Docker container for multiple VPN providers, written in Go, and using OpenVPN or Wireguard, DNS over TLS, with a few proxy servers built-in.", + "org.opencontainers.image.documentation": "https://github.com/qdm12/gluetun", + "org.opencontainers.image.licenses": "MIT", + "org.opencontainers.image.revision": "eecfb3952f202c0de3867d88e96d80c6b0f48359", + "org.opencontainers.image.source": "https://github.com/qdm12/gluetun", + "org.opencontainers.image.title": "gluetun", + "org.opencontainers.image.url": "https://github.com/qdm12/gluetun", + "org.opencontainers.image.version": "latest" + } + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "34a321b64bb1b15f994dfccff0e235f881504f240c2028876ff6683962eaa10e", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": { + "8000/tcp": null, + "8388/tcp": null, + "8388/udp": null, + "8888/tcp": null + }, + "SandboxKey": "/var/run/docker/netns/34a321b64bb1", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "", + "Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "MacAddress": "", + "Networks": { + "wt-contnet_default": { + "IPAMConfig": null, + "Links": null, + "Aliases": [ + "wt-contnet-producer-1", + "producer", + "25e75393800b" + ], + "NetworkID": "f0f652a79efc54bcad52aafb4cbcc3b5dce1acaf11b172d8678d25f665faf63d", + "EndpointID": "2429c2b5d08db6c986bbd419a52ca4dd352715d80c5aeae04742efb84b0356fc", + "Gateway": "172.19.0.1", + "IPAddress": "172.19.0.2", + "IPPrefixLen": 16, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "02:42:ac:13:00:02", + "DriverOpts": null + } + } + } +} diff --git a/pkg/container/mocks/data/image01.json b/pkg/container/mocks/data/image_default.json similarity index 100% rename from pkg/container/mocks/data/image01.json rename to pkg/container/mocks/data/image_default.json diff --git a/pkg/container/mocks/data/image_net_consumer.json b/pkg/container/mocks/data/image_net_consumer.json new file mode 100644 index 0000000..add6edf --- /dev/null +++ b/pkg/container/mocks/data/image_net_consumer.json @@ -0,0 +1,115 @@ +{ + "Id": "sha256:904b8cb13b932e23230836850610fa45dce9eb0650d5618c2b1487c2a4f577b8", + "RepoTags": [ + "nginx:latest" + ], + "RepoDigests": [ + "nginx@sha256:aa0afebbb3cfa473099a62c4b32e9b3fb73ed23f2a75a65ce1d4b4f55a5c2ef2" + ], + "Parent": "", + "Comment": "", + "Created": "2023-03-01T18:43:12.914398123Z", + "Container": "71a4c9a59d252d7c54812429bfe5df477e54e91ebfff1939ae39ecdf055d445c", + "ContainerConfig": { + "Hostname": "71a4c9a59d25", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "ExposedPorts": { + "80/tcp": {} + }, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "NGINX_VERSION=1.23.3", + "NJS_VERSION=0.7.9", + "PKG_RELEASE=1~bullseye" + ], + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "CMD [\"nginx\" \"-g\" \"daemon off;\"]" + ], + "Image": "sha256:6716b8a33f73b21e193bb63424ea1105eaaa6a8237fefe75570bea18c87a1711", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [ + "/docker-entrypoint.sh" + ], + "OnBuild": null, + "Labels": { + "maintainer": "NGINX Docker Maintainers " + }, + "StopSignal": "SIGQUIT" + }, + "DockerVersion": "20.10.23", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "ExposedPorts": { + "80/tcp": {} + }, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "NGINX_VERSION=1.23.3", + "NJS_VERSION=0.7.9", + "PKG_RELEASE=1~bullseye" + ], + "Cmd": [ + "nginx", + "-g", + "daemon off;" + ], + "Image": "sha256:6716b8a33f73b21e193bb63424ea1105eaaa6a8237fefe75570bea18c87a1711", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [ + "/docker-entrypoint.sh" + ], + "OnBuild": null, + "Labels": { + "maintainer": "NGINX Docker Maintainers " + }, + "StopSignal": "SIGQUIT" + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 141838643, + "VirtualSize": 141838643, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/09785ba17f27c783ef8b44f369f9aac0ca936000b57abf22b3c54d1e6eb8e27b/diff:/var/lib/docker/overlay2/6f8acd64ae44fd4d14bcb90c105eceba46854aa3985b5b6b317bcc5692cfc286/diff:/var/lib/docker/overlay2/73d41c15edb21c5f12cf53e313f48b5da55283aafc77d35b7bc662241879d7e7/diff:/var/lib/docker/overlay2/d97b55f3d966ae031492369a98e9e00d2bd31e520290fe2034e0a2b1ed77c91e/diff:/var/lib/docker/overlay2/053e9ca65c6b64cb9d98a812ff7488c7e77938b4fb8e0c4d2ad7f8ec235f0f20/diff", + "MergedDir": "/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/merged", + "UpperDir": "/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/diff", + "WorkDir": "/var/lib/docker/overlay2/105427179e5628eb7e893d53e21f42f9e76278f8b5665387ecdeed54a7231137/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:650abce4b096b06ac8bec2046d821d66d801af34f1f1d4c5e272ad030c7873db", + "sha256:4dc5cd799a08ff49a603870c8378ea93083bfc2a4176f56e5531997e94c195d0", + "sha256:e161c82b34d21179db1f546c1cd84153d28a17d865ccaf2dedeb06a903fec12c", + "sha256:83ba6d8ffb8c2974174c02d3ba549e7e0656ebb1bc075a6b6ee89b6c609c6a71", + "sha256:d8466e142d8710abf5b495ebb536478f7e19d9d03b151b5d5bd09df4cfb49248", + "sha256:101af4ba983b04be266217ecee414e88b23e394f62e9801c7c1bdb37cb37bcaa" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/pkg/container/mocks/data/image_net_producer.json b/pkg/container/mocks/data/image_net_producer.json new file mode 100644 index 0000000..563ad95 --- /dev/null +++ b/pkg/container/mocks/data/image_net_producer.json @@ -0,0 +1,210 @@ +{ + "Id": "sha256:c22b543d33bfdcb9992cbef23961677133cdf09da71d782468ae2517138bad51", + "RepoTags": [ + "qmcgaw/gluetun:latest" + ], + "RepoDigests": [ + "qmcgaw/gluetun@sha256:cd532bf4ef88a348a915c6dc62a9867a2eca89aa70559b0b4a1ea15cc0e595d1" + ], + "Parent": "", + "Comment": "buildkit.dockerfile.v0", + "Created": "2023-07-22T16:10:29.457146856Z", + "Container": "", + "ContainerConfig": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": null, + "Image": "", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "DockerVersion": "", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "ExposedPorts": { + "8000/tcp": {}, + "8388/tcp": {}, + "8388/udp": {}, + "8888/tcp": {} + }, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "VPN_SERVICE_PROVIDER=pia", + "VPN_TYPE=openvpn", + "VPN_ENDPOINT_IP=", + "VPN_ENDPOINT_PORT=", + "VPN_INTERFACE=tun0", + "OPENVPN_PROTOCOL=udp", + "OPENVPN_USER=", + "OPENVPN_PASSWORD=", + "OPENVPN_USER_SECRETFILE=/run/secrets/openvpn_user", + "OPENVPN_PASSWORD_SECRETFILE=/run/secrets/openvpn_password", + "OPENVPN_VERSION=2.5", + "OPENVPN_VERBOSITY=1", + "OPENVPN_FLAGS=", + "OPENVPN_CIPHERS=", + "OPENVPN_AUTH=", + "OPENVPN_PROCESS_USER=root", + "OPENVPN_CUSTOM_CONFIG=", + "WIREGUARD_PRIVATE_KEY=", + "WIREGUARD_PRESHARED_KEY=", + "WIREGUARD_PUBLIC_KEY=", + "WIREGUARD_ALLOWED_IPS=", + "WIREGUARD_ADDRESSES=", + "WIREGUARD_MTU=1400", + "WIREGUARD_IMPLEMENTATION=auto", + "SERVER_REGIONS=", + "SERVER_COUNTRIES=", + "SERVER_CITIES=", + "SERVER_HOSTNAMES=", + "ISP=", + "OWNED_ONLY=no", + "PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET=", + "VPN_PORT_FORWARDING=off", + "VPN_PORT_FORWARDING_PROVIDER=", + "VPN_PORT_FORWARDING_STATUS_FILE=/tmp/gluetun/forwarded_port", + "OPENVPN_CERT=", + "OPENVPN_KEY=", + "OPENVPN_CLIENTCRT_SECRETFILE=/run/secrets/openvpn_clientcrt", + "OPENVPN_CLIENTKEY_SECRETFILE=/run/secrets/openvpn_clientkey", + "OPENVPN_ENCRYPTED_KEY=", + "OPENVPN_ENCRYPTED_KEY_SECRETFILE=/run/secrets/openvpn_encrypted_key", + "OPENVPN_KEY_PASSPHRASE=", + "OPENVPN_KEY_PASSPHRASE_SECRETFILE=/run/secrets/openvpn_key_passphrase", + "SERVER_NUMBER=", + "SERVER_NAMES=", + "FREE_ONLY=", + "MULTIHOP_ONLY=", + "PREMIUM_ONLY=", + "FIREWALL=on", + "FIREWALL_VPN_INPUT_PORTS=", + "FIREWALL_INPUT_PORTS=", + "FIREWALL_OUTBOUND_SUBNETS=", + "FIREWALL_DEBUG=off", + "LOG_LEVEL=info", + "HEALTH_SERVER_ADDRESS=127.0.0.1:9999", + "HEALTH_TARGET_ADDRESS=cloudflare.com:443", + "HEALTH_SUCCESS_WAIT_DURATION=5s", + "HEALTH_VPN_DURATION_INITIAL=6s", + "HEALTH_VPN_DURATION_ADDITION=5s", + "DOT=on", + "DOT_PROVIDERS=cloudflare", + "DOT_PRIVATE_ADDRESS=127.0.0.1/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128,fc00::/7,fe80::/10,::ffff:7f00:1/104,::ffff:a00:0/104,::ffff:a9fe:0/112,::ffff:ac10:0/108,::ffff:c0a8:0/112", + "DOT_VERBOSITY=1", + "DOT_VERBOSITY_DETAILS=0", + "DOT_VALIDATION_LOGLEVEL=0", + "DOT_CACHING=on", + "DOT_IPV6=off", + "BLOCK_MALICIOUS=on", + "BLOCK_SURVEILLANCE=off", + "BLOCK_ADS=off", + "UNBLOCK=", + "DNS_UPDATE_PERIOD=24h", + "DNS_ADDRESS=127.0.0.1", + "DNS_KEEP_NAMESERVER=off", + "HTTPPROXY=", + "HTTPPROXY_LOG=off", + "HTTPPROXY_LISTENING_ADDRESS=:8888", + "HTTPPROXY_STEALTH=off", + "HTTPPROXY_USER=", + "HTTPPROXY_PASSWORD=", + "HTTPPROXY_USER_SECRETFILE=/run/secrets/httpproxy_user", + "HTTPPROXY_PASSWORD_SECRETFILE=/run/secrets/httpproxy_password", + "SHADOWSOCKS=off", + "SHADOWSOCKS_LOG=off", + "SHADOWSOCKS_LISTENING_ADDRESS=:8388", + "SHADOWSOCKS_PASSWORD=", + "SHADOWSOCKS_PASSWORD_SECRETFILE=/run/secrets/shadowsocks_password", + "SHADOWSOCKS_CIPHER=chacha20-ietf-poly1305", + "HTTP_CONTROL_SERVER_LOG=on", + "HTTP_CONTROL_SERVER_ADDRESS=:8000", + "UPDATER_PERIOD=0", + "UPDATER_MIN_RATIO=0.8", + "UPDATER_VPN_SERVICE_PROVIDERS=", + "PUBLICIP_FILE=/tmp/gluetun/ip", + "PUBLICIP_PERIOD=12h", + "PPROF_ENABLED=no", + "PPROF_BLOCK_PROFILE_RATE=0", + "PPROF_MUTEX_PROFILE_RATE=0", + "PPROF_HTTP_SERVER_ADDRESS=:6060", + "VERSION_INFORMATION=on", + "TZ=", + "PUID=", + "PGID=" + ], + "Cmd": null, + "Healthcheck": { + "Test": [ + "CMD-SHELL", + "/gluetun-entrypoint healthcheck" + ], + "Interval": 5000000000, + "Timeout": 5000000000, + "StartPeriod": 10000000000, + "Retries": 1 + }, + "Image": "", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": [ + "/gluetun-entrypoint" + ], + "OnBuild": null, + "Labels": { + "org.opencontainers.image.authors": "quentin.mcgaw@gmail.com", + "org.opencontainers.image.created": "2023-07-22T16:07:05.641Z", + "org.opencontainers.image.description": "VPN client in a thin Docker container for multiple VPN providers, written in Go, and using OpenVPN or Wireguard, DNS over TLS, with a few proxy servers built-in.", + "org.opencontainers.image.documentation": "https://github.com/qdm12/gluetun", + "org.opencontainers.image.licenses": "MIT", + "org.opencontainers.image.revision": "eecfb3952f202c0de3867d88e96d80c6b0f48359", + "org.opencontainers.image.source": "https://github.com/qdm12/gluetun", + "org.opencontainers.image.title": "gluetun", + "org.opencontainers.image.url": "https://github.com/qdm12/gluetun", + "org.opencontainers.image.version": "latest" + } + }, + "Architecture": "amd64", + "Os": "linux", + "Size": 42602255, + "VirtualSize": 42602255, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/a20c9490a23ee8af51898892d9bf32258d44e0e07f3799475be8e8f273a50f73/diff:/var/lib/docker/overlay2/d4c97f367c37c6ada9de57f438a3e19cc714be2a54a6f582a03de9e42d88b344/diff", + "MergedDir": "/var/lib/docker/overlay2/0d222a3aa067159831c4111a408e40325be1085b935c98d39c2e9a01ff50b224/merged", + "UpperDir": "/var/lib/docker/overlay2/0d222a3aa067159831c4111a408e40325be1085b935c98d39c2e9a01ff50b224/diff", + "WorkDir": "/var/lib/docker/overlay2/0d222a3aa067159831c4111a408e40325be1085b935c98d39c2e9a01ff50b224/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:78a822fe2a2d2c84f3de4a403188c45f623017d6a4521d23047c9fbb0801794c", + "sha256:122dbeefc08382d88b3fe57ad81c1e2428af5b81c172d112723a33e2a20fe880", + "sha256:3d215e55b88a99dcd7cf4349618326ab129771e12fdf6c6ef5cbb71a265dbb6c" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/pkg/container/mocks/data/image02.json b/pkg/container/mocks/data/image_running.json similarity index 100% rename from pkg/container/mocks/data/image02.json rename to pkg/container/mocks/data/image_running.json diff --git a/pkg/filters/filters.go b/pkg/filters/filters.go index 18f39c2..4fa0bcd 100644 --- a/pkg/filters/filters.go +++ b/pkg/filters/filters.go @@ -1,8 +1,10 @@ package filters import ( - t "github.com/containrrr/watchtower/pkg/types" + "regexp" "strings" + + t "github.com/containrrr/watchtower/pkg/types" ) // WatchtowerContainersFilter filters only watchtower containers @@ -11,7 +13,7 @@ func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatch // NoFilter will not filter out any containers func NoFilter(t.FilterableContainer) bool { return true } -// FilterByNames returns all containers that match the specified name +// FilterByNames returns all containers that match one of the specified names func FilterByNames(names []string, baseFilter t.Filter) t.Filter { if len(names) == 0 { return baseFilter @@ -19,14 +21,42 @@ func FilterByNames(names []string, baseFilter t.Filter) t.Filter { return func(c t.FilterableContainer) bool { for _, name := range names { - if (name == c.Name()) || (name == c.Name()[1:]) { + if name == c.Name() || name == c.Name()[1:] { return baseFilter(c) } + + if re, err := regexp.Compile(name); err == nil { + indices := re.FindStringIndex(c.Name()) + if indices == nil { + continue + } + start := indices[0] + end := indices[1] + if start <= 1 && end >= len(c.Name())-1 { + return baseFilter(c) + } + } } return false } } +// FilterByDisableNames returns all containers that don't match any of the specified names +func FilterByDisableNames(disableNames []string, baseFilter t.Filter) t.Filter { + if len(disableNames) == 0 { + return baseFilter + } + + return func(c t.FilterableContainer) bool { + for _, name := range disableNames { + if name == c.Name() || name == c.Name()[1:] { + return false + } + } + return baseFilter(c) + } +} + // FilterByEnableLabel returns all containers that have the enabled label set func FilterByEnableLabel(baseFilter t.Filter) t.Filter { return func(c t.FilterableContainer) bool { @@ -56,13 +86,14 @@ func FilterByDisabledLabel(baseFilter t.Filter) t.Filter { // FilterByScope returns all containers that belongs to a specific scope func FilterByScope(scope string, baseFilter t.Filter) t.Filter { - if scope == "" { - return baseFilter - } - return func(c t.FilterableContainer) bool { - containerScope, ok := c.Scope() - if ok && containerScope == scope { + containerScope, containerHasScope := c.Scope() + + if !containerHasScope || containerScope == "" { + containerScope = "none" + } + + if containerScope == scope { return baseFilter(c) } @@ -70,14 +101,33 @@ func FilterByScope(scope string, baseFilter t.Filter) t.Filter { } } +// FilterByImage returns all containers that have a specific image +func FilterByImage(images []string, baseFilter t.Filter) t.Filter { + if images == nil { + return baseFilter + } + + return func(c t.FilterableContainer) bool { + image := strings.Split(c.ImageName(), ":")[0] + for _, targetImage := range images { + if image == targetImage { + return baseFilter(c) + } + } + + return false + } +} + // BuildFilter creates the needed filter of containers -func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, string) { +func BuildFilter(names []string, disableNames []string, enableLabel bool, scope string) (t.Filter, string) { sb := strings.Builder{} filter := NoFilter filter = FilterByNames(names, filter) + filter = FilterByDisableNames(disableNames, filter) if len(names) > 0 { - sb.WriteString("with name \"") + sb.WriteString("which name matches \"") for i, n := range names { sb.WriteString(n) if i < len(names)-1 { @@ -86,6 +136,16 @@ func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, stri } sb.WriteString(`", `) } + if len(disableNames) > 0 { + sb.WriteString("not named one of \"") + for i, n := range disableNames { + sb.WriteString(n) + if i < len(disableNames)-1 { + sb.WriteString(`" or "`) + } + } + sb.WriteString(`", `) + } if enableLabel { // If label filtering is enabled, containers should only be considered @@ -93,7 +153,13 @@ func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, stri filter = FilterByEnableLabel(filter) sb.WriteString("using enable label, ") } - if scope != "" { + + if scope == "none" { + // If a scope has explicitly defined as "none", containers should only be considered + // if they do not have a scope defined, or if it's explicitly set to "none". + filter = FilterByScope(scope, filter) + sb.WriteString(`without a scope, "`) + } else if scope != "" { // If a scope has been defined, containers should only be considered // if the scope is specifically set. filter = FilterByScope(scope, filter) diff --git a/pkg/filters/filters_test.go b/pkg/filters/filters_test.go index 3b52b5e..2b5cb5e 100644 --- a/pkg/filters/filters_test.go +++ b/pkg/filters/filters_test.go @@ -47,6 +47,28 @@ func TestFilterByNames(t *testing.T) { container.AssertExpectations(t) } +func TestFilterByNamesRegex(t *testing.T) { + names := []string{`ba(b|ll)oon`} + + filter := FilterByNames(names, NoFilter) + assert.NotNil(t, filter) + + container := new(mocks.FilterableContainer) + container.On("Name").Return("balloon") + assert.True(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Name").Return("spoon") + assert.False(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Name").Return("baboonious") + assert.False(t, filter(container)) + container.AssertExpectations(t) +} + func TestFilterByEnableLabel(t *testing.T) { filter := FilterByEnableLabel(NoFilter) assert.NotNil(t, filter) @@ -68,8 +90,7 @@ func TestFilterByEnableLabel(t *testing.T) { } func TestFilterByScope(t *testing.T) { - var scope string - scope = "testscope" + scope := "testscope" filter := FilterByScope(scope, NoFilter) assert.NotNil(t, filter) @@ -90,6 +111,53 @@ func TestFilterByScope(t *testing.T) { container.AssertExpectations(t) } +func TestFilterByNoneScope(t *testing.T) { + scope := "none" + + filter := FilterByScope(scope, NoFilter) + assert.NotNil(t, filter) + + container := new(mocks.FilterableContainer) + container.On("Scope").Return("anyscope", true) + assert.False(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Scope").Return("", false) + assert.True(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Scope").Return("", true) + assert.True(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Scope").Return("none", true) + assert.True(t, filter(container)) + container.AssertExpectations(t) +} + +func TestBuildFilterNoneScope(t *testing.T) { + filter, desc := BuildFilter(nil, nil, false, "none") + + assert.Contains(t, desc, "without a scope") + + scoped := new(mocks.FilterableContainer) + scoped.On("Enabled").Return(false, false) + scoped.On("Scope").Return("anyscope", true) + + unscoped := new(mocks.FilterableContainer) + unscoped.On("Enabled").Return(false, false) + unscoped.On("Scope").Return("", false) + + assert.False(t, filter(scoped)) + assert.True(t, filter(unscoped)) + + scoped.AssertExpectations(t) + unscoped.AssertExpectations(t) +} + func TestFilterByDisabledLabel(t *testing.T) { filter := FilterByDisabledLabel(NoFilter) assert.NotNil(t, filter) @@ -110,12 +178,50 @@ func TestFilterByDisabledLabel(t *testing.T) { container.AssertExpectations(t) } -func TestBuildFilter(t *testing.T) { - var names []string - names = append(names, "test") +func TestFilterByImage(t *testing.T) { + filterEmpty := FilterByImage(nil, NoFilter) + filterSingle := FilterByImage([]string{"registry"}, NoFilter) + filterMultiple := FilterByImage([]string{"registry", "bla"}, NoFilter) + assert.NotNil(t, filterSingle) + assert.NotNil(t, filterMultiple) - filter, desc := BuildFilter(names, false, "") + container := new(mocks.FilterableContainer) + container.On("ImageName").Return("registry:2") + assert.True(t, filterEmpty(container)) + assert.True(t, filterSingle(container)) + assert.True(t, filterMultiple(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("ImageName").Return("registry:latest") + assert.True(t, filterEmpty(container)) + assert.True(t, filterSingle(container)) + assert.True(t, filterMultiple(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("ImageName").Return("abcdef1234") + assert.True(t, filterEmpty(container)) + assert.False(t, filterSingle(container)) + assert.False(t, filterMultiple(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("ImageName").Return("bla:latest") + assert.True(t, filterEmpty(container)) + assert.False(t, filterSingle(container)) + assert.True(t, filterMultiple(container)) + container.AssertExpectations(t) + +} + +func TestBuildFilter(t *testing.T) { + names := []string{"test", "valid"} + + filter, desc := BuildFilter(names, []string{}, false, "") assert.Contains(t, desc, "test") + assert.Contains(t, desc, "or") + assert.Contains(t, desc, "valid") container := new(mocks.FilterableContainer) container.On("Name").Return("Invalid") @@ -151,7 +257,7 @@ func TestBuildFilterEnableLabel(t *testing.T) { var names []string names = append(names, "test") - filter, desc := BuildFilter(names, true, "") + filter, desc := BuildFilter(names, []string{}, true, "") assert.Contains(t, desc, "using enable label") container := new(mocks.FilterableContainer) @@ -176,3 +282,52 @@ func TestBuildFilterEnableLabel(t *testing.T) { assert.False(t, filter(container)) container.AssertExpectations(t) } + +func TestBuildFilterDisableContainer(t *testing.T) { + filter, desc := BuildFilter([]string{}, []string{"excluded", "notfound"}, false, "") + assert.Contains(t, desc, "not named") + assert.Contains(t, desc, "excluded") + assert.Contains(t, desc, "or") + assert.Contains(t, desc, "notfound") + + container := new(mocks.FilterableContainer) + container.On("Name").Return("Another") + container.On("Enabled").Return(false, false) + assert.True(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Name").Return("AnotherOne") + container.On("Enabled").Return(true, true) + assert.True(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Name").Return("test") + container.On("Enabled").Return(false, false) + assert.True(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Name").Return("excluded") + container.On("Enabled").Return(true, true) + assert.False(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Name").Return("excludedAsSubstring") + container.On("Enabled").Return(true, true) + assert.True(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Name").Return("notfound") + container.On("Enabled").Return(true, true) + assert.False(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Enabled").Return(false, true) + assert.False(t, filter(container)) + container.AssertExpectations(t) +} diff --git a/pkg/lifecycle/lifecycle.go b/pkg/lifecycle/lifecycle.go index ed4ac20..c0f962e 100644 --- a/pkg/lifecycle/lifecycle.go +++ b/pkg/lifecycle/lifecycle.go @@ -29,7 +29,7 @@ func ExecutePostChecks(client container.Client, params types.UpdateParams) { } // ExecutePreCheckCommand tries to run the pre-check lifecycle hook for a single container. -func ExecutePreCheckCommand(client container.Client, container container.Container) { +func ExecutePreCheckCommand(client container.Client, container types.Container) { clog := log.WithField("container", container.Name()) command := container.GetLifecyclePreCheckCommand() if len(command) == 0 { @@ -45,7 +45,7 @@ func ExecutePreCheckCommand(client container.Client, container container.Contain } // ExecutePostCheckCommand tries to run the post-check lifecycle hook for a single container. -func ExecutePostCheckCommand(client container.Client, container container.Container) { +func ExecutePostCheckCommand(client container.Client, container types.Container) { clog := log.WithField("container", container.Name()) command := container.GetLifecyclePostCheckCommand() if len(command) == 0 { @@ -61,7 +61,7 @@ func ExecutePostCheckCommand(client container.Client, container container.Contai } // ExecutePreUpdateCommand tries to run the pre-update lifecycle hook for a single container. -func ExecutePreUpdateCommand(client container.Client, container container.Container) (SkipUpdate bool, err error) { +func ExecutePreUpdateCommand(client container.Client, container types.Container) (SkipUpdate bool, err error) { timeout := container.PreUpdateTimeout() command := container.GetLifecyclePreUpdateCommand() clog := log.WithField("container", container.Name()) diff --git a/pkg/notifications/common_templates.go b/pkg/notifications/common_templates.go new file mode 100644 index 0000000..84c0f54 --- /dev/null +++ b/pkg/notifications/common_templates.go @@ -0,0 +1,40 @@ +package notifications + +var commonTemplates = map[string]string{ + `default-legacy`: "{{range .}}{{.Message}}{{println}}{{end}}", + + `default`: ` +{{- if .Report -}} + {{- with .Report -}} + {{- if ( or .Updated .Failed ) -}} +{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed + {{- range .Updated}} +- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}} + {{- end -}} + {{- range .Fresh}} +- {{.Name}} ({{.ImageName}}): {{.State}} + {{- end -}} + {{- range .Skipped}} +- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- range .Failed}} +- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} + {{- end -}} + {{- end -}} + {{- end -}} +{{- else -}} + {{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}} +{{- end -}}`, + + `porcelain.v1.summary-no-log`: ` +{{- if .Report -}} + {{- range .Report.All }} + {{- .Name}} ({{.ImageName}}): {{.State -}} + {{- with .Error}} Error: {{.}}{{end}}{{ println }} + {{- else -}} + no containers matched filter + {{- end -}} +{{- end -}}`, + + `json.v1`: `{{ . | ToJSON }}`, +} diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index e162209..9103d38 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -15,18 +15,16 @@ const ( ) type emailTypeNotifier struct { - url string - From, To string - Server, User, Password, SubjectTag string - Port int - tlsSkipVerify bool - entries []*log.Entry - logLevels []log.Level - delay time.Duration + From, To string + Server, User, Password string + Port int + tlsSkipVerify bool + entries []*log.Entry + delay time.Duration } -func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier { - flags := c.PersistentFlags() +func newEmailNotifier(c *cobra.Command) t.ConvertibleNotifier { + flags := c.Flags() from, _ := flags.GetString("notification-email-from") to, _ := flags.GetString("notification-email-to") @@ -36,7 +34,6 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert port, _ := flags.GetInt("notification-email-server-port") tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify") delay, _ := flags.GetInt("notification-email-delay") - subjecttag, _ := flags.GetString("notification-email-subjecttag") n := &emailTypeNotifier{ entries: []*log.Entry{}, @@ -47,28 +44,26 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert Password: password, Port: port, tlsSkipVerify: tlsSkipVerify, - logLevels: acceptedLogLevels, delay: time.Duration(delay) * time.Second, - SubjectTag: subjecttag, } return n } -func (e *emailTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) { +func (e *emailTypeNotifier) GetURL(c *cobra.Command) (string, error) { conf := &shoutrrrSmtp.Config{ FromAddress: e.From, FromName: "Watchtower", ToAddresses: []string{e.To}, Port: uint16(e.Port), Host: e.Server, - Subject: e.getSubject(c, title), Username: e.User, Password: e.Password, UseStartTLS: !e.tlsSkipVerify, UseHTML: false, Encryption: shoutrrrSmtp.EncMethods.Auto, Auth: shoutrrrSmtp.AuthTypes.None, + ClientHost: "localhost", } if len(e.User) > 0 { @@ -85,11 +80,3 @@ func (e *emailTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro func (e *emailTypeNotifier) GetDelay() time.Duration { return e.delay } - -func (e *emailTypeNotifier) getSubject(_ *cobra.Command, title string) string { - if e.SubjectTag != "" { - return e.SubjectTag + " " + title - } - - return title -} diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go index a8c9ac4..c36eb4b 100644 --- a/pkg/notifications/gotify.go +++ b/pkg/notifications/gotify.go @@ -19,11 +19,10 @@ type gotifyTypeNotifier struct { gotifyURL string gotifyAppToken string gotifyInsecureSkipVerify bool - logLevels []log.Level } -func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifier { - flags := c.PersistentFlags() +func newGotifyNotifier(c *cobra.Command) t.ConvertibleNotifier { + flags := c.Flags() apiURL := getGotifyURL(flags) token := getGotifyToken(flags) @@ -34,7 +33,6 @@ func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifi gotifyURL: apiURL, gotifyAppToken: token, gotifyInsecureSkipVerify: skipVerify, - logLevels: levels, } return n @@ -62,7 +60,7 @@ func getGotifyURL(flags *pflag.FlagSet) string { return gotifyURL } -func (n *gotifyTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) { +func (n *gotifyTypeNotifier) GetURL(c *cobra.Command) (string, error) { apiURL, err := url.Parse(n.gotifyURL) if err != nil { return "", err @@ -72,7 +70,6 @@ func (n *gotifyTypeNotifier) GetURL(c *cobra.Command, title string) (string, err Host: apiURL.Host, Path: apiURL.Path, DisableTLS: apiURL.Scheme == "http", - Title: title, Token: n.gotifyAppToken, } diff --git a/pkg/notifications/json.go b/pkg/notifications/json.go new file mode 100644 index 0000000..20da92b --- /dev/null +++ b/pkg/notifications/json.go @@ -0,0 +1,61 @@ +package notifications + +import ( + "encoding/json" + + t "github.com/containrrr/watchtower/pkg/types" +) + +type jsonMap = map[string]interface{} + +// MarshalJSON implements json.Marshaler +func (d Data) MarshalJSON() ([]byte, error) { + var entries = make([]jsonMap, len(d.Entries)) + for i, entry := range d.Entries { + entries[i] = jsonMap{ + `level`: entry.Level, + `message`: entry.Message, + `data`: entry.Data, + `time`: entry.Time, + } + } + + var report jsonMap + if d.Report != nil { + report = jsonMap{ + `scanned`: marshalReports(d.Report.Scanned()), + `updated`: marshalReports(d.Report.Updated()), + `failed`: marshalReports(d.Report.Failed()), + `skipped`: marshalReports(d.Report.Skipped()), + `stale`: marshalReports(d.Report.Stale()), + `fresh`: marshalReports(d.Report.Fresh()), + } + } + + return json.Marshal(jsonMap{ + `report`: report, + `title`: d.Title, + `host`: d.Host, + `entries`: entries, + }) +} + +func marshalReports(reports []t.ContainerReport) []jsonMap { + jsonReports := make([]jsonMap, len(reports)) + for i, report := range reports { + jsonReports[i] = jsonMap{ + `id`: report.ID().ShortID(), + `name`: report.Name(), + `currentImageId`: report.CurrentImageID().ShortID(), + `latestImageId`: report.LatestImageID().ShortID(), + `imageName`: report.ImageName(), + `state`: report.State(), + } + if errorMessage := report.Error(); errorMessage != "" { + jsonReports[i][`error`] = errorMessage + } + } + return jsonReports +} + +var _ json.Marshaler = &Data{} diff --git a/pkg/notifications/json_test.go b/pkg/notifications/json_test.go new file mode 100644 index 0000000..ef30c59 --- /dev/null +++ b/pkg/notifications/json_test.go @@ -0,0 +1,118 @@ +package notifications + +import ( + s "github.com/containrrr/watchtower/pkg/session" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("JSON template", func() { + When("using report templates", func() { + When("JSON template is used", func() { + It("should format the messages to the expected format", func() { + expected := `{ + "entries": [ + { + "data": null, + "level": "info", + "message": "foo Bar", + "time": "0001-01-01T00:00:00Z" + } + ], + "host": "Mock", + "report": { + "failed": [ + { + "currentImageId": "01d210000000", + "error": "accidentally the whole container", + "id": "c79210000000", + "imageName": "mock/fail1:latest", + "latestImageId": "d0a210000000", + "name": "fail1", + "state": "Failed" + } + ], + "fresh": [ + { + "currentImageId": "01d310000000", + "id": "c79310000000", + "imageName": "mock/frsh1:latest", + "latestImageId": "01d310000000", + "name": "frsh1", + "state": "Fresh" + } + ], + "scanned": [ + { + "currentImageId": "01d110000000", + "id": "c79110000000", + "imageName": "mock/updt1:latest", + "latestImageId": "d0a110000000", + "name": "updt1", + "state": "Updated" + }, + { + "currentImageId": "01d120000000", + "id": "c79120000000", + "imageName": "mock/updt2:latest", + "latestImageId": "d0a120000000", + "name": "updt2", + "state": "Updated" + }, + { + "currentImageId": "01d210000000", + "error": "accidentally the whole container", + "id": "c79210000000", + "imageName": "mock/fail1:latest", + "latestImageId": "d0a210000000", + "name": "fail1", + "state": "Failed" + }, + { + "currentImageId": "01d310000000", + "id": "c79310000000", + "imageName": "mock/frsh1:latest", + "latestImageId": "01d310000000", + "name": "frsh1", + "state": "Fresh" + } + ], + "skipped": [ + { + "currentImageId": "01d410000000", + "error": "unpossible", + "id": "c79410000000", + "imageName": "mock/skip1:latest", + "latestImageId": "01d410000000", + "name": "skip1", + "state": "Skipped" + } + ], + "stale": [], + "updated": [ + { + "currentImageId": "01d110000000", + "id": "c79110000000", + "imageName": "mock/updt1:latest", + "latestImageId": "d0a110000000", + "name": "updt1", + "state": "Updated" + }, + { + "currentImageId": "01d120000000", + "id": "c79120000000", + "imageName": "mock/updt2:latest", + "latestImageId": "d0a120000000", + "name": "updt2", + "state": "Updated" + } + ] + }, + "title": "Watchtower updates on Mock" +}` + data := mockDataFromStates(s.UpdatedState, s.FreshState, s.FailedState, s.SkippedState, s.UpdatedState) + Expect(getTemplatedResult(`json.v1`, false, data)).To(MatchJSON(expected)) + }) + }) + }) +}) diff --git a/pkg/notifications/model.go b/pkg/notifications/model.go new file mode 100644 index 0000000..83c97ba --- /dev/null +++ b/pkg/notifications/model.go @@ -0,0 +1,19 @@ +package notifications + +import ( + t "github.com/containrrr/watchtower/pkg/types" + log "github.com/sirupsen/logrus" +) + +// StaticData is the part of the notification template data model set upon initialization +type StaticData struct { + Title string + Host string +} + +// Data is the notification template data model +type Data struct { + StaticData + Entries []*log.Entry + Report t.Report +} diff --git a/pkg/notifications/msteams.go b/pkg/notifications/msteams.go index be67d3b..cfca30e 100644 --- a/pkg/notifications/msteams.go +++ b/pkg/notifications/msteams.go @@ -15,13 +15,12 @@ const ( type msTeamsTypeNotifier struct { webHookURL string - levels []log.Level data bool } -func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier { +func newMsTeamsNotifier(cmd *cobra.Command) t.ConvertibleNotifier { - flags := cmd.PersistentFlags() + flags := cmd.Flags() webHookURL, _ := flags.GetString("notification-msteams-hook") if len(webHookURL) <= 0 { @@ -30,7 +29,6 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con withData, _ := flags.GetBool("notification-msteams-data") n := &msTeamsTypeNotifier{ - levels: acceptedLogLevels, webHookURL: webHookURL, data: withData, } @@ -38,7 +36,7 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con return n } -func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) { +func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command) (string, error) { webhookURL, err := url.Parse(n.webHookURL) if err != nil { return "", err @@ -50,7 +48,6 @@ func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command, title string) (string, er } config.Color = ColorHex - config.Title = title return config.GetURL().String(), nil } diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index 61861fb..ff7b6b5 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -2,17 +2,17 @@ package notifications import ( "os" + "strings" "time" ty "github.com/containrrr/watchtower/pkg/types" - "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) // NewNotifier creates and returns a new Notifier, using global configuration. func NewNotifier(c *cobra.Command) ty.Notifier { - f := c.PersistentFlags() + f := c.Flags() level, _ := f.GetString("notifications-level") logLevel, err := log.ParseLevel(level) @@ -20,24 +20,19 @@ func NewNotifier(c *cobra.Command) ty.Notifier { log.Fatalf("Notifications invalid log level: %s", err.Error()) } - acceptedLogLevels := slackrus.LevelThreshold(logLevel) - // slackrus does not allow log level TRACE, even though it's an accepted log level for logrus - if len(acceptedLogLevels) == 0 { - log.Fatalf("Unsupported notification log level provided: %s", level) - } - reportTemplate, _ := f.GetBool("notification-report") + stdout, _ := f.GetBool("notification-log-stdout") tplString, _ := f.GetString("notification-template") urls, _ := f.GetStringArray("notification-url") - hostname := GetHostname(c) - urls, delay := AppendLegacyUrls(urls, c, GetTitle(hostname)) + data := GetTemplateData(c) + urls, delay := AppendLegacyUrls(urls, c) - return newShoutrrrNotifier(tplString, acceptedLogLevels, !reportTemplate, hostname, delay, urls...) + return createNotifier(urls, logLevel, tplString, !reportTemplate, data, stdout, delay) } // AppendLegacyUrls creates shoutrrr equivalent URLs from legacy notification flags -func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string, time.Duration) { +func AppendLegacyUrls(urls []string, cmd *cobra.Command) ([]string, time.Duration) { // Parse types and create notifiers. types, err := cmd.Flags().GetStringSlice("notifications") @@ -45,7 +40,7 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string log.WithError(err).Fatal("could not read notifications argument") } - delay := time.Duration(0) + legacyDelay := time.Duration(0) for _, t := range types { @@ -54,13 +49,13 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string switch t { case emailType: - legacyNotifier = newEmailNotifier(cmd, []log.Level{}) + legacyNotifier = newEmailNotifier(cmd) case slackType: - legacyNotifier = newSlackNotifier(cmd, []log.Level{}) + legacyNotifier = newSlackNotifier(cmd) case msTeamsType: - legacyNotifier = newMsTeamsNotifier(cmd, []log.Level{}) + legacyNotifier = newMsTeamsNotifier(cmd) case gotifyType: - legacyNotifier = newGotifyNotifier(cmd, []log.Level{}) + legacyNotifier = newGotifyNotifier(cmd) case shoutrrrType: continue default: @@ -69,43 +64,80 @@ func AppendLegacyUrls(urls []string, cmd *cobra.Command, title string) ([]string continue } - shoutrrrURL, err := legacyNotifier.GetURL(cmd, title) + shoutrrrURL, err := legacyNotifier.GetURL(cmd) if err != nil { log.Fatal("failed to create notification config: ", err) } urls = append(urls, shoutrrrURL) if delayNotifier, ok := legacyNotifier.(ty.DelayNotifier); ok { - delay = delayNotifier.GetDelay() + legacyDelay = delayNotifier.GetDelay() } log.WithField("URL", shoutrrrURL).Trace("created Shoutrrr URL from legacy notifier") } + + delay := GetDelay(cmd, legacyDelay) return urls, delay } -// GetTitle returns a common notification title with hostname appended -func GetTitle(hostname string) string { - title := "Watchtower updates" - if hostname != "" { - title += " on " + hostname +// GetDelay returns the legacy delay if defined, otherwise the delay as set by args is returned +func GetDelay(c *cobra.Command, legacyDelay time.Duration) time.Duration { + if legacyDelay > 0 { + return legacyDelay } - return title + + delay, _ := c.PersistentFlags().GetInt("notifications-delay") + if delay > 0 { + return time.Duration(delay) * time.Second + } + return time.Duration(0) } -// GetHostname returns the hostname as set by args or resolved from OS -func GetHostname(c *cobra.Command) string { +// GetTitle formats the title based on the passed hostname and tag +func GetTitle(hostname string, tag string) string { + tb := strings.Builder{} - f := c.PersistentFlags() - hostname, _ := f.GetString("notifications-hostname") - - if hostname != "" { - return hostname - } else if hostname, err := os.Hostname(); err == nil { - return hostname + if tag != "" { + tb.WriteRune('[') + tb.WriteString(tag) + tb.WriteRune(']') + tb.WriteRune(' ') } - return "" + tb.WriteString("Watchtower updates") + + if hostname != "" { + tb.WriteString(" on ") + tb.WriteString(hostname) + } + + return tb.String() +} + +// GetTemplateData populates the static notification data from flags and environment +func GetTemplateData(c *cobra.Command) StaticData { + f := c.PersistentFlags() + + hostname, _ := f.GetString("notifications-hostname") + if hostname == "" { + hostname, _ = os.Hostname() + } + + title := "" + if skip, _ := f.GetBool("notification-skip-title"); !skip { + tag, _ := f.GetString("notification-title-tag") + if tag == "" { + // For legacy email support + tag, _ = f.GetString("notification-email-subjecttag") + } + title = GetTitle(hostname, tag) + } + + return StaticData{ + Host: hostname, + Title: title, + } } // ColorHex is the default notification color used for services that support it (formatted as a CSS hex string) diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go index 44b4dad..96d513c 100644 --- a/pkg/notifications/notifier_test.go +++ b/pkg/notifications/notifier_test.go @@ -3,7 +3,7 @@ package notifications_test import ( "fmt" "net/url" - "os" + "time" "github.com/containrrr/watchtower/cmd" "github.com/containrrr/watchtower/internal/flags" @@ -38,17 +38,103 @@ var _ = Describe("notifications", func() { "test.host", }) Expect(err).NotTo(HaveOccurred()) - hostname := notifications.GetHostname(command) - title := notifications.GetTitle(hostname) + data := notifications.GetTemplateData(command) + title := data.Title Expect(title).To(Equal("Watchtower updates on test.host")) }) }) When("no hostname can be resolved", func() { It("should use the default simple title", func() { - title := notifications.GetTitle("") + title := notifications.GetTitle("", "") Expect(title).To(Equal("Watchtower updates")) }) }) + When("title tag is set", func() { + It("should use the prefix in the title", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + Expect(command.ParseFlags([]string{ + "--notification-title-tag", + "PREFIX", + })).To(Succeed()) + + data := notifications.GetTemplateData(command) + Expect(data.Title).To(HavePrefix("[PREFIX]")) + }) + }) + When("legacy email tag is set", func() { + It("should use the prefix in the title", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + Expect(command.ParseFlags([]string{ + "--notification-email-subjecttag", + "PREFIX", + })).To(Succeed()) + + data := notifications.GetTemplateData(command) + Expect(data.Title).To(HavePrefix("[PREFIX]")) + }) + }) + When("the skip title flag is set", func() { + It("should return an empty title", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + Expect(command.ParseFlags([]string{ + "--notification-skip-title", + })).To(Succeed()) + + data := notifications.GetTemplateData(command) + Expect(data.Title).To(BeEmpty()) + }) + }) + When("no delay is defined", func() { + It("should use the default delay", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + delay := notifications.GetDelay(command, time.Duration(0)) + Expect(delay).To(Equal(time.Duration(0))) + }) + }) + When("delay is defined", func() { + It("should use the specified delay", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + err := command.ParseFlags([]string{ + "--notifications-delay", + "5", + }) + Expect(err).NotTo(HaveOccurred()) + delay := notifications.GetDelay(command, time.Duration(0)) + Expect(delay).To(Equal(time.Duration(5) * time.Second)) + }) + }) + When("legacy delay is defined", func() { + It("should use the specified legacy delay", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + delay := notifications.GetDelay(command, time.Duration(5)*time.Second) + Expect(delay).To(Equal(time.Duration(5) * time.Second)) + }) + }) + When("legacy delay and delay is defined", func() { + It("should use the specified legacy delay and ignore the specified delay", func() { + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + err := command.ParseFlags([]string{ + "--notifications-delay", + "0", + }) + Expect(err).NotTo(HaveOccurred()) + delay := notifications.GetDelay(command, time.Duration(7)*time.Second) + Expect(delay).To(Equal(time.Duration(7) * time.Second)) + }) + }) }) Describe("the slack notifier", func() { // builderFn := notifications.NewSlackNotifier @@ -60,9 +146,9 @@ var _ = Describe("notifications", func() { channel := "123456789" token := "abvsihdbau" color := notifications.ColorInt - hostname := notifications.GetHostname(command) - title := url.QueryEscape(notifications.GetTitle(hostname)) - expected := fmt.Sprintf("discord://%s@%s?color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&title=%s&username=watchtower", token, channel, color, title) + username := "containrrrbot" + iconURL := "https://containrrr.dev/watchtower-sq180.png" + expected := fmt.Sprintf("discord://%s@%s?color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&username=watchtower", token, channel, color) buildArgs := func(url string) []string { return []string{ "--notifications", @@ -74,11 +160,32 @@ var _ = Describe("notifications", func() { It("should return a discord url when using a hook url with the domain discord.com", func() { hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discord.com", channel, token) - testURL(buildArgs(hookURL), expected) + testURL(buildArgs(hookURL), expected, time.Duration(0)) }) It("should return a discord url when using a hook url with the domain discordapp.com", func() { hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discordapp.com", channel, token) - testURL(buildArgs(hookURL), expected) + testURL(buildArgs(hookURL), expected, time.Duration(0)) + }) + When("icon URL and username are specified", func() { + It("should return the expected URL", func() { + hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discord.com", channel, token) + expectedOutput := fmt.Sprintf("discord://%s@%s?avatar=%s&color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&username=%s", token, channel, url.QueryEscape(iconURL), color, username) + expectedDelay := time.Duration(7) * time.Second + args := []string{ + "--notifications", + "slack", + "--notification-slack-hook-url", + hookURL, + "--notification-slack-identifier", + username, + "--notification-slack-icon-url", + iconURL, + "--notifications-delay", + fmt.Sprint(expectedDelay.Seconds()), + } + + testURL(args, expectedOutput, expectedDelay) + }) }) }) When("converting a slack service config into a shoutrrr url", func() { @@ -89,8 +196,6 @@ var _ = Describe("notifications", func() { tokenB := "BBBBBBBBB" tokenC := "123456789123456789123456" color := url.QueryEscape(notifications.ColorHex) - hostname := notifications.GetHostname(command) - title := url.QueryEscape(notifications.GetTitle(hostname)) iconURL := "https://containrrr.dev/watchtower-sq180.png" iconEmoji := "whale" @@ -98,7 +203,8 @@ var _ = Describe("notifications", func() { It("should return the expected URL", func() { hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC) - expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s&title=%s", tokenA, tokenB, tokenC, username, color, url.QueryEscape(iconURL), title) + expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s", tokenA, tokenB, tokenC, username, color, url.QueryEscape(iconURL)) + expectedDelay := time.Duration(7) * time.Second args := []string{ "--notifications", @@ -109,16 +215,18 @@ var _ = Describe("notifications", func() { username, "--notification-slack-icon-url", iconURL, + "--notifications-delay", + fmt.Sprint(expectedDelay.Seconds()), } - testURL(args, expectedOutput) + testURL(args, expectedOutput, expectedDelay) }) }) When("icon emoji is specified", func() { It("should return the expected URL", func() { hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC) - expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s&title=%s", tokenA, tokenB, tokenC, username, color, iconEmoji, title) + expectedOutput := fmt.Sprintf("slack://hook:%s-%s-%s@webhook?botname=%s&color=%s&icon=%s", tokenA, tokenB, tokenC, username, color, iconEmoji) args := []string{ "--notifications", @@ -131,7 +239,7 @@ var _ = Describe("notifications", func() { iconEmoji, } - testURL(args, expectedOutput) + testURL(args, expectedOutput, time.Duration(0)) }) }) }) @@ -145,10 +253,8 @@ var _ = Describe("notifications", func() { token := "aaa" host := "shoutrrr.local" - hostname := notifications.GetHostname(command) - title := url.QueryEscape(notifications.GetTitle(hostname)) - expectedOutput := fmt.Sprintf("gotify://%s/%s?title=%s", host, token, title) + expectedOutput := fmt.Sprintf("gotify://%s/%s?title=", host, token) args := []string{ "--notifications", @@ -159,7 +265,7 @@ var _ = Describe("notifications", func() { token, } - testURL(args, expectedOutput) + testURL(args, expectedOutput, time.Duration(0)) }) }) }) @@ -174,11 +280,9 @@ var _ = Describe("notifications", func() { tokenB := "33333333012222222222333333333344" tokenC := "44444444-4444-4444-8444-cccccccccccc" color := url.QueryEscape(notifications.ColorHex) - hostname := notifications.GetHostname(command) - title := url.QueryEscape(notifications.GetTitle(hostname)) hookURL := fmt.Sprintf("https://outlook.office.com/webhook/%s/IncomingWebhook/%s/%s", tokenA, tokenB, tokenC) - expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s&title=%s", tokenA, tokenB, tokenC, color, title) + expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s", tokenA, tokenB, tokenC, color) args := []string{ "--notifications", @@ -187,7 +291,7 @@ var _ = Describe("notifications", func() { hookURL, } - testURL(args, expectedOutput) + testURL(args, expectedOutput, time.Duration(0)) }) }) }) @@ -197,6 +301,8 @@ var _ = Describe("notifications", func() { It("should set the from address in the URL", func() { fromAddress := "lala@example.com" expectedOutput := buildExpectedURL("containrrrbot", "secret-password", "mail.containrrr.dev", 25, fromAddress, "mail@example.com", "Plain") + expectedDelay := time.Duration(7) * time.Second + args := []string{ "--notifications", "email", @@ -210,8 +316,10 @@ var _ = Describe("notifications", func() { "secret-password", "--notification-email-server", "mail.containrrr.dev", + "--notifications-delay", + fmt.Sprint(expectedDelay.Seconds()), } - testURL(args, expectedOutput) + testURL(args, expectedOutput, expectedDelay) }) It("should return the expected URL", func() { @@ -219,6 +327,7 @@ var _ = Describe("notifications", func() { fromAddress := "sender@example.com" toAddress := "receiver@example.com" expectedOutput := buildExpectedURL("containrrrbot", "secret-password", "mail.containrrr.dev", 25, fromAddress, toAddress, "Plain") + expectedDelay := time.Duration(7) * time.Second args := []string{ "--notifications", @@ -233,44 +342,36 @@ var _ = Describe("notifications", func() { "secret-password", "--notification-email-server", "mail.containrrr.dev", + "--notification-email-delay", + fmt.Sprint(expectedDelay.Seconds()), } - testURL(args, expectedOutput) + testURL(args, expectedOutput, expectedDelay) }) }) }) }) func buildExpectedURL(username string, password string, host string, port int, from string, to string, auth string) string { - hostname, err := os.Hostname() - Expect(err).NotTo(HaveOccurred()) - - subject := fmt.Sprintf("Watchtower updates on %s", hostname) - - var template = "smtp://%s:%s@%s:%d/?auth=%s&fromaddress=%s&fromname=Watchtower&subject=%s&toaddresses=%s" + var template = "smtp://%s:%s@%s:%d/?auth=%s&fromaddress=%s&fromname=Watchtower&subject=&toaddresses=%s" return fmt.Sprintf(template, url.QueryEscape(username), url.QueryEscape(password), host, port, auth, url.QueryEscape(from), - url.QueryEscape(subject), url.QueryEscape(to)) } -func testURL(args []string, expectedURL string) { +func testURL(args []string, expectedURL string, expectedDelay time.Duration) { defer GinkgoRecover() command := cmd.NewRootCommand() flags.RegisterNotificationFlags(command) - err := command.ParseFlags(args) - Expect(err).NotTo(HaveOccurred()) + Expect(command.ParseFlags(args)).To(Succeed()) - hostname := notifications.GetHostname(command) - title := notifications.GetTitle(hostname) - urls, _ := notifications.AppendLegacyUrls([]string{}, command, title) - - Expect(err).NotTo(HaveOccurred()) + urls, delay := notifications.AppendLegacyUrls([]string{}, command) Expect(urls).To(ContainElement(expectedURL)) + Expect(delay).To(Equal(expectedDelay)) } diff --git a/pkg/notifications/preview/data/data.go b/pkg/notifications/preview/data/data.go new file mode 100644 index 0000000..4a002ed --- /dev/null +++ b/pkg/notifications/preview/data/data.go @@ -0,0 +1,143 @@ +package data + +import ( + "encoding/hex" + "errors" + "math/rand" + "strconv" + "time" + + "github.com/containrrr/watchtower/pkg/types" +) + +type previewData struct { + rand *rand.Rand + lastTime time.Time + report *report + containerCount int + Entries []*logEntry + StaticData staticData +} + +type staticData struct { + Title string + Host string +} + +// New initializes a new preview data struct +func New() *previewData { + return &previewData{ + rand: rand.New(rand.NewSource(1)), + lastTime: time.Now().Add(-30 * time.Minute), + report: nil, + containerCount: 0, + Entries: []*logEntry{}, + StaticData: staticData{ + Title: "Title", + Host: "Host", + }, + } +} + +// AddFromState adds a container status entry to the report with the given state +func (pb *previewData) AddFromState(state State) { + cid := types.ContainerID(pb.generateID()) + old := types.ImageID(pb.generateID()) + new := types.ImageID(pb.generateID()) + name := pb.generateName() + image := pb.generateImageName(name) + var err error + if state == FailedState { + err = errors.New(pb.randomEntry(errorMessages)) + } else if state == SkippedState { + err = errors.New(pb.randomEntry(skippedMessages)) + } + pb.addContainer(containerStatus{ + containerID: cid, + oldImage: old, + newImage: new, + containerName: name, + imageName: image, + error: err, + state: state, + }) +} + +func (pb *previewData) addContainer(c containerStatus) { + if pb.report == nil { + pb.report = &report{} + } + switch c.state { + case ScannedState: + pb.report.scanned = append(pb.report.scanned, &c) + case UpdatedState: + pb.report.updated = append(pb.report.updated, &c) + case FailedState: + pb.report.failed = append(pb.report.failed, &c) + case SkippedState: + pb.report.skipped = append(pb.report.skipped, &c) + case StaleState: + pb.report.stale = append(pb.report.stale, &c) + case FreshState: + pb.report.fresh = append(pb.report.fresh, &c) + default: + return + } + pb.containerCount += 1 +} + +// AddLogEntry adds a preview log entry of the given level +func (pd *previewData) AddLogEntry(level LogLevel) { + var msg string + switch level { + case FatalLevel: + fallthrough + case ErrorLevel: + fallthrough + case WarnLevel: + msg = pd.randomEntry(logErrors) + default: + msg = pd.randomEntry(logMessages) + } + pd.Entries = append(pd.Entries, &logEntry{ + Message: msg, + Data: map[string]any{}, + Time: pd.generateTime(), + Level: level, + }) +} + +// Report returns a preview report +func (pb *previewData) Report() types.Report { + return pb.report +} + +func (pb *previewData) generateID() string { + buf := make([]byte, 32) + _, _ = pb.rand.Read(buf) + return hex.EncodeToString(buf) +} + +func (pb *previewData) generateTime() time.Time { + pb.lastTime = pb.lastTime.Add(time.Duration(pb.rand.Intn(30)) * time.Second) + return pb.lastTime +} + +func (pb *previewData) randomEntry(arr []string) string { + return arr[pb.rand.Intn(len(arr))] +} + +func (pb *previewData) generateName() string { + index := pb.containerCount + if index <= len(containerNames) { + return "/" + containerNames[index] + } + suffix := index / len(containerNames) + index %= len(containerNames) + return "/" + containerNames[index] + strconv.FormatInt(int64(suffix), 10) +} + +func (pb *previewData) generateImageName(name string) string { + index := pb.containerCount % len(organizationNames) + return organizationNames[index] + name + ":latest" +} diff --git a/pkg/notifications/preview/data/logs.go b/pkg/notifications/preview/data/logs.go new file mode 100644 index 0000000..3ca7710 --- /dev/null +++ b/pkg/notifications/preview/data/logs.go @@ -0,0 +1,56 @@ +package data + +import ( + "time" +) + +type logEntry struct { + Message string + Data map[string]any + Time time.Time + Level LogLevel +} + +// LogLevel is the analog of logrus.Level +type LogLevel string + +const ( + TraceLevel LogLevel = "trace" + DebugLevel LogLevel = "debug" + InfoLevel LogLevel = "info" + WarnLevel LogLevel = "warning" + ErrorLevel LogLevel = "error" + FatalLevel LogLevel = "fatal" + PanicLevel LogLevel = "panic" +) + +// LevelsFromString parses a string of level characters and returns a slice of the corresponding log levels +func LevelsFromString(str string) []LogLevel { + levels := make([]LogLevel, 0, len(str)) + for _, c := range str { + switch c { + case 'p': + levels = append(levels, PanicLevel) + case 'f': + levels = append(levels, FatalLevel) + case 'e': + levels = append(levels, ErrorLevel) + case 'w': + levels = append(levels, WarnLevel) + case 'i': + levels = append(levels, InfoLevel) + case 'd': + levels = append(levels, DebugLevel) + case 't': + levels = append(levels, TraceLevel) + default: + continue + } + } + return levels +} + +// String returns the log level as a string +func (level LogLevel) String() string { + return string(level) +} diff --git a/pkg/notifications/preview/data/preview_strings.go b/pkg/notifications/preview/data/preview_strings.go new file mode 100644 index 0000000..9212a71 --- /dev/null +++ b/pkg/notifications/preview/data/preview_strings.go @@ -0,0 +1,178 @@ +package data + +var containerNames = []string{ + "cyberscribe", + "datamatrix", + "nexasync", + "quantumquill", + "aerosphere", + "virtuos", + "fusionflow", + "neuralink", + "pixelpulse", + "synthwave", + "codecraft", + "zapzone", + "robologic", + "dreamstream", + "infinisync", + "megamesh", + "novalink", + "xenogenius", + "ecosim", + "innovault", + "techtracer", + "fusionforge", + "quantumquest", + "neuronest", + "codefusion", + "datadyno", + "pixelpioneer", + "vortexvision", + "cybercraft", + "synthsphere", + "infinitescript", + "roborhythm", + "dreamengine", + "aquasync", + "geniusgrid", + "megamind", + "novasync-pro", + "xenonwave", + "ecologic", + "innoscan", +} + +var organizationNames = []string{ + "techwave", + "codecrafters", + "innotechlabs", + "fusionsoft", + "cyberpulse", + "quantumscribe", + "datadynamo", + "neuralink", + "pixelpro", + "synthwizards", + "virtucorplabs", + "robologic", + "dreamstream", + "novanest", + "megamind", + "xenonwave", + "ecologic", + "innosync", + "techgenius", + "nexasoft", + "codewave", + "zapzone", + "techsphere", + "aquatech", + "quantumcraft", + "neuronest", + "datafusion", + "pixelpioneer", + "synthsphere", + "infinitescribe", + "roborhythm", + "dreamengine", + "vortexvision", + "geniusgrid", + "megamesh", + "novasync", + "xenogeniuslabs", + "ecosim", + "innovault", +} + +var errorMessages = []string{ + "Error 404: Resource not found", + "Critical Error: System meltdown imminent", + "Error 500: Internal server error", + "Invalid input: Please check your data", + "Access denied: Unauthorized access detected", + "Network connection lost: Please check your connection", + "Error 403: Forbidden access", + "Fatal error: System crash imminent", + "File not found: Check the file path", + "Invalid credentials: Authentication failed", + "Error 502: Bad Gateway", + "Database connection failed: Please try again later", + "Security breach detected: Take immediate action", + "Error 400: Bad request", + "Out of memory: Close unnecessary applications", + "Invalid configuration: Check your settings", + "Error 503: Service unavailable", + "File is read-only: Cannot modify", + "Data corruption detected: Backup your data", + "Error 401: Unauthorized", + "Disk space full: Free up disk space", + "Connection timeout: Retry your request", + "Error 504: Gateway timeout", + "File access denied: Permission denied", + "Unexpected error: Please contact support", + "Error 429: Too many requests", + "Invalid URL: Check the URL format", + "Database query failed: Try again later", + "Error 408: Request timeout", + "File is in use: Close the file and try again", + "Invalid parameter: Check your input", + "Error 502: Proxy error", + "Database connection lost: Reconnect and try again", + "File size exceeds limit: Reduce the file size", + "Error 503: Overloaded server", + "Operation aborted: Try again", + "Invalid API key: Check your API key", + "Error 507: Insufficient storage", + "Database deadlock: Retry your transaction", + "Error 405: Method not allowed", + "File format not supported: Choose a different format", + "Unknown error: Contact system administrator", +} + +var skippedMessages = []string{ + "Fear of introducing new bugs", + "Don't have time for the update process", + "Current version works fine for my needs", + "Concerns about compatibility with other software", + "Limited bandwidth for downloading updates", + "Worries about losing custom settings or configurations", + "Lack of trust in the software developer's updates", + "Dislike changes to the user interface", + "Avoiding potential subscription fees", + "Suspicion of hidden data collection in updates", + "Apprehension about changes in privacy policies", + "Prefer the older version's features or design", + "Worry about software becoming more resource-intensive", + "Avoiding potential changes in licensing terms", + "Waiting for initial bugs to be resolved in the update", + "Concerns about update breaking third-party plugins or extensions", + "Belief that the software is already secure enough", + "Don't want to relearn how to use the software", + "Fear of losing access to older file formats", + "Avoiding the hassle of having to update multiple devices", +} + +var logMessages = []string{ + "Checking for available updates...", + "Downloading update package...", + "Verifying update integrity...", + "Preparing to install update...", + "Backing up existing configuration...", + "Installing update...", + "Update installation complete.", + "Applying configuration settings...", + "Cleaning up temporary files...", + "Update successful! Software is now up-to-date.", + "Restarting the application...", + "Restart complete. Enjoy the latest features!", + "Update rollback complete. Your software remains at the previous version.", +} + +var logErrors = []string{ + "Unable to check for updates. Please check your internet connection.", + "Update package download failed. Try again later.", + "Update verification failed. Please contact support.", + "Update installation failed. Rolling back to the previous version...", + "Your configuration settings may have been reset to defaults.", +} diff --git a/pkg/notifications/preview/data/report.go b/pkg/notifications/preview/data/report.go new file mode 100644 index 0000000..2c8627f --- /dev/null +++ b/pkg/notifications/preview/data/report.go @@ -0,0 +1,110 @@ +package data + +import ( + "sort" + + "github.com/containrrr/watchtower/pkg/types" +) + +// State is the outcome of a container in a session report +type State string + +const ( + ScannedState State = "scanned" + UpdatedState State = "updated" + FailedState State = "failed" + SkippedState State = "skipped" + StaleState State = "stale" + FreshState State = "fresh" +) + +// StatesFromString parses a string of state characters and returns a slice of the corresponding report states +func StatesFromString(str string) []State { + states := make([]State, 0, len(str)) + for _, c := range str { + switch c { + case 'c': + states = append(states, ScannedState) + case 'u': + states = append(states, UpdatedState) + case 'e': + states = append(states, FailedState) + case 'k': + states = append(states, SkippedState) + case 't': + states = append(states, StaleState) + case 'f': + states = append(states, FreshState) + default: + continue + } + } + return states +} + +type report struct { + scanned []types.ContainerReport + updated []types.ContainerReport + failed []types.ContainerReport + skipped []types.ContainerReport + stale []types.ContainerReport + fresh []types.ContainerReport +} + +func (r *report) Scanned() []types.ContainerReport { + return r.scanned +} +func (r *report) Updated() []types.ContainerReport { + return r.updated +} +func (r *report) Failed() []types.ContainerReport { + return r.failed +} +func (r *report) Skipped() []types.ContainerReport { + return r.skipped +} +func (r *report) Stale() []types.ContainerReport { + return r.stale +} +func (r *report) Fresh() []types.ContainerReport { + return r.fresh +} + +func (r *report) All() []types.ContainerReport { + allLen := len(r.scanned) + len(r.updated) + len(r.failed) + len(r.skipped) + len(r.stale) + len(r.fresh) + all := make([]types.ContainerReport, 0, allLen) + + presentIds := map[types.ContainerID][]string{} + + appendUnique := func(reports []types.ContainerReport) { + for _, cr := range reports { + if _, found := presentIds[cr.ID()]; found { + continue + } + all = append(all, cr) + presentIds[cr.ID()] = nil + } + } + + appendUnique(r.updated) + appendUnique(r.failed) + appendUnique(r.skipped) + appendUnique(r.stale) + appendUnique(r.fresh) + appendUnique(r.scanned) + + sort.Sort(sortableContainers(all)) + + return all +} + +type sortableContainers []types.ContainerReport + +// Len implements sort.Interface.Len +func (s sortableContainers) Len() int { return len(s) } + +// Less implements sort.Interface.Less +func (s sortableContainers) Less(i, j int) bool { return s[i].ID() < s[j].ID() } + +// Swap implements sort.Interface.Swap +func (s sortableContainers) Swap(i, j int) { s[i], s[j] = s[j], s[i] } diff --git a/pkg/notifications/preview/data/status.go b/pkg/notifications/preview/data/status.go new file mode 100644 index 0000000..33f9bec --- /dev/null +++ b/pkg/notifications/preview/data/status.go @@ -0,0 +1,44 @@ +package data + +import wt "github.com/containrrr/watchtower/pkg/types" + +type containerStatus struct { + containerID wt.ContainerID + oldImage wt.ImageID + newImage wt.ImageID + containerName string + imageName string + error + state State +} + +func (u *containerStatus) ID() wt.ContainerID { + return u.containerID +} + +func (u *containerStatus) Name() string { + return u.containerName +} + +func (u *containerStatus) CurrentImageID() wt.ImageID { + return u.oldImage +} + +func (u *containerStatus) LatestImageID() wt.ImageID { + return u.newImage +} + +func (u *containerStatus) ImageName() string { + return u.imageName +} + +func (u *containerStatus) Error() string { + if u.error == nil { + return "" + } + return u.error.Error() +} + +func (u *containerStatus) State() string { + return string(u.state) +} diff --git a/pkg/notifications/preview/tplprev.go b/pkg/notifications/preview/tplprev.go new file mode 100644 index 0000000..8855416 --- /dev/null +++ b/pkg/notifications/preview/tplprev.go @@ -0,0 +1,36 @@ +package preview + +import ( + "fmt" + "strings" + "text/template" + + "github.com/containrrr/watchtower/pkg/notifications/preview/data" + "github.com/containrrr/watchtower/pkg/notifications/templates" +) + +func Render(input string, states []data.State, loglevels []data.LogLevel) (string, error) { + + data := data.New() + + tpl, err := template.New("").Funcs(templates.Funcs).Parse(input) + if err != nil { + return "", fmt.Errorf("failed to parse %v", err) + } + + for _, state := range states { + data.AddFromState(state) + } + + for _, level := range loglevels { + data.AddLogEntry(level) + } + + var buf strings.Builder + err = tpl.Execute(&buf, data) + if err != nil { + return "", fmt.Errorf("failed to execute template: %v", err) + } + + return buf.String(), nil +} diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index bc9499e..cc3a931 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -3,12 +3,14 @@ package notifications import ( "bytes" stdlog "log" + "os" "strings" "text/template" "time" "github.com/containrrr/shoutrrr" "github.com/containrrr/shoutrrr/pkg/types" + "github.com/containrrr/watchtower/pkg/notifications/templates" t "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" ) @@ -17,29 +19,6 @@ import ( var LocalLog = log.WithField("notify", "no") const ( - shoutrrrDefaultLegacyTemplate = "{{range .}}{{.Message}}{{println}}{{end}}" - shoutrrrDefaultTemplate = ` -{{- if .Report -}} - {{- with .Report -}} - {{- if ( or .Updated .Failed ) -}} -{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed - {{- range .Updated}} -- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}} - {{- end -}} - {{- range .Fresh}} -- {{.Name}} ({{.ImageName}}): {{.State}} - {{- end -}} - {{- range .Skipped}} -- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} - {{- end -}} - {{- range .Failed}} -- {{.Name}} ({{.ImageName}}): {{.State}}: {{.Error}} - {{- end -}} - {{- end -}} - {{- end -}} -{{- else -}} - {{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}} -{{- end -}}` shoutrrrType = "shoutrrr" ) @@ -52,13 +31,15 @@ type shoutrrrTypeNotifier struct { Urls []string Router router entries []*log.Entry - logLevels []log.Level + logLevel log.Level template *template.Template messages chan string done chan bool legacyTemplate bool params *types.Params - hostname string + data StaticData + receiving bool + delay time.Duration } // GetScheme returns the scheme part of a Shoutrrr URL @@ -79,45 +60,62 @@ func (n *shoutrrrTypeNotifier) GetNames() []string { return names } -func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, hostname string, delay time.Duration, urls ...string) t.Notifier { - - notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy) - notifier.hostname = hostname - notifier.params = &types.Params{"title": GetTitle(hostname)} - log.AddHook(notifier) - - // Do the sending in a separate goroutine so we don't block the main process. - go sendNotifications(notifier, delay) - - return notifier +// GetURLs returns a list of URLs for notification services that has been added +func (n *shoutrrrTypeNotifier) GetURLs() []string { + return n.Urls } -func createNotifier(urls []string, levels []log.Level, tplString string, legacy bool) *shoutrrrTypeNotifier { +// AddLogHook adds the notifier as a receiver of log messages and starts a go func for processing them +func (n *shoutrrrTypeNotifier) AddLogHook() { + if n.receiving { + return + } + n.receiving = true + log.AddHook(n) + + // Do the sending in a separate goroutine, so we don't block the main process. + go sendNotifications(n) +} + +func createNotifier(urls []string, level log.Level, tplString string, legacy bool, data StaticData, stdout bool, delay time.Duration) *shoutrrrTypeNotifier { tpl, err := getShoutrrrTemplate(tplString, legacy) if err != nil { log.Errorf("Could not use configured notification template: %s. Using default template", err) } - traceWriter := log.StandardLogger().WriterLevel(log.TraceLevel) - r, err := shoutrrr.NewSender(stdlog.New(traceWriter, "Shoutrrr: ", 0), urls...) + var logger types.StdLogger + if stdout { + logger = stdlog.New(os.Stdout, ``, 0) + } else { + logger = stdlog.New(log.StandardLogger().WriterLevel(log.TraceLevel), "Shoutrrr: ", 0) + } + r, err := shoutrrr.NewSender(logger, urls...) if err != nil { log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error()) } + params := &types.Params{} + if data.Title != "" { + params.SetTitle(data.Title) + } + return &shoutrrrTypeNotifier{ Urls: urls, Router: r, messages: make(chan string, 1), done: make(chan bool), - logLevels: levels, + logLevel: level, template: tpl, legacyTemplate: legacy, + data: data, + params: params, + delay: delay, } } -func sendNotifications(n *shoutrrrTypeNotifier, delay time.Duration) { +func sendNotifications(n *shoutrrrTypeNotifier) { for msg := range n.messages { - time.Sleep(delay) + time.Sleep(n.delay) errs := n.Router.Send(msg, n.params) for i, err := range errs { @@ -149,9 +147,7 @@ func (n *shoutrrrTypeNotifier) buildMessage(data Data) (string, error) { } func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report t.Report) { - title, _ := n.params.Title() - host := n.hostname - msg, err := n.buildMessage(Data{entries, report, title, host}) + msg, err := n.buildMessage(Data{n.data, entries, report}) if msg == "" { // Log in go func in case we entered from Fire to avoid stalling @@ -187,12 +183,12 @@ func (n *shoutrrrTypeNotifier) Close() { // Use fmt so it doesn't trigger another notification. LocalLog.Info("Waiting for the notification goroutine to finish") - _ = <-n.done + <-n.done } // Levels return what log levels trigger notifications func (n *shoutrrrTypeNotifier) Levels() []log.Level { - return n.logLevels + return log.AllLevels[:n.logLevel+1] } // Fire is the hook that logrus calls on a new log message @@ -211,12 +207,13 @@ func (n *shoutrrrTypeNotifier) Fire(entry *log.Entry) error { } func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, err error) { - funcs := template.FuncMap{ - "ToUpper": strings.ToUpper, - "ToLower": strings.ToLower, - "Title": strings.Title, + + tplBase := template.New("").Funcs(templates.Funcs) + + if builtin, found := commonTemplates[tplString]; found { + log.WithField(`template`, tplString).Debug(`Using common template`) + tplString = builtin } - tplBase := template.New("").Funcs(funcs) // If we succeed in getting a non-empty template configuration // try to parse the template string. @@ -225,25 +222,17 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, } // If we had an error (either from parsing the template string - // or from getting the template configuration) or we a + // or from getting the template configuration) or a // template wasn't configured (the empty template string) // fallback to using the default template. if err != nil || tplString == "" { - defaultTemplate := shoutrrrDefaultTemplate + defaultKey := `default` if legacy { - defaultTemplate = shoutrrrDefaultLegacyTemplate + defaultKey = `default-legacy` } - tpl = template.Must(tplBase.Parse(defaultTemplate)) + tpl = template.Must(tplBase.Parse(commonTemplates[defaultKey])) } return } - -// Data is the notification template data model -type Data struct { - Entries []*log.Entry - Report t.Report - Title string - Host string -} diff --git a/pkg/notifications/shoutrrr_test.go b/pkg/notifications/shoutrrr_test.go index dbdd9eb..703958b 100644 --- a/pkg/notifications/shoutrrr_test.go +++ b/pkg/notifications/shoutrrr_test.go @@ -14,7 +14,7 @@ import ( "github.com/spf13/cobra" ) -var allButTrace = logrus.AllLevels[0:logrus.TraceLevel] +var allButTrace = logrus.DebugLevel var legacyMockData = Data{ Entries: []*logrus.Entry{ @@ -49,11 +49,14 @@ var mockDataAllFresh = Data{ func mockDataFromStates(states ...s.State) Data { hostname := "Mock" + prefix := "" return Data{ Entries: legacyMockData.Entries, Report: mocks.CreateMockProgressReport(states...), - Title: GetTitle(hostname), - Host: hostname, + StaticData: StaticData{ + Title: GetTitle(hostname, prefix), + Host: hostname, + }, } } @@ -70,6 +73,40 @@ var _ = Describe("Shoutrrr", func() { }) }) + When("passing a common template name", func() { + It("should format using that template", func() { + expected := ` +updt1 (mock/updt1:latest): Updated +`[1:] + data := mockDataFromStates(s.UpdatedState) + Expect(getTemplatedResult(`porcelain.v1.summary-no-log`, false, data)).To(Equal(expected)) + }) + }) + + When("adding a log hook", func() { + When("it has not been added before", func() { + It("should be added to the logrus hooks", func() { + level := logrus.TraceLevel + hooksBefore := len(logrus.StandardLogger().Hooks[level]) + shoutrrr := createNotifier([]string{}, level, "", true, StaticData{}, false, time.Second) + shoutrrr.AddLogHook() + hooksAfter := len(logrus.StandardLogger().Hooks[level]) + Expect(hooksAfter).To(BeNumerically(">", hooksBefore)) + }) + }) + When("it is being added a second time", func() { + It("should not be added to the logrus hooks", func() { + level := logrus.TraceLevel + shoutrrr := createNotifier([]string{}, level, "", true, StaticData{}, false, time.Second) + shoutrrr.AddLogHook() + hooksBefore := len(logrus.StandardLogger().Hooks[level]) + shoutrrr.AddLogHook() + hooksAfter := len(logrus.StandardLogger().Hooks[level]) + Expect(hooksAfter).To(Equal(hooksBefore)) + }) + }) + }) + When("using legacy templates", func() { When("no custom template is provided", func() { @@ -77,7 +114,7 @@ var _ = Describe("Shoutrrr", func() { cmd := new(cobra.Command) flags.RegisterNotificationFlags(cmd) - shoutrrr := createNotifier([]string{}, logrus.AllLevels, "", true) + shoutrrr := createNotifier([]string{}, logrus.TraceLevel, "", true, StaticData{}, false, time.Second) entries := []*logrus.Entry{ { @@ -165,7 +202,6 @@ var _ = Describe("Shoutrrr", func() { }) When("using report templates", func() { - When("no custom template is provided", func() { It("should format the messages using the default template", func() { expected := `4 Scanned, 2 Updated, 1 Failed @@ -233,7 +269,7 @@ Turns out everything is on fire When("batching notifications", func() { When("no messages are queued", func() { It("should not send any notification", func() { - shoutrrr := newShoutrrrNotifier("", allButTrace, true, "", time.Duration(0), "logger://") + shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{}, false, time.Duration(0)) shoutrrr.StartNotification() shoutrrr.SendNotification(nil) Consistently(logBuffer).ShouldNot(gbytes.Say(`Shoutrrr:`)) @@ -241,7 +277,8 @@ Turns out everything is on fire }) When("at least one message is queued", func() { It("should send a notification", func() { - shoutrrr := newShoutrrrNotifier("", allButTrace, true, "", time.Duration(0), "logger://") + shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{}, false, time.Duration(0)) + shoutrrr.AddLogHook() shoutrrr.StartNotification() logrus.Info("This log message is sponsored by ContainrrrVPN") shoutrrr.SendNotification(nil) @@ -250,6 +287,17 @@ Turns out everything is on fire }) }) + When("the title data field is empty", func() { + It("should not have set the title param", func() { + shoutrrr := createNotifier([]string{"logger://"}, allButTrace, "", true, StaticData{ + Host: "test.host", + Title: "", + }, false, time.Second) + _, found := shoutrrr.params.Title() + Expect(found).ToNot(BeTrue()) + }) + }) + When("sending notifications", func() { It("SlowNotificationNotSent", func() { @@ -276,7 +324,7 @@ type blockingRouter struct { } func (b blockingRouter) Send(_ string, _ *types.Params) []error { - _ = <-b.unlock + <-b.unlock b.sent <- true return nil } @@ -298,13 +346,14 @@ func sendNotificationsWithBlockingRouter(legacy bool) (*shoutrrrTypeNotifier, *b Router: router, legacyTemplate: legacy, params: &types.Params{}, + delay: time.Duration(0), } entry := &logrus.Entry{ Message: "foo bar", } - go sendNotifications(shoutrrr, time.Duration(0)) + go sendNotifications(shoutrrr) shoutrrr.StartNotification() _ = shoutrrr.Fire(entry) diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go index faff944..9118527 100644 --- a/pkg/notifications/slack.go +++ b/pkg/notifications/slack.go @@ -6,7 +6,6 @@ import ( shoutrrrDisco "github.com/containrrr/shoutrrr/pkg/services/discord" shoutrrrSlack "github.com/containrrr/shoutrrr/pkg/services/slack" t "github.com/containrrr/watchtower/pkg/types" - "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -16,11 +15,15 @@ const ( ) type slackTypeNotifier struct { - slackrus.SlackrusHook + HookURL string + Username string + Channel string + IconEmoji string + IconURL string } -func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier { - flags := c.PersistentFlags() +func newSlackNotifier(c *cobra.Command) t.ConvertibleNotifier { + flags := c.Flags() hookURL, _ := flags.GetString("notification-slack-hook-url") userName, _ := flags.GetString("notification-slack-identifier") @@ -29,21 +32,18 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert iconURL, _ := flags.GetString("notification-slack-icon-url") n := &slackTypeNotifier{ - SlackrusHook: slackrus.SlackrusHook{ - HookURL: hookURL, - Username: userName, - Channel: channel, - IconEmoji: emoji, - IconURL: iconURL, - AcceptedLevels: acceptedLogLevels, - }, + HookURL: hookURL, + Username: userName, + Channel: channel, + IconEmoji: emoji, + IconURL: iconURL, } return n } -func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, error) { +func (s *slackTypeNotifier) GetURL(c *cobra.Command) (string, error) { trimmedURL := strings.TrimRight(s.HookURL, "/") - trimmedURL = strings.TrimLeft(trimmedURL, "https://") + trimmedURL = strings.TrimPrefix(trimmedURL, "https://") parts := strings.Split(trimmedURL, "/") if parts[0] == "discord.com" || parts[0] == "discordapp.com" { @@ -52,10 +52,14 @@ func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro WebhookID: parts[len(parts)-3], Token: parts[len(parts)-2], Color: ColorInt, - Title: title, SplitLines: true, Username: s.Username, } + + if s.IconURL != "" { + conf.Avatar = s.IconURL + } + return conf.GetURL().String(), nil } @@ -65,7 +69,6 @@ func (s *slackTypeNotifier) GetURL(c *cobra.Command, title string) (string, erro BotName: s.Username, Color: ColorHex, Channel: "webhook", - Title: title, } if s.IconURL != "" { diff --git a/pkg/notifications/templates/funcs.go b/pkg/notifications/templates/funcs.go new file mode 100644 index 0000000..6958c1a --- /dev/null +++ b/pkg/notifications/templates/funcs.go @@ -0,0 +1,27 @@ +package templates + +import ( + "encoding/json" + "fmt" + "strings" + "text/template" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +var Funcs = template.FuncMap{ + "ToUpper": strings.ToUpper, + "ToLower": strings.ToLower, + "ToJSON": toJSON, + "Title": cases.Title(language.AmericanEnglish).String, +} + +func toJSON(v interface{}) string { + var bytes []byte + var err error + if bytes, err = json.MarshalIndent(v, "", " "); err != nil { + return fmt.Sprintf("failed to marshal JSON in notification template: %v", err) + } + return string(bytes) +} diff --git a/pkg/registry/auth/auth.go b/pkg/registry/auth/auth.go index 99e307c..99b05c9 100644 --- a/pkg/registry/auth/auth.go +++ b/pkg/registry/auth/auth.go @@ -4,14 +4,15 @@ import ( "encoding/json" "errors" "fmt" - "github.com/containrrr/watchtower/pkg/registry/helpers" - "github.com/containrrr/watchtower/pkg/types" - "github.com/docker/distribution/reference" - "github.com/sirupsen/logrus" - "io/ioutil" + "io" "net/http" "net/url" "strings" + + "github.com/containrrr/watchtower/pkg/registry/helpers" + "github.com/containrrr/watchtower/pkg/types" + ref "github.com/distribution/reference" + "github.com/sirupsen/logrus" ) // ChallengeHeader is the HTTP Header containing challenge instructions @@ -19,13 +20,13 @@ const ChallengeHeader = "WWW-Authenticate" // GetToken fetches a token for the registry hosting the provided image func GetToken(container types.Container, registryAuth string) (string, error) { - var err error - var URL url.URL - - if URL, err = GetChallengeURL(container.ImageName()); err != nil { + normalizedRef, err := ref.ParseNormalizedNamed(container.ImageName()) + if err != nil { return "", err } - logrus.WithField("URL", URL.String()).Debug("Building challenge URL") + + URL := GetChallengeURL(normalizedRef) + logrus.WithField("URL", URL.String()).Debug("Built challenge URL") var req *http.Request if req, err = GetChallengeRequest(URL); err != nil { @@ -54,7 +55,7 @@ func GetToken(container types.Container, registryAuth string) (string, error) { return fmt.Sprintf("Basic %s", registryAuth), nil } if strings.HasPrefix(challenge, "bearer") { - return GetBearerHeader(challenge, container.ImageName(), err, registryAuth) + return GetBearerHeader(challenge, normalizedRef, registryAuth) } return "", errors.New("unsupported challenge type from registry") @@ -72,12 +73,9 @@ func GetChallengeRequest(URL url.URL) (*http.Request, error) { } // GetBearerHeader tries to fetch a bearer token from the registry based on the challenge instructions -func GetBearerHeader(challenge string, img string, err error, registryAuth string) (string, error) { +func GetBearerHeader(challenge string, imageRef ref.Named, registryAuth string) (string, error) { client := http.Client{} - if strings.Contains(img, ":") { - img = strings.Split(img, ":")[0] - } - authURL, err := GetAuthURL(challenge, img) + authURL, err := GetAuthURL(challenge, imageRef) if err != nil { return "", err @@ -90,7 +88,8 @@ func GetBearerHeader(challenge string, img string, err error, registryAuth strin if registryAuth != "" { logrus.Debug("Credentials found.") - logrus.Tracef("Credentials: %v", registryAuth) + // CREDENTIAL: Uncomment to log registry credentials + // logrus.Tracef("Credentials: %v", registryAuth) r.Header.Add("Authorization", fmt.Sprintf("Basic %s", registryAuth)) } else { logrus.Debug("No credentials found.") @@ -101,7 +100,7 @@ func GetBearerHeader(challenge string, img string, err error, registryAuth strin return "", err } - body, _ := ioutil.ReadAll(authResponse.Body) + body, _ := io.ReadAll(authResponse.Body) tokenResponse := &types.TokenResponse{} err = json.Unmarshal(body, tokenResponse) @@ -113,7 +112,7 @@ func GetBearerHeader(challenge string, img string, err error, registryAuth strin } // GetAuthURL from the instructions in the challenge -func GetAuthURL(challenge string, img string) (*url.URL, error) { +func GetAuthURL(challenge string, imageRef ref.Named) (*url.URL, error) { loweredChallenge := strings.ToLower(challenge) raw := strings.TrimPrefix(loweredChallenge, "bearer") @@ -122,10 +121,9 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) { for _, pair := range pairs { trimmed := strings.Trim(pair, " ") - kv := strings.Split(trimmed, "=") - key := kv[0] - val := strings.Trim(kv[1], "\"") - values[key] = val + if key, val, ok := strings.Cut(trimmed, "="); ok { + values[key] = strings.Trim(val, `"`) + } } logrus.WithFields(logrus.Fields{ "realm": values["realm"], @@ -136,57 +134,29 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) { return nil, fmt.Errorf("challenge header did not include all values needed to construct an auth url") } - authURL, _ := url.Parse(fmt.Sprintf("%s", values["realm"])) + authURL, _ := url.Parse(values["realm"]) q := authURL.Query() q.Add("service", values["service"]) - scopeImage := GetScopeFromImageName(img, values["service"]) + scopeImage := ref.Path(imageRef) scope := fmt.Sprintf("repository:%s:pull", scopeImage) - logrus.WithFields(logrus.Fields{"scope": scope, "image": img}).Debug("Setting scope for auth token") + logrus.WithFields(logrus.Fields{"scope": scope, "image": imageRef.Name()}).Debug("Setting scope for auth token") q.Add("scope", scope) authURL.RawQuery = q.Encode() return authURL, nil } -// GetScopeFromImageName normalizes an image name for use as scope during auth and head requests -func GetScopeFromImageName(img, svc string) string { - parts := strings.Split(img, "/") - - if len(parts) > 2 { - if strings.Contains(svc, "docker.io") { - return fmt.Sprintf("%s/%s", parts[1], strings.Join(parts[2:], "/")) - } - return strings.Join(parts, "/") - } - - if len(parts) == 2 { - if strings.Contains(parts[0], "docker.io") { - return fmt.Sprintf("library/%s", parts[1]) - } - return strings.Replace(img, svc+"/", "", 1) - } - - if strings.Contains(svc, "docker.io") { - return fmt.Sprintf("library/%s", parts[0]) - } - return img -} - -// GetChallengeURL creates a URL object based on the image info -func GetChallengeURL(img string) (url.URL, error) { - - normalizedNamed, _ := reference.ParseNormalizedNamed(img) - host, err := helpers.NormalizeRegistry(normalizedNamed.String()) - if err != nil { - return url.URL{}, err - } +// GetChallengeURL returns the URL to check auth requirements +// for access to a given image +func GetChallengeURL(imageRef ref.Named) url.URL { + host, _ := helpers.GetRegistryAddress(imageRef.Name()) URL := url.URL{ Scheme: "https", Host: host, Path: "/v2/", } - return URL, nil + return URL } diff --git a/pkg/registry/auth/auth_test.go b/pkg/registry/auth/auth_test.go index 6ad2307..d295310 100644 --- a/pkg/registry/auth/auth_test.go +++ b/pkg/registry/auth/auth_test.go @@ -2,14 +2,17 @@ package auth_test import ( "fmt" - "github.com/containrrr/watchtower/internal/actions/mocks" - "github.com/containrrr/watchtower/pkg/registry/auth" "net/url" "os" + "strings" "testing" "time" + "github.com/containrrr/watchtower/internal/actions/mocks" + "github.com/containrrr/watchtower/pkg/registry/auth" + wtTypes "github.com/containrrr/watchtower/pkg/types" + ref "github.com/distribution/reference" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) @@ -51,7 +54,7 @@ var _ = Describe("the auth module", func() { mockCreated, mockDigest) - When("getting an auth url", func() { + Describe("GetToken", func() { It("should parse the token from the response", SkipIfCredentialsEmpty(GHCRCredentials, func() { creds := fmt.Sprintf("%s:%s", GHCRCredentials.Username, GHCRCredentials.Password) @@ -60,61 +63,100 @@ var _ = Describe("the auth module", func() { Expect(token).NotTo(Equal("")) }), ) + }) + Describe("GetAuthURL", func() { It("should create a valid auth url object based on the challenge header supplied", func() { - input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull"` + challenge := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull"` + imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower") + Expect(err).NotTo(HaveOccurred()) expected := &url.URL{ Host: "ghcr.io", Scheme: "https", Path: "/token", RawQuery: "scope=repository%3Acontainrrr%2Fwatchtower%3Apull&service=ghcr.io", } - res, err := auth.GetAuthURL(input, "containrrr/watchtower") + + URL, err := auth.GetAuthURL(challenge, imageRef) Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(expected)) + Expect(URL).To(Equal(expected)) }) - It("should create a valid auth url object based on the challenge header supplied", func() { - input := `bearer realm="https://ghcr.io/token"` - res, err := auth.GetAuthURL(input, "containrrr/watchtower") - Expect(err).To(HaveOccurred()) - Expect(res).To(BeNil()) + + When("given an invalid challenge header", func() { + It("should return an error", func() { + challenge := `bearer realm="https://ghcr.io/token"` + imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower") + Expect(err).NotTo(HaveOccurred()) + URL, err := auth.GetAuthURL(challenge, imageRef) + Expect(err).To(HaveOccurred()) + Expect(URL).To(BeNil()) + }) + }) + + When("deriving the auth scope from an image name", func() { + It("should prepend official dockerhub images with \"library/\"", func() { + Expect(getScopeFromImageAuthURL("registry")).To(Equal("library/registry")) + Expect(getScopeFromImageAuthURL("docker.io/registry")).To(Equal("library/registry")) + Expect(getScopeFromImageAuthURL("index.docker.io/registry")).To(Equal("library/registry")) + }) + It("should not include vanity hosts\"", func() { + Expect(getScopeFromImageAuthURL("docker.io/containrrr/watchtower")).To(Equal("containrrr/watchtower")) + Expect(getScopeFromImageAuthURL("index.docker.io/containrrr/watchtower")).To(Equal("containrrr/watchtower")) + }) + It("should not destroy three segment image names\"", func() { + Expect(getScopeFromImageAuthURL("piksel/containrrr/watchtower")).To(Equal("piksel/containrrr/watchtower")) + Expect(getScopeFromImageAuthURL("ghcr.io/piksel/containrrr/watchtower")).To(Equal("piksel/containrrr/watchtower")) + }) + It("should not prepend library/ to image names if they're not on dockerhub", func() { + Expect(getScopeFromImageAuthURL("ghcr.io/watchtower")).To(Equal("watchtower")) + Expect(getScopeFromImageAuthURL("ghcr.io/containrrr/watchtower")).To(Equal("containrrr/watchtower")) + }) + }) + It("should not crash when an empty field is received", func() { + input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull",` + imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower") + Expect(err).NotTo(HaveOccurred()) + res, err := auth.GetAuthURL(input, imageRef) + Expect(err).NotTo(HaveOccurred()) + Expect(res).NotTo(BeNil()) + }) + It("should not crash when a field without a value is received", func() { + input := `bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull",valuelesskey` + imageRef, err := ref.ParseNormalizedNamed("containrrr/watchtower") + Expect(err).NotTo(HaveOccurred()) + res, err := auth.GetAuthURL(input, imageRef) + Expect(err).NotTo(HaveOccurred()) + Expect(res).NotTo(BeNil()) }) }) - When("getting a challenge url", func() { + + Describe("GetChallengeURL", func() { It("should create a valid challenge url object based on the image ref supplied", func() { expected := url.URL{Host: "ghcr.io", Scheme: "https", Path: "/v2/"} - Expect(auth.GetChallengeURL("ghcr.io/containrrr/watchtower:latest")).To(Equal(expected)) + imageRef, _ := ref.ParseNormalizedNamed("ghcr.io/containrrr/watchtower:latest") + Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected)) }) - It("should assume dockerhub if the image ref is not fully qualified", func() { + It("should assume Docker Hub for image refs with no explicit registry", func() { expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"} - Expect(auth.GetChallengeURL("containrrr/watchtower:latest")).To(Equal(expected)) + imageRef, _ := ref.ParseNormalizedNamed("containrrr/watchtower:latest") + Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected)) }) - It("should convert legacy dockerhub hostnames to index.docker.io", func() { + It("should use index.docker.io if the image ref specifies docker.io", func() { expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"} - Expect(auth.GetChallengeURL("docker.io/containrrr/watchtower:latest")).To(Equal(expected)) - Expect(auth.GetChallengeURL("registry-1.docker.io/containrrr/watchtower:latest")).To(Equal(expected)) - }) - }) - When("getting the auth scope from an image name", func() { - It("should prepend official dockerhub images with \"library/\"", func() { - Expect(auth.GetScopeFromImageName("docker.io/registry", "index.docker.io")).To(Equal("library/registry")) - Expect(auth.GetScopeFromImageName("docker.io/registry", "docker.io")).To(Equal("library/registry")) - - Expect(auth.GetScopeFromImageName("registry", "index.docker.io")).To(Equal("library/registry")) - Expect(auth.GetScopeFromImageName("watchtower", "registry-1.docker.io")).To(Equal("library/watchtower")) - - }) - It("should not include vanity hosts\"", func() { - Expect(auth.GetScopeFromImageName("docker.io/containrrr/watchtower", "index.docker.io")).To(Equal("containrrr/watchtower")) - Expect(auth.GetScopeFromImageName("index.docker.io/containrrr/watchtower", "index.docker.io")).To(Equal("containrrr/watchtower")) - }) - It("should not destroy three segment image names\"", func() { - Expect(auth.GetScopeFromImageName("piksel/containrrr/watchtower", "index.docker.io")).To(Equal("containrrr/watchtower")) - Expect(auth.GetScopeFromImageName("piksel/containrrr/watchtower", "ghcr.io")).To(Equal("piksel/containrrr/watchtower")) - }) - It("should not add \"library/\" for one segment image names if they're not on dockerhub", func() { - Expect(auth.GetScopeFromImageName("ghcr.io/watchtower", "ghcr.io")).To(Equal("watchtower")) - Expect(auth.GetScopeFromImageName("watchtower", "ghcr.io")).To(Equal("watchtower")) + imageRef, _ := ref.ParseNormalizedNamed("docker.io/containrrr/watchtower:latest") + Expect(auth.GetChallengeURL(imageRef)).To(Equal(expected)) }) }) }) + +var scopeImageRegexp = MatchRegexp("^repository:[a-z0-9]+(/[a-z0-9]+)*:pull$") + +func getScopeFromImageAuthURL(imageName string) string { + normalizedRef, _ := ref.ParseNormalizedNamed(imageName) + challenge := `bearer realm="https://dummy.host/token",service="dummy.host",scope="repository:user/image:pull"` + URL, _ := auth.GetAuthURL(challenge, normalizedRef) + + scope := URL.Query().Get("scope") + Expect(scopeImageRegexp.Match(scope)).To(BeTrue()) + return strings.Replace(scope[11:], ":pull", "", 1) +} diff --git a/pkg/registry/digest/digest.go b/pkg/registry/digest/digest.go index 26fbd8e..e569599 100644 --- a/pkg/registry/digest/digest.go +++ b/pkg/registry/digest/digest.go @@ -6,15 +6,16 @@ import ( "encoding/json" "errors" "fmt" + "net" + "net/http" + "strings" + "time" + "github.com/containrrr/watchtower/internal/meta" "github.com/containrrr/watchtower/pkg/registry/auth" "github.com/containrrr/watchtower/pkg/registry/manifest" "github.com/containrrr/watchtower/pkg/types" "github.com/sirupsen/logrus" - "net" - "net/http" - "strings" - "time" ) // ContentDigestHeader is the key for the key-value pair containing the digest header @@ -25,7 +26,7 @@ func CompareDigest(container types.Container, registryAuth string) (bool, error) if !container.HasImageInfo() { return false, errors.New("container image info missing") } - + var digest string registryAuth = TransformAuth(registryAuth) @@ -93,16 +94,18 @@ func GetDigest(url string, token string) (string, error) { req, _ := http.NewRequest("HEAD", url, nil) req.Header.Set("User-Agent", meta.UserAgent) - if token != "" { - logrus.WithField("token", token).Trace("Setting request token") - } else { + if token == "" { return "", errors.New("could not fetch token") } + // CREDENTIAL: Uncomment to log the request token + // logrus.WithField("token", token).Trace("Setting request token") + req.Header.Add("Authorization", token) req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json") req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.list.v2+json") req.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v1+json") + req.Header.Add("Accept", "application/vnd.oci.image.index.v1+json") logrus.WithField("url", url).Debug("Doing a HEAD request to fetch a digest") diff --git a/pkg/registry/helpers/helpers.go b/pkg/registry/helpers/helpers.go index 1469331..35d6ca3 100644 --- a/pkg/registry/helpers/helpers.go +++ b/pkg/registry/helpers/helpers.go @@ -1,36 +1,28 @@ package helpers import ( - "fmt" - url2 "net/url" + "github.com/distribution/reference" ) -// ConvertToHostname strips a url from everything but the hostname part -func ConvertToHostname(url string) (string, string, error) { - urlWithSchema := fmt.Sprintf("x://%s", url) - u, err := url2.Parse(urlWithSchema) - if err != nil { - return "", "", err - } - hostName := u.Hostname() - port := u.Port() +// domains for Docker Hub, the default registry +const ( + DefaultRegistryDomain = "docker.io" + DefaultRegistryHost = "index.docker.io" + LegacyDefaultRegistryDomain = "index.docker.io" +) - return hostName, port, err -} - -// NormalizeRegistry makes sure variations of DockerHubs registry -func NormalizeRegistry(registry string) (string, error) { - hostName, port, err := ConvertToHostname(registry) +// GetRegistryAddress parses an image name +// and returns the address of the specified registry +func GetRegistryAddress(imageRef string) (string, error) { + normalizedRef, err := reference.ParseNormalizedNamed(imageRef) if err != nil { return "", err } - if hostName == "registry-1.docker.io" || hostName == "docker.io" { - hostName = "index.docker.io" - } + address := reference.Domain(normalizedRef) - if port != "" { - return fmt.Sprintf("%s:%s", hostName, port), nil + if address == DefaultRegistryDomain { + address = DefaultRegistryHost } - return hostName, nil + return address, nil } diff --git a/pkg/registry/helpers/helpers_test.go b/pkg/registry/helpers/helpers_test.go index 92e9116..a561c2c 100644 --- a/pkg/registry/helpers/helpers_test.go +++ b/pkg/registry/helpers/helpers_test.go @@ -1,9 +1,10 @@ package helpers import ( + "testing" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "testing" ) func TestHelpers(t *testing.T) { @@ -12,20 +13,25 @@ func TestHelpers(t *testing.T) { } var _ = Describe("the helpers", func() { - - When("converting an url to a hostname", func() { - It("should return docker.io given docker.io/containrrr/watchtower:latest", func() { - host, port, err := ConvertToHostname("docker.io/containrrr/watchtower:latest") - Expect(err).NotTo(HaveOccurred()) - Expect(host).To(Equal("docker.io")) - Expect(port).To(BeEmpty()) + Describe("GetRegistryAddress", func() { + It("should return error if passed empty string", func() { + _, err := GetRegistryAddress("") + Expect(err).To(HaveOccurred()) }) - }) - When("normalizing the registry information", func() { - It("should return index.docker.io given docker.io", func() { - out, err := NormalizeRegistry("docker.io/containrrr/watchtower:latest") - Expect(err).NotTo(HaveOccurred()) - Expect(out).To(Equal("index.docker.io")) + It("should return index.docker.io for image refs with no explicit registry", func() { + Expect(GetRegistryAddress("watchtower")).To(Equal("index.docker.io")) + Expect(GetRegistryAddress("containrrr/watchtower")).To(Equal("index.docker.io")) + }) + It("should return index.docker.io for image refs with docker.io domain", func() { + Expect(GetRegistryAddress("docker.io/watchtower")).To(Equal("index.docker.io")) + Expect(GetRegistryAddress("docker.io/containrrr/watchtower")).To(Equal("index.docker.io")) + }) + It("should return the host if passed an image name containing a local host", func() { + Expect(GetRegistryAddress("henk:80/watchtower")).To(Equal("henk:80")) + Expect(GetRegistryAddress("localhost/watchtower")).To(Equal("localhost")) + }) + It("should return the server address if passed a fully qualified image name", func() { + Expect(GetRegistryAddress("github.com/containrrr/config")).To(Equal("github.com")) }) }) }) diff --git a/pkg/registry/manifest/manifest.go b/pkg/registry/manifest/manifest.go index facbb6c..c732bae 100644 --- a/pkg/registry/manifest/manifest.go +++ b/pkg/registry/manifest/manifest.go @@ -1,42 +1,41 @@ package manifest import ( + "errors" "fmt" - "github.com/containrrr/watchtower/pkg/registry/auth" + url2 "net/url" + "github.com/containrrr/watchtower/pkg/registry/helpers" "github.com/containrrr/watchtower/pkg/types" - ref "github.com/docker/distribution/reference" + ref "github.com/distribution/reference" "github.com/sirupsen/logrus" - url2 "net/url" - "strings" ) // BuildManifestURL from raw image data func BuildManifestURL(container types.Container) (string, error) { - - normalizedName, err := ref.ParseNormalizedNamed(container.ImageName()) + normalizedRef, err := ref.ParseDockerRef(container.ImageName()) if err != nil { return "", err } + normalizedTaggedRef, isTagged := normalizedRef.(ref.NamedTagged) + if !isTagged { + return "", errors.New("Parsed container image ref has no tag: " + normalizedRef.String()) + } - host, err := helpers.NormalizeRegistry(normalizedName.String()) - img, tag := ExtractImageAndTag(strings.TrimPrefix(container.ImageName(), host+"/")) + host, _ := helpers.GetRegistryAddress(normalizedTaggedRef.Name()) + img, tag := ref.Path(normalizedTaggedRef), normalizedTaggedRef.Tag() logrus.WithFields(logrus.Fields{ "image": img, "tag": tag, - "normalized": normalizedName, + "normalized": normalizedTaggedRef.Name(), "host": host, }).Debug("Parsing image ref") if err != nil { return "", err } - img = auth.GetScopeFromImageName(img, host) - if !strings.Contains(img, "/") { - img = "library/" + img - } url := url2.URL{ Scheme: "https", Host: host, @@ -44,24 +43,3 @@ func BuildManifestURL(container types.Container) (string, error) { } return url.String(), nil } - -// ExtractImageAndTag from a concatenated string -func ExtractImageAndTag(imageName string) (string, string) { - var img string - var tag string - - if strings.Contains(imageName, ":") { - parts := strings.Split(imageName, ":") - if len(parts) > 2 { - img = parts[0] - tag = strings.Join(parts[1:], ":") - } else { - img = parts[0] - tag = parts[1] - } - } else { - img = imageName - tag = "latest" - } - return img, tag -} diff --git a/pkg/registry/manifest/manifest_test.go b/pkg/registry/manifest/manifest_test.go index 95f196b..b24d9bc 100644 --- a/pkg/registry/manifest/manifest_test.go +++ b/pkg/registry/manifest/manifest_test.go @@ -1,13 +1,14 @@ package manifest_test import ( + "testing" + "time" + "github.com/containrrr/watchtower/internal/actions/mocks" "github.com/containrrr/watchtower/pkg/registry/manifest" apiTypes "github.com/docker/docker/api/types" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "testing" - "time" ) func TestManifest(t *testing.T) { @@ -16,60 +17,58 @@ func TestManifest(t *testing.T) { } var _ = Describe("the manifest module", func() { - mockId := "mock-id" - mockName := "mock-container" - mockCreated := time.Now() - - When("building a manifest url", func() { + Describe("BuildManifestURL", func() { It("should return a valid url given a fully qualified image", func() { - expected := "https://ghcr.io/v2/containrrr/watchtower/manifests/latest" - imageInfo := apiTypes.ImageInspect{ - RepoTags: []string{ - "ghcr.io/k6io/operator:latest", - }, - } - mock := mocks.CreateMockContainerWithImageInfo(mockId, mockName, "ghcr.io/containrrr/watchtower:latest", mockCreated, imageInfo) - res, err := manifest.BuildManifestURL(mock) + imageRef := "ghcr.io/containrrr/watchtower:mytag" + expected := "https://ghcr.io/v2/containrrr/watchtower/manifests/mytag" + + URL, err := buildMockContainerManifestURL(imageRef) Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(expected)) + Expect(URL).To(Equal(expected)) }) - It("should assume dockerhub for non-qualified images", func() { + It("should assume Docker Hub for image refs with no explicit registry", func() { + imageRef := "containrrr/watchtower:latest" expected := "https://index.docker.io/v2/containrrr/watchtower/manifests/latest" - imageInfo := apiTypes.ImageInspect{ - RepoTags: []string{ - "containrrr/watchtower:latest", - }, - } - mock := mocks.CreateMockContainerWithImageInfo(mockId, mockName, "containrrr/watchtower:latest", mockCreated, imageInfo) - res, err := manifest.BuildManifestURL(mock) + URL, err := buildMockContainerManifestURL(imageRef) Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(expected)) + Expect(URL).To(Equal(expected)) }) - It("should assume latest for images that lack an explicit tag", func() { + It("should assume latest for image refs with no explicit tag", func() { + imageRef := "containrrr/watchtower" expected := "https://index.docker.io/v2/containrrr/watchtower/manifests/latest" - imageInfo := apiTypes.ImageInspect{ - RepoTags: []string{ - "containrrr/watchtower", - }, - } - - mock := mocks.CreateMockContainerWithImageInfo(mockId, mockName, "containrrr/watchtower", mockCreated, imageInfo) - - res, err := manifest.BuildManifestURL(mock) + URL, err := buildMockContainerManifestURL(imageRef) Expect(err).NotTo(HaveOccurred()) - Expect(res).To(Equal(expected)) + Expect(URL).To(Equal(expected)) }) - It("should combine the tag name and digest pinning into one digest, given multiple colons", func() { - in := "containrrr/watchtower:latest@sha256:daf7034c5c89775afe3008393ae033529913548243b84926931d7c84398ecda7" - image, tag := "containrrr/watchtower", "latest@sha256:daf7034c5c89775afe3008393ae033529913548243b84926931d7c84398ecda7" + It("should not prepend library/ for single-part container names in registries other than Docker Hub", func() { + imageRef := "docker-registry.domain/imagename:latest" + expected := "https://docker-registry.domain/v2/imagename/manifests/latest" - imageOut, tagOut := manifest.ExtractImageAndTag(in) - - Expect(imageOut).To(Equal(image)) - Expect(tagOut).To(Equal(tag)) + URL, err := buildMockContainerManifestURL(imageRef) + Expect(err).NotTo(HaveOccurred()) + Expect(URL).To(Equal(expected)) + }) + It("should throw an error on pinned images", func() { + imageRef := "docker-registry.domain/imagename@sha256:daf7034c5c89775afe3008393ae033529913548243b84926931d7c84398ecda7" + URL, err := buildMockContainerManifestURL(imageRef) + Expect(err).To(HaveOccurred()) + Expect(URL).To(BeEmpty()) }) }) - }) + +func buildMockContainerManifestURL(imageRef string) (string, error) { + imageInfo := apiTypes.ImageInspect{ + RepoTags: []string{ + imageRef, + }, + } + mockID := "mock-id" + mockName := "mock-container" + mockCreated := time.Now() + mock := mocks.CreateMockContainerWithImageInfo(mockID, mockName, imageRef, mockCreated, imageInfo) + + return manifest.BuildManifestURL(mock) +} diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 9edd66f..430b401 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -3,7 +3,7 @@ package registry import ( "github.com/containrrr/watchtower/pkg/registry/helpers" watchtowerTypes "github.com/containrrr/watchtower/pkg/types" - ref "github.com/docker/distribution/reference" + ref "github.com/distribution/reference" "github.com/docker/docker/api/types" log "github.com/sirupsen/logrus" ) @@ -19,7 +19,9 @@ func GetPullOptions(imageName string) (types.ImagePullOptions, error) { if auth == "" { return types.ImagePullOptions{}, nil } - log.Tracef("Got auth value: %s", auth) + + // CREDENTIAL: Uncomment to log docker config auth + // log.Tracef("Got auth value: %s", auth) return types.ImagePullOptions{ RegistryAuth: auth, @@ -41,17 +43,17 @@ func DefaultAuthHandler() (string, error) { // Will return false if behavior for container is unknown. func WarnOnAPIConsumption(container watchtowerTypes.Container) bool { - normalizedName, err := ref.ParseNormalizedNamed(container.ImageName()) + normalizedRef, err := ref.ParseNormalizedNamed(container.ImageName()) if err != nil { return true } - containerHost, err := helpers.NormalizeRegistry(normalizedName.String()) + containerHost, err := helpers.GetRegistryAddress(normalizedRef.Name()) if err != nil { return true } - if containerHost == "index.docker.io" || containerHost == "ghcr.io" { + if containerHost == helpers.DefaultRegistryHost || containerHost == "ghcr.io" { return true } diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index 5f3f57f..481c91d 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -23,11 +23,9 @@ var _ = Describe("Registry", func() { }) When("Given a container with an image explicitly from dockerhub", func() { It("should want to warn", func() { - Expect(testContainerWithImage("registry-1.docker.io/docker:latest")).To(BeTrue()) Expect(testContainerWithImage("index.docker.io/docker:latest")).To(BeTrue()) Expect(testContainerWithImage("docker.io/docker:latest")).To(BeTrue()) }) - }) When("Given a container with an image from some other registry", func() { It("should not want to warn", func() { diff --git a/pkg/registry/trust.go b/pkg/registry/trust.go index ab9e353..0b20248 100644 --- a/pkg/registry/trust.go +++ b/pkg/registry/trust.go @@ -5,13 +5,12 @@ import ( "encoding/json" "errors" "os" - "strings" + "github.com/containrrr/watchtower/pkg/registry/helpers" cliconfig "github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/credentials" "github.com/docker/cli/cli/config/types" - "github.com/docker/distribution/reference" log "github.com/sirupsen/logrus" ) @@ -19,7 +18,7 @@ import ( // loaded from environment variables or docker config // as available in that order func EncodedAuth(ref string) (string, error) { - auth, err := EncodedEnvAuth(ref) + auth, err := EncodedEnvAuth() if err != nil { auth, err = EncodedConfigAuth(ref) } @@ -29,7 +28,7 @@ func EncodedAuth(ref string) (string, error) { // EncodedEnvAuth returns an encoded auth config for the given registry // loaded from environment variables // Returns an error if authentication environment variables have not been set -func EncodedEnvAuth(ref string) (string, error) { +func EncodedEnvAuth() (string, error) { username := os.Getenv("REPO_USER") password := os.Getenv("REPO_PASS") if username != "" && password != "" { @@ -37,30 +36,34 @@ func EncodedEnvAuth(ref string) (string, error) { Username: username, Password: password, } - log.Debugf("Loaded auth credentials for user %s on registry %s", auth.Username, ref) - log.Tracef("Using auth password %s", auth.Password) + + log.Debugf("Loaded auth credentials for registry user %s from environment", auth.Username) + // CREDENTIAL: Uncomment to log REPO_PASS environment variable + // log.Tracef("Using auth password %s", auth.Password) + return EncodeAuth(auth) } - return "", errors.New("Registry auth environment variables (REPO_USER, REPO_PASS) not set") + return "", errors.New("registry auth environment variables (REPO_USER, REPO_PASS) not set") } // EncodedConfigAuth returns an encoded auth config for the given registry // loaded from the docker config // Returns an empty string if credentials cannot be found for the referenced server // The docker config must be mounted on the container -func EncodedConfigAuth(ref string) (string, error) { - server, err := ParseServerAddress(ref) +func EncodedConfigAuth(imageRef string) (string, error) { + server, err := helpers.GetRegistryAddress(imageRef) if err != nil { - log.Errorf("Unable to parse the image ref %s", err) + log.Errorf("Could not get registry from image ref %s", imageRef) return "", err } + configDir := os.Getenv("DOCKER_CONFIG") if configDir == "" { configDir = "/" } configFile, err := cliconfig.Load(configDir) if err != nil { - log.Errorf("Unable to find default config file %s", err) + log.Errorf("Unable to find default config file: %s", err) return "", err } credStore := CredentialsStore(*configFile) @@ -70,23 +73,12 @@ func EncodedConfigAuth(ref string) (string, error) { log.WithField("config_file", configFile.Filename).Debugf("No credentials for %s found", server) return "", nil } - log.Debugf("Loaded auth credentials for user %s, on registry %s, from file %s", auth.Username, ref, configFile.Filename) - log.Tracef("Using auth password %s", auth.Password) + log.Debugf("Loaded auth credentials for user %s, on registry %s, from file %s", auth.Username, server, configFile.Filename) + // CREDENTIAL: Uncomment to log docker config password + // log.Tracef("Using auth password %s", auth.Password) return EncodeAuth(auth) } -// ParseServerAddress extracts the server part from a container image ref -func ParseServerAddress(ref string) (string, error) { - - parsedRef, err := reference.Parse(ref) - if err != nil { - return ref, err - } - - parts := strings.Split(parsedRef.String(), "/") - return parts[0], nil -} - // CredentialsStore returns a new credentials store based // on the settings provided in the configuration file. func CredentialsStore(configFile configfile.ConfigFile) credentials.Store { diff --git a/pkg/registry/trust_test.go b/pkg/registry/trust_test.go index 3dab6ad..00fc8a7 100644 --- a/pkg/registry/trust_test.go +++ b/pkg/registry/trust_test.go @@ -1,65 +1,49 @@ package registry import ( + "os" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "os" ) -var _ = Describe("Testing with Ginkgo", func() { - It("encoded env auth_ should return an error if repo envs are unset", func() { - _ = os.Unsetenv("REPO_USER") - _ = os.Unsetenv("REPO_PASS") +var _ = Describe("Registry credential helpers", func() { + Describe("EncodedAuth", func() { + It("should return repo credentials from env when set", func() { + var err error + expected := "eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0=" - _, err := EncodedEnvAuth("") - Expect(err).To(HaveOccurred()) + err = os.Setenv("REPO_USER", "containrrr-user") + Expect(err).NotTo(HaveOccurred()) + + err = os.Setenv("REPO_PASS", "containrrr-pass") + Expect(err).NotTo(HaveOccurred()) + + config, err := EncodedEnvAuth() + Expect(config).To(Equal(expected)) + Expect(err).NotTo(HaveOccurred()) + }) }) - It("encoded env auth_ should return auth hash if repo envs are set", func() { - var err error - expectedHash := "eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0=" - err = os.Setenv("REPO_USER", "containrrr-user") - Expect(err).NotTo(HaveOccurred()) + Describe("EncodedEnvAuth", func() { + It("should return an error if repo envs are unset", func() { + _ = os.Unsetenv("REPO_USER") + _ = os.Unsetenv("REPO_PASS") - err = os.Setenv("REPO_PASS", "containrrr-pass") - Expect(err).NotTo(HaveOccurred()) - - config, err := EncodedEnvAuth("") - Expect(config).To(Equal(expectedHash)) - Expect(err).NotTo(HaveOccurred()) + _, err := EncodedEnvAuth() + Expect(err).To(HaveOccurred()) + }) }) - It("encoded config auth_ should return an error if file is not present", func() { - var err error - err = os.Setenv("DOCKER_CONFIG", "/dev/null/should-fail") - Expect(err).NotTo(HaveOccurred()) + Describe("EncodedConfigAuth", func() { + It("should return an error if file is not present", func() { + var err error - _, err = EncodedConfigAuth("") - Expect(err).To(HaveOccurred()) + err = os.Setenv("DOCKER_CONFIG", "/dev/null/should-fail") + Expect(err).NotTo(HaveOccurred()) - }) - /* - * TODO: - * This part only confirms that it still works in the same way as it did - * with the old version of the docker api client sdk. I'd say that - * ParseServerAddress likely needs to be elaborated a bit to default to - * dockerhub in case no server address was provided. - * - * ++ @simskij, 2019-04-04 - */ - It("parse server address_ should return error if passed empty string", func() { - - _, err := ParseServerAddress("") - Expect(err).To(HaveOccurred()) - }) - It("parse server address_ should return the organization part if passed an image name missing server name", func() { - - val, _ := ParseServerAddress("containrrr/config") - Expect(val).To(Equal("containrrr")) - }) - It("parse server address_ should return the server name if passed a fully qualified image name", func() { - - val, _ := ParseServerAddress("github.com/containrrrr/config") - Expect(val).To(Equal("github.com")) + _, err = EncodedConfigAuth("") + Expect(err).To(HaveOccurred()) + }) }) }) diff --git a/pkg/session/report.go b/pkg/session/report.go index 646a0c0..707eb91 100644 --- a/pkg/session/report.go +++ b/pkg/session/report.go @@ -1,8 +1,9 @@ package session import ( - "github.com/containrrr/watchtower/pkg/types" "sort" + + "github.com/containrrr/watchtower/pkg/types" ) type report struct { @@ -32,6 +33,33 @@ func (r *report) Stale() []types.ContainerReport { func (r *report) Fresh() []types.ContainerReport { return r.fresh } +func (r *report) All() []types.ContainerReport { + allLen := len(r.scanned) + len(r.updated) + len(r.failed) + len(r.skipped) + len(r.stale) + len(r.fresh) + all := make([]types.ContainerReport, 0, allLen) + + presentIds := map[types.ContainerID][]string{} + + appendUnique := func(reports []types.ContainerReport) { + for _, cr := range reports { + if _, found := presentIds[cr.ID()]; found { + continue + } + all = append(all, cr) + presentIds[cr.ID()] = nil + } + } + + appendUnique(r.updated) + appendUnique(r.failed) + appendUnique(r.skipped) + appendUnique(r.stale) + appendUnique(r.fresh) + appendUnique(r.scanned) + + sort.Sort(sortableContainers(all)) + + return all +} // NewReport creates a types.Report from the supplied Progress func NewReport(progress Progress) types.Report { diff --git a/pkg/sorter/sort.go b/pkg/sorter/sort.go index 1e27f1b..b9d1e12 100644 --- a/pkg/sorter/sort.go +++ b/pkg/sorter/sort.go @@ -2,13 +2,14 @@ package sorter import ( "fmt" - "github.com/containrrr/watchtower/pkg/container" "time" + + "github.com/containrrr/watchtower/pkg/types" ) // ByCreated allows a list of Container structs to be sorted by the container's // created date. -type ByCreated []container.Container +type ByCreated []types.Container func (c ByCreated) Len() int { return len(c) } func (c ByCreated) Swap(i, j int) { c[i], c[j] = c[j], c[i] } @@ -34,18 +35,18 @@ func (c ByCreated) Less(i, j int) bool { // the front of the list while containers with links will be sorted after all // of their dependencies. This sort order ensures that linked containers can // be started in the correct order. -func SortByDependencies(containers []container.Container) ([]container.Container, error) { +func SortByDependencies(containers []types.Container) ([]types.Container, error) { sorter := dependencySorter{} return sorter.Sort(containers) } type dependencySorter struct { - unvisited []container.Container + unvisited []types.Container marked map[string]bool - sorted []container.Container + sorted []types.Container } -func (ds *dependencySorter) Sort(containers []container.Container) ([]container.Container, error) { +func (ds *dependencySorter) Sort(containers []types.Container) ([]types.Container, error) { ds.unvisited = containers ds.marked = map[string]bool{} @@ -58,7 +59,7 @@ func (ds *dependencySorter) Sort(containers []container.Container) ([]container. return ds.sorted, nil } -func (ds *dependencySorter) visit(c container.Container) error { +func (ds *dependencySorter) visit(c types.Container) error { if _, ok := ds.marked[c.Name()]; ok { return fmt.Errorf("circular reference to %s", c.Name()) @@ -84,7 +85,7 @@ func (ds *dependencySorter) visit(c container.Container) error { return nil } -func (ds *dependencySorter) findUnvisited(name string) *container.Container { +func (ds *dependencySorter) findUnvisited(name string) *types.Container { for _, c := range ds.unvisited { if c.Name() == name { return &c @@ -94,7 +95,7 @@ func (ds *dependencySorter) findUnvisited(name string) *container.Container { return nil } -func (ds *dependencySorter) removeUnvisited(c container.Container) { +func (ds *dependencySorter) removeUnvisited(c types.Container) { var idx int for i := range ds.unvisited { if ds.unvisited[i].Name() == c.Name() { diff --git a/pkg/types/container.go b/pkg/types/container.go index 22742e9..8a22f44 100644 --- a/pkg/types/container.go +++ b/pkg/types/container.go @@ -1,8 +1,10 @@ package types import ( - "github.com/docker/docker/api/types" "strings" + + "github.com/docker/docker/api/types" + dc "github.com/docker/docker/api/types/container" ) // ImageID is a hash string representing a container image @@ -50,7 +52,7 @@ type Container interface { SafeImageID() ImageID ImageName() string Enabled() (bool, bool) - IsMonitorOnly() bool + IsMonitorOnly(UpdateParams) bool Scope() (string, bool) Links() []string ToRestart() bool @@ -62,4 +64,15 @@ type Container interface { GetLifecyclePostCheckCommand() string GetLifecyclePreUpdateCommand() string GetLifecyclePostUpdateCommand() string + VerifyConfiguration() error + SetStale(bool) + IsStale() bool + IsNoPull(UpdateParams) bool + SetLinkedToRestarting(bool) + IsLinkedToRestarting() bool + PreUpdateTimeout() int + PostUpdateTimeout() int + IsRestarting() bool + GetCreateConfig() *dc.Config + GetCreateHostConfig() *dc.HostConfig } diff --git a/pkg/types/convertible_notifier.go b/pkg/types/convertible_notifier.go index 37d8872..82d7b7b 100644 --- a/pkg/types/convertible_notifier.go +++ b/pkg/types/convertible_notifier.go @@ -1,16 +1,17 @@ package types import ( - "github.com/spf13/cobra" "time" + + "github.com/spf13/cobra" ) // ConvertibleNotifier is a notifier capable of creating a shoutrrr URL type ConvertibleNotifier interface { - GetURL(c *cobra.Command, title string) (string, error) + GetURL(c *cobra.Command) (string, error) } // DelayNotifier is a notifier that might need to be delayed before sending notifications type DelayNotifier interface { GetDelay() time.Duration -} \ No newline at end of file +} diff --git a/pkg/types/filterable_container.go b/pkg/types/filterable_container.go index 3c46295..b410b1c 100644 --- a/pkg/types/filterable_container.go +++ b/pkg/types/filterable_container.go @@ -7,4 +7,5 @@ type FilterableContainer interface { IsWatchtower() bool Enabled() (bool, bool) Scope() (string, bool) + ImageName() string } diff --git a/pkg/types/notifier.go b/pkg/types/notifier.go index ccb2cb6..478a4c4 100644 --- a/pkg/types/notifier.go +++ b/pkg/types/notifier.go @@ -4,6 +4,8 @@ package types type Notifier interface { StartNotification() SendNotification(Report) + AddLogHook() GetNames() []string + GetURLs() []string Close() } diff --git a/pkg/types/report.go b/pkg/types/report.go index 8013b58..f454fc6 100644 --- a/pkg/types/report.go +++ b/pkg/types/report.go @@ -8,6 +8,7 @@ type Report interface { Skipped() []ContainerReport Stale() []ContainerReport Fresh() []ContainerReport + All() []ContainerReport } // ContainerReport represents a container that was included in watchtower session diff --git a/pkg/types/update_params.go b/pkg/types/update_params.go index 611cc70..2b6d3c4 100644 --- a/pkg/types/update_params.go +++ b/pkg/types/update_params.go @@ -6,11 +6,13 @@ import ( // UpdateParams contains all different options available to alter the behavior of the Update func type UpdateParams struct { - Filter Filter - Cleanup bool - NoRestart bool - Timeout time.Duration - MonitorOnly bool - LifecycleHooks bool - RollingRestart bool + Filter Filter + Cleanup bool + NoRestart bool + Timeout time.Duration + MonitorOnly bool + NoPull bool + LifecycleHooks bool + RollingRestart bool + LabelPrecedence bool } diff --git a/scripts/build-tplprev.sh b/scripts/build-tplprev.sh new file mode 100755 index 0000000..293710c --- /dev/null +++ b/scripts/build-tplprev.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +cd $(git rev-parse --show-toplevel) + +cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ./docs/assets/ + +GOARCH=wasm GOOS=js go build -o ./docs/assets/tplprev.wasm ./tplprev \ No newline at end of file diff --git a/scripts/contnet-tests.sh b/scripts/contnet-tests.sh new file mode 100755 index 0000000..25269dc --- /dev/null +++ b/scripts/contnet-tests.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -e + +function exit_env_err() { + >&2 echo "Required environment variable not set: $1" + exit 1 +} + +if [ -z "$VPN_SERVICE_PROVIDER" ]; then exit_env_err "VPN_SERVICE_PROVIDER"; fi +if [ -z "$OPENVPN_USER" ]; then exit_env_err "OPENVPN_USER"; fi +if [ -z "$OPENVPN_PASSWORD" ]; then exit_env_err "OPENVPN_PASSWORD"; fi +# if [ -z "$SERVER_COUNTRIES" ]; then exit_env_err "SERVER_COUNTRIES"; fi + + +export SERVER_COUNTRIES=${SERVER_COUNTRIES:"Sweden"} +REPO_ROOT="$(git rev-parse --show-toplevel)" +COMPOSE_FILE="$REPO_ROOT/dockerfiles/container-networking/docker-compose.yml" +DEFAULT_WATCHTOWER="$REPO_ROOT/watchtower" +WATCHTOWER="$*" +WATCHTOWER=${WATCHTOWER:-$DEFAULT_WATCHTOWER} +echo "repo root path is $REPO_ROOT" +echo "watchtower path is $WATCHTOWER" +echo "compose file path is $COMPOSE_FILE" + +echo; echo "=== Forcing network container producer update..." + +echo "Pull previous version of gluetun..." +docker pull qmcgaw/gluetun:v3.34.3 +echo "Fake new version of gluetun by retagging v3.34.4 as v3.35.0..." +docker tag qmcgaw/gluetun:v3.34.3 qmcgaw/gluetun:v3.35.0 + +echo; echo "=== Creating containers..." + +docker compose -p "wt-contnet" -f "$COMPOSE_FILE" up -d + +echo; echo "=== Running watchtower" +$WATCHTOWER --run-once + +echo; echo "=== Removing containers..." + +docker compose -p "wt-contnet" -f "$COMPOSE_FILE" down diff --git a/scripts/docker-util.sh b/scripts/docker-util.sh index 13a84ca..bd0dbda 100644 --- a/scripts/docker-util.sh +++ b/scripts/docker-util.sh @@ -122,4 +122,65 @@ function container-started() { return 1 fi docker container inspect "$Name" | jq -r .[].State.StartedAt +} + + +function container-exists() { + local Name=$1 + if [ -z "$Name" ]; then + echo "NAME missing" + return 1 + fi + + docker container inspect "$Name" 1> /dev/null 2> /dev/null +} + +function registry-exists() { + container-exists "$CONTAINER_PREFIX-registry" +} + +function create-container() { + local container_name=$1 + if [ -z "$container_name" ]; then + echo "NAME missing" + return 1 + fi + local image_name="${2:-$container_name}" + + echo -en "Creating \e[94m$container_name\e[0m container... " + local result + result=$(docker run -d --name "$container_name" "$(registry-host)/$image_name" 2>&1) + if [ "${#result}" -eq 64 ]; then + echo -e "\e[92m${result:0:12}\e[0m" + return 0 + else + echo -e "\e[91mFailed!\n\e[97m$result\e[0m" + return 1 + fi +} + +function remove-images() { + local image_name=$1 + if [ -z "$image_name" ]; then + echo "NAME missing" + return 1 + fi + + local images + mapfile -t images < <(docker images -q "$image_name" | uniq) + if [ -n "${images[*]}" ]; then + docker image rm "${images[@]}" + else + echo "No images matched \"$image_name\"" + fi +} + +function remove-repo-images() { + local image_name=$1 + if [ -z "$image_name" ]; then + echo "NAME missing" + return 1 + fi + + remove-images "$(registry-host)/images/$image_name" } \ No newline at end of file diff --git a/scripts/du-cli.sh b/scripts/du-cli.sh old mode 100644 new mode 100755 index 973dec5..611f720 --- a/scripts/du-cli.sh +++ b/scripts/du-cli.sh @@ -16,7 +16,7 @@ case $1 in registry-host ;; *) - echo "Unknown keyword \"$2\"" + echo "Unknown registry action \"$2\"" ;; esac ;; @@ -28,8 +28,11 @@ case $1 in latest) latest-image-rev "$3" ;; + rm) + remove-repo-images "$3" + ;; *) - echo "Unknown keyword \"$2\"" + echo "Unknown image action \"$2\"" ;; esac ;; @@ -47,8 +50,26 @@ case $1 in started) container-started "$3" ;; + create) + create-container "${@:3:2}" + ;; + create-stale) + if [ -z "$3" ]; then + echo "NAME missing" + exit 1 + fi + if ! registry-exists; then + echo "Registry container missing! Creating..." + start-registry || exit 1 + fi + image_name="images/$3" + container_name=$3 + $0 image rev "$image_name" || exit 1 + $0 container create "$container_name" "$image_name" || exit 1 + $0 image rev "$image_name" || exit 1 + ;; *) - echo "Unknown keyword \"$2\"" + echo "Unknown container action \"$2\"" ;; esac ;; diff --git a/tplprev/main.go b/tplprev/main.go new file mode 100644 index 0000000..120f968 --- /dev/null +++ b/tplprev/main.go @@ -0,0 +1,49 @@ +//go:build !wasm + +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/containrrr/watchtower/internal/meta" + "github.com/containrrr/watchtower/pkg/notifications/preview" + "github.com/containrrr/watchtower/pkg/notifications/preview/data" +) + +func main() { + fmt.Fprintf(os.Stderr, "watchtower/tplprev %v\n\n", meta.Version) + + var states string + var entries string + + flag.StringVar(&states, "states", "cccuuueeekkktttfff", "sCanned, Updated, failEd, sKipped, sTale, Fresh") + flag.StringVar(&entries, "entries", "ewwiiidddd", "Fatal,Error,Warn,Info,Debug,Trace") + + flag.Parse() + + if len(flag.Args()) < 1 { + fmt.Fprintln(os.Stderr, "Missing required argument TEMPLATE") + flag.Usage() + os.Exit(1) + return + } + + input, err := os.ReadFile(flag.Arg(0)) + if err != nil { + + fmt.Fprintf(os.Stderr, "Failed to read template file %q: %v\n", flag.Arg(0), err) + os.Exit(1) + return + } + + result, err := preview.Render(string(input), data.StatesFromString(states), data.LevelsFromString(entries)) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to read template file %q: %v\n", flag.Arg(0), err) + os.Exit(1) + return + } + + fmt.Println(result) +} diff --git a/tplprev/main_wasm.go b/tplprev/main_wasm.go new file mode 100644 index 0000000..5e2ce6a --- /dev/null +++ b/tplprev/main_wasm.go @@ -0,0 +1,62 @@ +//go:build wasm + +package main + +import ( + "fmt" + + "github.com/containrrr/watchtower/internal/meta" + "github.com/containrrr/watchtower/pkg/notifications/preview" + "github.com/containrrr/watchtower/pkg/notifications/preview/data" + + "syscall/js" +) + +func main() { + fmt.Println("watchtower/tplprev v" + meta.Version) + + js.Global().Set("WATCHTOWER", js.ValueOf(map[string]any{ + "tplprev": js.FuncOf(jsTplPrev), + })) + <-make(chan bool) + +} + +func jsTplPrev(this js.Value, args []js.Value) any { + + if len(args) < 3 { + return "Requires 3 arguments passed" + } + + input := args[0].String() + + statesArg := args[1] + var states []data.State + + if statesArg.Type() == js.TypeString { + states = data.StatesFromString(statesArg.String()) + } else { + for i := 0; i < statesArg.Length(); i++ { + state := data.State(statesArg.Index(i).String()) + states = append(states, state) + } + } + + levelsArg := args[2] + var levels []data.LogLevel + + if levelsArg.Type() == js.TypeString { + levels = data.LevelsFromString(statesArg.String()) + } else { + for i := 0; i < levelsArg.Length(); i++ { + level := data.LogLevel(levelsArg.Index(i).String()) + levels = append(levels, level) + } + } + + result, err := preview.Render(input, states, levels) + if err != nil { + return "Error: " + err.Error() + } + return result +}