diff --git a/.all-contributorsrc b/.all-contributorsrc index d50406c..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", @@ -841,6 +843,12 @@ "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 ", @@ -849,6 +857,25 @@ "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, @@ -857,5 +884,6 @@ "repoType": "github", "repoHost": "https://github.com", "commitConvention": "none", - "skipCi": true + "skipCi": true, + "commitType": "docs" } diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 98f8387..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@v3 + 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@v2 + 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@v2 + 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@v2 + 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/publish-docs.yml b/.github/workflows/publish-docs.yml index e7ac0c7..4818e54 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -14,11 +14,17 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + 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@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' cache: 'pip' diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index a99bef1..f6866af 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -12,16 +12,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: 1.18.x - - uses: dominikh/staticcheck-action@a3513ade2e5cb8075ba1c1ed1890a989cf0f2aa0 #v1.2.0 + go-version: 1.20.x + - uses: dominikh/staticcheck-action@ba605356b4b29a60e87ab9404b712f3461e566dc #v1.3.0 with: - version: "2022.1.1" + version: "2023.1.6" install-go: "false" # StaticCheck uses go v1.17 which does not support `any` test: name: Test @@ -29,7 +29,7 @@ jobs: fail-fast: false matrix: go-version: - - 1.18.x + - 1.20.x platform: - macos-latest - windows-latest @@ -37,13 +37,13 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: 1.18.x + go-version: 1.20.x - name: Run tests run: | go test -v -coverprofile coverage.out -covermode atomic ./... @@ -56,15 +56,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: 1.18.x + go-version: 1.20.x - name: Build - uses: goreleaser/goreleaser-action@b508e2e3ef3b19d4e4146d4f8fb3ba9db644a757 #v3 + 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 1aa1373..95ee68d 100644 --- a/.github/workflows/release-dev.yaml +++ b/.github/workflows/release-dev.yaml @@ -10,21 +10,23 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v3 + - uses: actions/checkout@v4 with: - go-version: 1.18 + 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@v3 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: 1.18 + go-version: 1.20.x - name: Test run: go test -v -coverprofile coverage.out -covermode atomic ./... - name: Publish coverage @@ -37,7 +39,7 @@ jobs: - test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Publish to Docker Hub uses: jerray/publish-docker-action@87d84711629b0dc9f6bb127b568413cc92a2088e #master@2022-10-14 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf0d61d..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,14 +13,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: 1.18.x - - uses: dominikh/staticcheck-action@a3513ade2e5cb8075ba1c1ed1890a989cf0f2aa0 #v1.2.0 + 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` @@ -32,7 +30,7 @@ jobs: strategy: matrix: go-version: - - 1.18.x + - 1.20.x platform: - ubuntu-latest - macos-latest @@ -40,13 +38,13 @@ jobs: runs-on: ${{ matrix.platform }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: 1.18.x + go-version: 1.20.x - name: Run tests run: | go test ./... -coverprofile coverage.out @@ -59,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@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v4 with: - go-version: 1.18.x + go-version: 1.20.x - name: Login to Docker Hub - uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a #v2 + uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc #v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR - uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a #v2 + 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@b508e2e3ef3b19d4e4146d4f8fb3ba9db644a757 #v3 + uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 #v3 with: version: v0.155.0 args: --debug @@ -93,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 \ @@ -191,7 +189,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Pull new module version - uses: andrewslotin/go-proxy-pull-action@bfc19ec6536e1638181b2ad6a03e16c7ccfb122f #master@2022-10-14 + 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 cc8c0bb..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. @@ -44,126 +46,128 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + + + + + + - - -
James
James

⚠️ 🤔
Florian
Florian

👀 📖
Brian DeHamer
Brian DeHamer

💻 🚧
Ross Cadogan
Ross Cadogan

💻
stffabi
stffabi

💻 🚧
Austin
Austin

📖
David Gardner
David Gardner

👀 📖
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

💻 🚧
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

📖
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

💻
fomk
fomk

💻
Sven Gottwald
Sven Gottwald

🚇
techknowlogick
techknowlogick

💻
waja
waja

📖
Scott Albertson
Scott Albertson

📖
Jason Huddleston
Jason Huddleston

📖
Napster
Napster

💻
Robotex
Robotex

📖
Gerald Pape
Gerald Pape

📖
fomk
fomk

💻
Sven Gottwald
Sven Gottwald

🚇
techknowlogick
techknowlogick

💻
waja
waja

📖
Scott Albertson
Scott Albertson

📖
Maxim
Maxim

💻 📖
Max Schmitt
Max Schmitt

📖
cron410
cron410

📖
Paulo Henrique
Paulo Henrique

📖
Kaleb Elwert
Kaleb Elwert

📖
Bill Butler
Bill Butler

📖
Mario Tacke
Mario Tacke

💻
Jason Huddleston
Jason Huddleston

📖
Napster
Napster

💻
Maxim
Maxim

💻 📖
Max Schmitt
Max Schmitt

📖
cron410
cron410

📖
Paulo Henrique
Paulo Henrique

📖
Kaleb Elwert
Kaleb Elwert

📖
Mark Woodbridge
Mark Woodbridge

💻
Simon Aronsson
Simon Aronsson

💻 🚧 👀 📖
Ansem93
Ansem93

📖
Luka Peschke
Luka Peschke

💻 📖
Zois Pagoulatos
Zois Pagoulatos

💻 👀 🚧
Alexandre Menif
Alexandre Menif

💻
Andrey
Andrey

📖
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

💻
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

📖
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.

💻
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

💻
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

📖
Oliver Cervera
Oliver Cervera

📖
Victor Moura
Victor Moura

⚠️ 💻 📖
Maximilian Brandau
Maximilian Brandau

💻 ⚠️
Andrew
Andrew

📖
sixcorners
sixcorners

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

📖 💻
Arne Jørgensen
Arne Jørgensen

⚠️ 👀
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

🐛 💡
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

💻
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

📖
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

📖
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

📖
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/cmd/root.go b/cmd/root.go index 8e20b95..eef13ce 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "math" "net/http" "os" @@ -28,17 +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 + 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() @@ -77,23 +81,8 @@ func Execute() { func PreRun(cmd *cobra.Command, _ []string) { f := cmd.PersistentFlags() flags.ProcessFlagAliases(f) - - 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, - }) - } - - rawLogLevel, _ := f.GetString(`log-level`) - if logLevel, err := log.ParseLevel(rawLogLevel); err != nil { - log.Fatalf("Invalid log level: %s", err.Error()) - } else { - log.SetLevel(logLevel) + if err := flags.SetupLogging(f); err != nil { + log.Fatalf("Failed to initialize logging: %s", err.Error()) } scheduleSpec, _ = f.GetString("schedule") @@ -106,9 +95,11 @@ 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") if scope != "" { log.Debugf(`Using scope %q`, scope) @@ -120,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") @@ -132,7 +123,6 @@ func PreRun(cmd *cobra.Command, _ []string) { } client = container.NewClient(container.ClientOptions{ - PullImages: !noPull, IncludeStopped: includeStopped, ReviveStopped: reviveStopped, RemoveVolumes: removeVolumes, @@ -146,12 +136,22 @@ func PreRun(cmd *cobra.Command, _ []string) { // 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") @@ -182,9 +182,12 @@ func Run(c *cobra.Command, names []string) { httpAPI := api.New(apiToken) if enableUpdateAPI { - updateHandler := update.New(func(images []string) { runUpdatesWithNotifications(filters.FilterByImage(images, 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) @@ -196,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) } @@ -356,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 2d5a181..2fc571d 100644 --- a/dockerfiles/Dockerfile +++ b/dockerfiles/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM alpine:3.16.2 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/arguments.md b/docs/arguments.md index 58e7e7b..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 @@ -107,6 +134,17 @@ Environment Variable: WATCHTOWER_LOG_LEVEL 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. @@ -151,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 @@ -178,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 @@ -188,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 @@ -211,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. @@ -234,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. @@ -248,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 @@ -267,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 @@ -289,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 @@ -360,4 +435,33 @@ requests and may rate limit pull requests (mainly docker.io). Environment Variable: WATCHTOWER_WARN_ON_HEAD_FAILURE Possible values: always, auto, never Default: auto -``` \ No newline at end of file +``` + +## 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 4b6facd..8327c66 100644 --- a/docs/container-selection.md +++ b/docs/container-selection.md @@ -58,6 +58,7 @@ If instead you want to [only include containers with the enable label](https://c 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 3905abf..d5da4fe 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -18,19 +18,20 @@ system, [logrus](http://github.com/sirupsen/logrus). - `--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_NOTIFICATION_DELAY`): Delay before sending notifications expressed in seconds. +- `--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-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. -## [shoutrrr](https://github.com/containrrr/shoutrrr) notifications +## [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.6/services/overview](https://containrrr.dev/shoutrrr/v0.6/services/overview) to +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) @@ -56,6 +57,10 @@ outputs timestamp and log level. 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 @@ -110,7 +115,7 @@ Example using a custom report template that always sends a session report after docker run -d \ --name watchtower \ -v /var/run/docker.sock:/var/run/docker.sock \ - -e WATCHTOWER_NOTIFICATION_REPORT="true" + -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 -}} @@ -130,7 +135,7 @@ Example using a custom report template that always sends a session report after {{- end -}} {{- end -}} {{- else -}} - {{range .Entries -}}{{.Message}}{{"\n"}}{{- end -}} + {{range .Entries -}}{{.Message}}{{\"\n\"}}{{- end -}} {{- end -}} " \ containrrr/watchtower diff --git a/docs/private-registries.md b/docs/private-registries.md index ee4ed41..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 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 fb5e675..e552129 100644 --- a/docs/stylesheets/theme.css +++ b/docs/stylesheets/theme.css @@ -14,56 +14,56 @@ --md-hue: 199; /* Primary and accent */ - --md-primary-fg-color: hsla(199deg 27% 35% 100%); - --md-primary-fg-color--link: hsla(199deg 45% 65% 100%); - --md-primary-fg-color--light: hsla(198deg 19% 73% 100%); - --md-primary-fg-color--dark: hsla(194deg 100% 13% 100%); - --md-accent-fg-color: hsla(194deg 45% 50% 100%); - --md-accent-fg-color--transparent: hsla(194deg 45% 50% 6.3%); + --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: hsla(var(--md-hue) 75% 95% 100%); - --md-default-fg-color--light: hsla(var(--md-hue) 75% 90% 62%); - --md-default-fg-color--lighter: hsla(var(--md-hue) 75% 90% 32%); - --md-default-fg-color--lightest: hsla(var(--md-hue) 75% 90% 12%); - --md-default-bg-color: hsla(var(--md-hue) 15% 21% 100%); - --md-default-bg-color--light: hsla(var(--md-hue) 15% 21% 54%); - --md-default-bg-color--lighter: hsla(var(--md-hue) 15% 21% 26%); - --md-default-bg-color--lightest: hsla(var(--md-hue) 15% 21% 7%); + --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: hsla(var(--md-hue) 18% 86% 100%); - --md-code-bg-color: hsla(var(--md-hue) 15% 15% 100%); - --md-code-hl-color: hsla(218deg 100% 63% 15%); - --md-code-hl-number-color: hsla(346deg 74% 63% 100%); - --md-code-hl-special-color: hsla(320deg 83% 66% 100%); - --md-code-hl-function-color: hsla(271deg 57% 65% 100%); - --md-code-hl-constant-color: hsla(230deg 62% 70% 100%); - --md-code-hl-keyword-color: hsla(199deg 33% 64% 100%); - --md-code-hl-string-color: hsla( 50deg 34% 74% 100%); + --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: hsla(241deg 22% 60% 100%); + --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: hsla(218deg 100% 63% 30%); - --md-typeset-kbd-color: hsla(var(--md-hue) 15% 94% 12%); - --md-typeset-kbd-accent-color: hsla(var(--md-hue) 15% 94% 20%); - --md-typeset-kbd-border-color: hsla(var(--md-hue) 15% 14% 100%); - --md-typeset-table-color: hsla(var(--md-hue) 75% 95% 12%); + --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: hsla(var(--md-hue) 15% 12% 87%); - --md-footer-bg-color--dark: hsla(var(--md-hue) 15% 10% 100%); + --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: 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 8c1e12f..1cac352 100644 --- a/docs/usage-overview.md +++ b/docs/usage-overview.md @@ -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 87875bd..6c20d11 100644 --- a/go.mod +++ b/go.mod @@ -1,69 +1,74 @@ module github.com/containrrr/watchtower -go 1.18 +go 1.20 require ( - github.com/containrrr/shoutrrr v0.6.1 - github.com/docker/cli v20.10.21+incompatible - github.com/docker/distribution v2.8.1+incompatible - github.com/docker/docker v20.10.21+incompatible + 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/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.23.0 - github.com/prometheus/client_golang v1.13.0 - github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 - github.com/sirupsen/logrus v1.9.0 - github.com/spf13/cobra v1.6.0 + 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.13.0 - github.com/stretchr/testify v1.8.1 - golang.org/x/net v0.1.0 + 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.1.2 // indirect - github.com/davecgh/go-spew v1.1.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.13.0 // indirect - github.com/fsnotify/fsnotify v1.5.4 // 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.2 // indirect - github.com/google/go-cmp v0.5.9 // 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.0.1 // indirect - github.com/magiconair/properties v1.8.6 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // 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/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 v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.5 // 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.0 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect - github.com/spf13/afero v1.8.2 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // 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.4.1 // indirect - golang.org/x/sys v0.1.0 // indirect - golang.org/x/text v0.4.0 - golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect - google.golang.org/protobuf v1.28.1 // 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.v2 v2.4.0 // 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 c57ef97..cab338f 100644 --- a/go.sum +++ b/go.sum @@ -1,763 +1,240 @@ -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.44.3/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 v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -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/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -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/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -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= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 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/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.17 h1:iT12IBVClFevaf8PuVyi3UmZOVh4OqnaLxDTW2O6j3w= github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -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/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -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/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/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/containrrr/shoutrrr v0.6.1 h1:6ih7jA6mo3t6C97MZbd3SxL/kRizOE3bI9CpBQZ6wzg= -github.com/containrrr/shoutrrr v0.6.1/go.mod h1:ye9jGX5YzMnJ76waaNVWlJ4luhMEyt1EWU5unYTQSb0= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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/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/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/docker/cli v20.10.21+incompatible h1:qVkgyYUnOLQ98LtXBrwd/duVqPT2X4SHndOuGsfwyhU= -github.com/docker/cli v20.10.21+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= -github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v20.10.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog= -github.com/docker/docker v20.10.21+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-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -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/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +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/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -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-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-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -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-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +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/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/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-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/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 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/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -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.4.1/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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -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-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/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/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/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/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/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/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -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/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -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/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -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/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +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/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.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -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.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -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/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +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/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.6/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.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/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 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.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= +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.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys= -github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +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.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -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.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pkg/errors v0.8.0/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/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -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/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.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= -github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= -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-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.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= -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.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -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/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +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/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -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/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.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -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/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= -github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= -github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= -github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= -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/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.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= -github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= -github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= +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.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.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.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -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.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -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-20180904163835-0709b304e793/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-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-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -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/lint v0.0.0-20201208152925-83fdc39ff7b5/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/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/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-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-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/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-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -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/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +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-20200317015054-43a5402ce75a/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-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-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/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-20191026070338-33540a1f6037/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-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-20200122134326-e047566fdf82/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-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-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -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-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-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-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-20200227222343-706bc42d1f0d/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-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -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.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -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/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -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-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-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-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -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.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.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -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-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/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/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -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= 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 2afc43c..737404a 100644 --- a/internal/actions/mocks/client.go +++ b/internal/actions/mocks/client.go @@ -5,8 +5,6 @@ import ( "fmt" "time" - "github.com/containrrr/watchtower/pkg/container" - t "github.com/containrrr/watchtower/pkg/types" ) @@ -21,7 +19,7 @@ type MockClient struct { type TestData struct { TriedToRemoveImageCount int NameOfContainerToKeep string - Containers []container.Container + Containers []t.Container Staleness map[string]bool } @@ -40,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") } @@ -53,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 } @@ -69,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 } @@ -88,7 +86,7 @@ func (client MockClient) ExecuteCommand(_ t.ContainerID, command string, _ int) } // IsContainerStale is true if not explicitly stated in TestData for the mock client -func (client MockClient) IsContainerStale(cont container.Container) (bool, t.ImageID, error) { +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 @@ -97,6 +95,6 @@ func (client MockClient) IsContainerStale(cont container.Container) (bool, t.Ima } // 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 3272d63..e830587 100644 --- a/internal/actions/mocks/container.go +++ b/internal/actions/mocks/container.go @@ -14,7 +14,7 @@ import ( ) // 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, @@ -31,7 +31,7 @@ func CreateMockContainer(id string, name string, image string, created time.Time ExposedPorts: map[nat.Port]struct{}{}, }, } - return *container.NewContainer( + return container.NewContainer( &content, CreateMockImageInfo(image), ) @@ -48,12 +48,12 @@ func CreateMockImageInfo(image string) *types.ImageInspect { } // 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, @@ -66,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, @@ -97,14 +97,14 @@ func CreateMockContainerWithConfig(id string, name string, image string, running }, Config: config, } - return *container.NewContainer( + return container.NewContainer( &content, 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 @@ -120,7 +120,7 @@ func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (con } // CreateMockContainerWithLinks should only be used for testing -func CreateMockContainerWithLinks(id string, name string, image string, created time.Time, links []string, imageInfo *types.ImageInspect) container.Container { +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, @@ -136,7 +136,7 @@ func CreateMockContainerWithLinks(id string, name string, image string, created Labels: make(map[string]string), }, } - return *container.NewContainer( + return container.NewContainer( &content, imageInfo, ) diff --git a/internal/actions/update.go b/internal/actions/update.go index bd3791f..8853c6e 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -2,7 +2,6 @@ package actions import ( "errors" - "strings" "github.com/containrrr/watchtower/internal/util" "github.com/containrrr/watchtower/pkg/container" @@ -34,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() @@ -58,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++ @@ -72,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()) } } @@ -97,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)) @@ -109,7 +106,7 @@ 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].Stale { + } else if containers[i].IsStale() { // Only add (previously) stale containers' images to cleanup cleanupImageIDs[containers[i].ImageID()] = true } @@ -123,7 +120,7 @@ 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-- { @@ -138,7 +135,7 @@ func stopContainersInReversedOrder(containers []container.Container, client cont 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 @@ -149,7 +146,7 @@ func stopStaleContainer(container container.Container, client container.Client, } // Perform an additional check here to prevent us from stopping a linked container we cannot restart - if container.LinkedToRestarting { + if container.IsLinkedToRestarting() { if err := container.VerifyConfiguration(); err != nil { return err } @@ -175,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)) @@ -186,7 +183,7 @@ func restartContainersInSortedOrder(containers []container.Container, client con if stoppedImages[c.SafeImageID()] { if err := restartStaleContainer(c, client, params); err != nil { failed[c.ID()] = err - } else if c.Stale { + } else if c.IsStale() { // Only add (previously) stale containers' images to cleanup cleanupImageIDs[c.ImageID()] = true } @@ -211,7 +208,7 @@ func cleanupImages(client container.Client, imageIDs map[types.ImageID]bool) { } } -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 @@ -236,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() { @@ -250,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) } } @@ -258,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 eb540b1..9209dcd 100644 --- a/internal/actions/update_test.go +++ b/internal/actions/update_test.go @@ -4,7 +4,6 @@ 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" @@ -18,7 +17,7 @@ import ( func getCommonTestData(keepContainer string) *TestData { return &TestData{ NameOfContainerToKeep: keepContainer, - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container-01", "test-container-01", @@ -59,7 +58,7 @@ func getLinkedTestData(withImageInfo bool) *TestData { return &TestData{ Staleness: map[string]bool{linkingContainer.Name(): false}, - Containers: []container.Container{ + Containers: []types.Container{ staleContainer, linkingContainer, }, @@ -130,7 +129,7 @@ var _ = Describe("the update action", func() { client := CreateMockClient( &TestData{ NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container-01", "test-container-01", @@ -163,7 +162,7 @@ var _ = Describe("the update action", func() { It("should not update any containers", func() { client := CreateMockClient( &TestData{ - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container-01", "test-container-01", @@ -179,12 +178,84 @@ var _ = Describe("the update action", func() { false, false, ) - _, 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() { @@ -194,7 +265,7 @@ var _ = Describe("the update action", func() { client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", @@ -227,7 +298,7 @@ var _ = Describe("the update action", func() { client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", @@ -259,7 +330,7 @@ var _ = Describe("the update action", func() { client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", @@ -300,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", @@ -316,7 +387,7 @@ var _ = Describe("the update action", func() { ExposedPorts: map[nat.Port]struct{}{}, }) - containers := []container.Container{ + containers := []types.Container{ provider, consumer, } @@ -338,7 +409,7 @@ var _ = Describe("the update action", func() { client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", @@ -370,7 +441,7 @@ var _ = Describe("the update action", func() { client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 5428b95..c11cdae 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -4,8 +4,8 @@ import ( "bufio" "errors" "fmt" - "io/ioutil" "os" + "regexp" "strings" "time" @@ -24,9 +24,9 @@ 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 @@ -35,132 +35,145 @@ 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/ @@ -173,19 +186,31 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { flags.StringP( "scope", "", - viper.GetString("WATCHTOWER_SCOPE"), + envString("WATCHTOWER_SCOPE"), "Defines a monitoring scope for the Watchtower instance.") flags.StringP( "porcelain", "P", - viper.GetString("WATCHTOWER_PORCELAIN"), + envString("WATCHTOWER_PORCELAIN"), `Write session results to stdout using a stable versioned format. Supported values: "v1"`) flags.String( "log-level", - viper.GetString("WATCHTOWER_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 @@ -195,177 +220,202 @@ 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", "", - viper.GetInt("WATCHTOWER_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", "", - viper.GetString("WATCHTOWER_NOTIFICATION_TITLE_TAG"), + envString("WATCHTOWER_NOTIFICATION_TITLE_TAG"), "Title prefix tag for notifications") flags.Bool("notification-skip-title", - viper.GetBool("WATCHTOWER_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", - viper.GetBool("WATCHTOWER_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() { viper.AutomaticEnv() @@ -379,6 +429,7 @@ func SetDefaults() { 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 @@ -467,14 +518,17 @@ func GetSecretsFromFiles(rootCmd *cobra.Command) { "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) { +func getSecretFromFile(flags *pflag.FlagSet, secret string) error { flag := flags.Lookup(secret) if sliceValue, ok := flag.Value.(pflag.SliceValue); ok { oldValues := sliceValue.GetSlice() @@ -483,7 +537,7 @@ func getSecretFromFile(flags *pflag.FlagSet, secret string) { if value != "" && isFile(value) { file, err := os.Open(value) if err != nil { - log.Fatal(err) + return err } scanner := bufio.NewScanner(file) for scanner.Scan() { @@ -493,25 +547,26 @@ func getSecretFromFile(flags *pflag.FlagSet, secret string) { } values = append(values, line) } + if err := file.Close(); err != nil { + return err + } } else { values = append(values, value) } } - sliceValue.Replace(values) - return + 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 { @@ -564,19 +619,59 @@ func ProcessFlagAliases(flags *pflag.FlagSet) { // 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)) + _ = flags.Set(`schedule`, fmt.Sprintf(`@every %ds`, interval)) } if flagIsEnabled(flags, `debug`) { - flags.Set(`log-level`, `debug`) + _ = flags.Set(`log-level`, `debug`) } if flagIsEnabled(flags, `trace`) { - flags.Set(`log-level`, `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 { @@ -593,7 +688,7 @@ func appendFlagValue(flags *pflag.FlagSet, name string, values ...string) error if flagValues, ok := flag.Value.(pflag.SliceValue); ok { for _, value := range values { - flagValues.Append(value) + _ = flagValues.Append(value) } } else { return fmt.Errorf(`the value for flag %q is not a slice value`, name) diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index ca6f4ae..2856456 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -1,20 +1,22 @@ 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") + _ = os.Unsetenv("DOCKER_TLS_VERIFY") + _ = os.Unsetenv("DOCKER_HOST") cmd := new(cobra.Command) SetDefaults() @@ -48,10 +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) - defer os.Unsetenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD") + t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value) testGetSecretsFromFiles(t, "notification-email-server-password", value) } @@ -60,18 +59,15 @@ 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) - defer os.Unsetenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD") + t.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name()) testGetSecretsFromFiles(t, "notification-email-server-password", value) } @@ -80,16 +76,15 @@ func TestGetSliceSecretsFromFiles(t *testing.T) { values := []string{"entry2", "", "entry3"} // 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. for _, value := range values { _, err = file.WriteString("\n" + value) require.NoError(t, err) } - file.Close() + require.NoError(t, file.Close()) testGetSecretsFromFiles(t, "notification-url", `[entry1,entry2,entry3]`, `--notification-url`, "entry1", @@ -99,6 +94,7 @@ func TestGetSliceSecretsFromFiles(t *testing.T) { 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) @@ -166,9 +162,7 @@ func TestProcessFlagAliases(t *testing.T) { func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) { cmd := new(cobra.Command) - err := os.Setenv("WATCHTOWER_DEBUG", `true`) - require.NoError(t, err) - defer os.Unsetenv("WATCHTOWER_DEBUG") + t.Setenv("WATCHTOWER_DEBUG", `true`) SetDefaults() RegisterDockerFlags(cmd) @@ -183,6 +177,57 @@ func TestProcessFlagAliasesLogLevelFromEnvironment(t *testing.T) { 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) @@ -202,9 +247,7 @@ func TestProcessFlagAliasesSchedAndInterval(t *testing.T) { func TestProcessFlagAliasesScheduleFromEnvironment(t *testing.T) { cmd := new(cobra.Command) - err := os.Setenv("WATCHTOWER_SCHEDULE", `@hourly`) - require.NoError(t, err) - defer os.Unsetenv("WATCHTOWER_SCHEDULE") + t.Setenv("WATCHTOWER_SCHEDULE", `@hourly`) SetDefaults() RegisterDockerFlags(cmd) @@ -234,3 +277,63 @@ func TestProcessFlagAliasesInvalidPorcelaineVersion(t *testing.T) { 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 f87708f..5227004 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,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/container/client.go b/pkg/container/client.go index f534bd0..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,23 +25,23 @@ 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 +// - 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) @@ -57,7 +57,6 @@ func NewClient(opts ClientOptions) Client { // ClientOptions contains the options for how the docker client wrapper should behave type ClientOptions struct { - PullImages bool RemoveVolumes bool IncludeStopped bool ReviveStopped bool @@ -82,7 +81,7 @@ type dockerClient struct { ClientOptions } -func (client dockerClient) WarnOnHeadPullFailed(container Container) bool { +func (client dockerClient) WarnOnHeadPullFailed(container t.Container) bool { if client.WarnOnHeadFailed == WarnAlways { return true } @@ -93,8 +92,8 @@ func (client dockerClient) WarnOnHeadPullFailed(container Container) bool { 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 { @@ -149,24 +148,40 @@ func (client dockerClient) createListFilter() filters.Args { 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 == "" { @@ -186,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 sdkClient.IsErrNotFound(err) { + log.Debugf("Container %s not found, skipping removal.", shortID) + return nil + } return err } } @@ -204,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 { @@ -224,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 @@ -256,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()) @@ -267,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 @@ -285,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) @@ -306,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() @@ -355,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 } @@ -365,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 } @@ -474,7 +538,7 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e 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 02b31eb..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,6 +12,7 @@ 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" @@ -33,8 +38,8 @@ 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 := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAlways}} @@ -64,8 +69,72 @@ var _ = Describe("the client", func() { When("the image consist of a pinned hash", func() { It("should gracefully fail with a useful message", func() { c := dockerClient{} - pinnedContainer := *mockContainerWithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b") - c.PullImage(context.Background(), pinnedContainer) + 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()) }) }) }) @@ -73,10 +142,10 @@ var _ = Describe("the client", 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, - ClientOptions: ClientOptions{PullImages: false}, + ClientOptions: ClientOptions{}, } containers, err := client.ListContainers(filters.NoFilter) Expect(err).NotTo(HaveOccurred()) @@ -86,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, - ClientOptions: ClientOptions{PullImages: false}, + ClientOptions: ClientOptions{}, } containers, err := client.ListContainers(filter) Expect(err).NotTo(HaveOccurred()) @@ -100,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, - ClientOptions: ClientOptions{PullImages: false}, + ClientOptions: ClientOptions{}, } containers, err := client.ListContainers(filters.WatchtowerContainersFilter) Expect(err).NotTo(HaveOccurred()) @@ -113,10 +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, - ClientOptions: ClientOptions{PullImages: false, IncludeStopped: true}, + ClientOptions: ClientOptions{IncludeStopped: true}, } containers, err := client.ListContainers(filters.NoFilter) Expect(err).NotTo(HaveOccurred()) @@ -126,10 +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, - ClientOptions: ClientOptions{PullImages: false, IncludeRestarting: true}, + ClientOptions: ClientOptions{IncludeRestarting: true}, } containers, err := client.ListContainers(filters.NoFilter) Expect(err).NotTo(HaveOccurred()) @@ -139,30 +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, - ClientOptions: ClientOptions{PullImages: false, IncludeRestarting: false}, + 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, - ClientOptions: ClientOptions{PullImages: false}, + 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") @@ -219,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 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) gt.GomegaMatcher { - return WithTransform(func(container Container) bool { - return container.containerInfo.State.Restarting + return WithTransform(func(container t.Container) bool { + return container.ContainerInfo().State.Restarting }, Equal(expected)) } func havingRunningState(expected bool) gt.GomegaMatcher { - return WithTransform(func(container Container) bool { - return container.containerInfo.State.Running + 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 0bbea16..10ed677 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -2,12 +2,14 @@ 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" @@ -32,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 @@ -109,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 @@ -144,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 } @@ -153,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 @@ -217,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 @@ -252,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) @@ -272,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 6cd5c86..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,45 +263,123 @@ 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{ + 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() @@ -232,53 +389,3 @@ var _ = Describe("the container", func() { }) }) - -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) -} diff --git a/pkg/container/errors.go b/pkg/container/errors.go index 0b72067..05dc722 100644 --- a/pkg/container/errors.go +++ b/pkg/container/errors.go @@ -5,3 +5,4 @@ import "errors" var errorNoImageInfo = errors.New("no available image info") var errorNoContainerInfo = errors.New("no available container info") 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/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 fa5ed2a..4fa0bcd 100644 --- a/pkg/filters/filters.go +++ b/pkg/filters/filters.go @@ -13,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 @@ -41,6 +41,22 @@ func FilterByNames(names []string, baseFilter t.Filter) t.Filter { } } +// 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 { @@ -70,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) } @@ -103,10 +120,11 @@ func FilterByImage(images []string, baseFilter t.Filter) t.Filter { } // 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("which name matches \"") @@ -118,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 @@ -125,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 951e7ca..2b5cb5e 100644 --- a/pkg/filters/filters_test.go +++ b/pkg/filters/filters_test.go @@ -111,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) @@ -171,7 +218,7 @@ func TestFilterByImage(t *testing.T) { func TestBuildFilter(t *testing.T) { names := []string{"test", "valid"} - filter, desc := BuildFilter(names, false, "") + filter, desc := BuildFilter(names, []string{}, false, "") assert.Contains(t, desc, "test") assert.Contains(t, desc, "or") assert.Contains(t, desc, "valid") @@ -210,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) @@ -235,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 index 64a53c0..84c0f54 100644 --- a/pkg/notifications/common_templates.go +++ b/pkg/notifications/common_templates.go @@ -35,5 +35,6 @@ var commonTemplates = map[string]string{ no containers matched filter {{- end -}} {{- end -}}`, -} + `json.v1`: `{{ . | ToJSON }}`, +} diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index b6883a2..9103d38 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -63,6 +63,7 @@ func (e *emailTypeNotifier) GetURL(c *cobra.Command) (string, error) { UseHTML: false, Encryption: shoutrrrSmtp.EncMethods.Auto, Auth: shoutrrrSmtp.AuthTypes.None, + ClientHost: "localhost", } if len(e.User) > 0 { 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/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 47141e8..cc3a931 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -10,10 +10,9 @@ import ( "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" - "golang.org/x/text/cases" - "golang.org/x/text/language" ) // LocalLog is a logrus logger that does not send entries as notifications @@ -61,7 +60,7 @@ func (n *shoutrrrTypeNotifier) GetNames() []string { return names } -// GetNames returns a list of URLs for notification services that has been added +// GetURLs returns a list of URLs for notification services that has been added func (n *shoutrrrTypeNotifier) GetURLs() []string { return n.Urls } @@ -74,7 +73,7 @@ func (n *shoutrrrTypeNotifier) AddLogHook() { n.receiving = true log.AddHook(n) - // Do the sending in a separate goroutine so we don't block the main process. + // Do the sending in a separate goroutine, so we don't block the main process. go sendNotifications(n) } @@ -110,6 +109,7 @@ func createNotifier(urls []string, level log.Level, tplString string, legacy boo legacyTemplate: legacy, data: data, params: params, + delay: delay, } } @@ -207,12 +207,8 @@ 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": cases.Title(language.AmericanEnglish).String, - } - tplBase := template.New("").Funcs(funcs) + + tplBase := template.New("").Funcs(templates.Funcs) if builtin, found := commonTemplates[tplString]; found { log.WithField(`template`, tplString).Debug(`Using common template`) @@ -240,16 +236,3 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, return } - -// 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/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 23aef60..99b05c9 100644 --- a/pkg/registry/auth/auth.go +++ b/pkg/registry/auth/auth.go @@ -4,14 +4,14 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "strings" "github.com/containrrr/watchtower/pkg/registry/helpers" "github.com/containrrr/watchtower/pkg/types" - "github.com/docker/distribution/reference" + ref "github.com/distribution/reference" "github.com/sirupsen/logrus" ) @@ -20,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 { @@ -55,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(), registryAuth) + return GetBearerHeader(challenge, normalizedRef, registryAuth) } return "", errors.New("unsupported challenge type from registry") @@ -73,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, 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 @@ -91,7 +88,8 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string, 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.") @@ -102,7 +100,7 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string, return "", err } - body, _ := ioutil.ReadAll(authResponse.Body) + body, _ := io.ReadAll(authResponse.Body) tokenResponse := &types.TokenResponse{} err = json.Unmarshal(body, tokenResponse) @@ -114,7 +112,7 @@ func GetBearerHeader(challenge string, img string, registryAuth string) (string, } // 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") @@ -123,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"], @@ -141,53 +138,25 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) { 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 fa17bbc..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,8 +36,11 @@ 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") @@ -48,19 +50,20 @@ func EncodedEnvAuth(ref string) (string, error) { // 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/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/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/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 +}