From 589b00bbe1e3cf56f6e1420974fd848e19cd6dca Mon Sep 17 00:00:00 2001 From: ksurl Date: Thu, 3 Dec 2020 04:23:15 -0800 Subject: [PATCH 01/98] fix host flag (#685) --- docs/arguments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/arguments.md b/docs/arguments.md index b80257a..64bbdfc 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -102,7 +102,7 @@ Environment Variable: NO_COLOR Docker daemon socket to connect to. Can be pointed at a remote Docker host by specifying a TCP endpoint as "tcp://hostname:port". ``` - Argument: --host, -h + Argument: --host, -H Environment Variable: DOCKER_HOST Type: String Default: "unix:///var/run/docker.sock" From c8bd484b9ebf006986080b82a05c0021d5258dd2 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 3 Dec 2020 13:26:50 +0100 Subject: [PATCH 02/98] docs: add ksurl as a contributor (#697) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 266a339..67b4092 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -684,6 +684,15 @@ "contributions": [ "code" ] + }, + { + "login": "ksurl", + "name": "ksurl", + "avatar_url": "https://avatars1.githubusercontent.com/u/1371562?v=4", + "profile": "https://github.com/ksurl", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index e7d78d8..da9dfcf 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
David H.

💻
Chander Ganesan

📖
yrien30

💻 +
ksurl

📖 From cb62b16369cc048455f6da2759d5c8862f57e273 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 6 Dec 2020 13:21:04 +0100 Subject: [PATCH 03/98] add http head based digest comparison to avoid dockerhub rate limits --- .gitignore | 1 + coverage.out | 620 +++++++++++++++++++++++++ docs/arguments.md | 2 +- go.mod | 14 +- go.sum | 39 +- internal/actions/check.go | 2 +- internal/actions/mocks/container.go | 31 ++ internal/flags/flags.go | 3 +- pkg/container/client.go | 25 +- pkg/container/container.go | 5 + pkg/registry/auth/auth.go | 168 +++++++ pkg/registry/auth/auth_test.go | 98 ++++ pkg/registry/digest/digest.go | 98 ++++ pkg/registry/digest/digest_test.go | 87 ++++ pkg/registry/helpers/helpers.go | 36 ++ pkg/registry/helpers/helpers_test.go | 31 ++ pkg/registry/manifest/manifest.go | 64 +++ pkg/registry/manifest/manifest_test.go | 66 +++ pkg/registry/trust.go | 2 +- pkg/registry/trust_test.go | 102 ++-- pkg/types/container.go | 26 ++ pkg/types/registry_credentials.go | 7 + pkg/types/token_response.go | 6 + 23 files changed, 1476 insertions(+), 57 deletions(-) create mode 100644 coverage.out create mode 100644 pkg/registry/auth/auth.go create mode 100644 pkg/registry/auth/auth_test.go create mode 100644 pkg/registry/digest/digest.go create mode 100644 pkg/registry/digest/digest_test.go create mode 100644 pkg/registry/helpers/helpers.go create mode 100644 pkg/registry/helpers/helpers_test.go create mode 100644 pkg/registry/manifest/manifest.go create mode 100644 pkg/registry/manifest/manifest_test.go create mode 100644 pkg/types/container.go create mode 100644 pkg/types/registry_credentials.go create mode 100644 pkg/types/token_response.go diff --git a/.gitignore b/.gitignore index fda8d42..50b5c2d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist .idea .DS_Store /site +coverage.out diff --git a/coverage.out b/coverage.out new file mode 100644 index 0000000..40470d1 --- /dev/null +++ b/coverage.out @@ -0,0 +1,620 @@ +mode: set +github.com/containrrr/watchtower/internal/util/rand_name.go:8.24,10.19 2 0 +github.com/containrrr/watchtower/internal/util/rand_name.go:14.2,14.18 1 0 +github.com/containrrr/watchtower/internal/util/rand_name.go:10.19,12.3 1 0 +github.com/containrrr/watchtower/internal/util/util.go:4.39,5.24 1 1 +github.com/containrrr/watchtower/internal/util/util.go:9.2,9.20 1 1 +github.com/containrrr/watchtower/internal/util/util.go:15.2,15.13 1 1 +github.com/containrrr/watchtower/internal/util/util.go:5.24,7.3 1 1 +github.com/containrrr/watchtower/internal/util/util.go:9.20,10.21 1 1 +github.com/containrrr/watchtower/internal/util/util.go:10.21,12.4 1 1 +github.com/containrrr/watchtower/internal/util/util.go:19.46,22.24 2 1 +github.com/containrrr/watchtower/internal/util/util.go:37.2,37.10 1 1 +github.com/containrrr/watchtower/internal/util/util.go:22.24,25.25 2 1 +github.com/containrrr/watchtower/internal/util/util.go:32.3,32.13 1 1 +github.com/containrrr/watchtower/internal/util/util.go:25.25,26.16 1 1 +github.com/containrrr/watchtower/internal/util/util.go:26.16,28.10 2 1 +github.com/containrrr/watchtower/internal/util/util.go:32.13,34.4 1 1 +github.com/containrrr/watchtower/internal/util/util.go:41.68,44.25 2 1 +github.com/containrrr/watchtower/internal/util/util.go:54.2,54.10 1 1 +github.com/containrrr/watchtower/internal/util/util.go:44.25,45.27 1 1 +github.com/containrrr/watchtower/internal/util/util.go:45.27,46.16 1 1 +github.com/containrrr/watchtower/internal/util/util.go:46.16,48.5 1 1 +github.com/containrrr/watchtower/internal/util/util.go:49.9,51.4 1 1 +github.com/containrrr/watchtower/internal/util/util.go:58.72,61.25 2 1 +github.com/containrrr/watchtower/internal/util/util.go:67.2,67.10 1 1 +github.com/containrrr/watchtower/internal/util/util.go:61.25,62.27 1 1 +github.com/containrrr/watchtower/internal/util/util.go:62.27,64.4 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:6.63,6.90 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:9.43,9.58 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:12.66,13.21 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:17.2,17.44 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:13.21,15.3 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:17.44,18.30 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:23.3,23.15 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:18.30,19.52 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:19.52,21.5 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:28.56,29.44 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:29.44,33.10 2 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:37.3,37.23 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:33.10,35.4 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:42.58,43.44 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:43.44,45.26 2 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:50.3,50.23 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:45.26,48.4 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:55.64,56.17 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:60.2,60.44 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:56.17,58.3 1 0 +github.com/containrrr/watchtower/pkg/filters/filters.go:60.44,62.36 2 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:66.3,66.15 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:62.36,64.4 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:71.75,74.17 3 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:79.2,79.17 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:84.2,85.15 2 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:74.17,78.3 1 1 +github.com/containrrr/watchtower/pkg/filters/filters.go:79.17,83.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:20.50,25.2 4 1 +github.com/containrrr/watchtower/internal/flags/flags.go:28.50,154.2 22 0 +github.com/containrrr/watchtower/internal/flags/flags.go:157.56,299.2 24 1 +github.com/containrrr/watchtower/internal/flags/flags.go:302.20,313.2 10 1 +github.com/containrrr/watchtower/internal/flags/flags.go:317.42,325.53 6 1 +github.com/containrrr/watchtower/internal/flags/flags.go:328.2,328.55 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:331.2,331.63 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:334.2,334.57 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:337.2,337.63 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:340.2,340.67 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:343.2,343.12 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:325.53,327.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:328.55,330.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:331.63,333.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:334.57,336.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:337.63,339.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:340.67,342.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:347.70,356.57 7 0 +github.com/containrrr/watchtower/internal/flags/flags.go:359.2,359.62 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:362.2,362.66 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:365.2,365.66 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:369.2,369.49 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:356.57,358.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:359.62,361.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:362.66,364.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:365.66,367.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:372.49,373.40 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:376.2,377.16 2 1 +github.com/containrrr/watchtower/internal/flags/flags.go:380.2,380.12 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:373.40,375.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:377.16,379.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:383.48,384.9 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:387.2,387.12 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:384.9,386.3 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:392.50,401.33 3 1 +github.com/containrrr/watchtower/internal/flags/flags.go:401.33,403.3 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:407.61,409.16 2 1 +github.com/containrrr/watchtower/internal/flags/flags.go:412.2,412.34 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:409.16,411.3 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:412.34,414.17 2 1 +github.com/containrrr/watchtower/internal/flags/flags.go:417.3,418.17 2 1 +github.com/containrrr/watchtower/internal/flags/flags.go:414.17,416.4 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:418.17,420.4 1 0 +github.com/containrrr/watchtower/internal/flags/flags.go:424.28,426.24 2 1 +github.com/containrrr/watchtower/internal/flags/flags.go:429.2,429.13 1 1 +github.com/containrrr/watchtower/internal/flags/flags.go:426.24,428.3 1 1 +github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:9.60,12.16 3 1 +github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:15.2,19.28 4 1 +github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:12.16,14.3 1 0 +github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:23.57,25.16 2 1 +github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:29.2,29.67 1 1 +github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:33.2,33.16 1 1 +github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:36.2,36.22 1 1 +github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:25.16,27.3 1 0 +github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:29.67,31.3 1 1 +github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:33.16,35.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:16.46,23.16 5 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:27.2,31.16 3 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:34.2,34.26 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:53.2,53.10 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:23.16,25.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:31.16,33.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:34.26,36.12 2 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:50.3,50.32 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:37.18,38.47 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:39.18,40.47 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:41.20,42.49 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:43.19,44.48 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:45.21,46.50 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:47.11,48.49 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:57.40,58.28 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:58.28,60.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:64.39,65.28 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:65.28,67.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:71.28,72.28 1 0 +github.com/containrrr/watchtower/pkg/notifications/notifier.go:72.28,74.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:36.86,41.16 4 0 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:45.2,59.10 4 0 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:41.16,43.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:62.49,63.30 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:74.2,74.16 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:63.30,66.28 2 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:66.28,67.18 1 0 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:67.18,70.5 1 0 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:77.74,79.59 2 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:83.2,83.22 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:79.59,81.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:86.66,89.2 2 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:91.52,92.22 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:92.22,94.3 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:97.51,98.45 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:102.2,103.17 2 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:98.45,100.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:106.40,113.2 3 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:115.53,117.2 1 0 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:119.61,120.22 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:126.2,126.12 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:120.22,122.3 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:122.8,125.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:129.63,144.35 5 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:152.2,152.16 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:160.2,160.35 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:164.2,164.12 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:144.35,146.3 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:152.16,154.3 1 1 +github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:160.35,162.3 1 1 +github.com/containrrr/watchtower/pkg/notifications/slack.go:18.83,40.2 9 0 +github.com/containrrr/watchtower/pkg/notifications/slack.go:42.50,42.51 0 0 +github.com/containrrr/watchtower/pkg/notifications/slack.go:44.49,44.50 0 0 +github.com/containrrr/watchtower/pkg/notifications/slack.go:46.38,46.39 0 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:33.110,35.16 2 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:38.2,39.44 2 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:42.2,42.42 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:49.2,49.14 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:56.2,56.36 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:59.2,59.26 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:64.2,65.16 2 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:68.2,69.16 2 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:72.2,73.16 2 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:76.2,76.17 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:35.16,37.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:39.44,41.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:42.42,45.43 3 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:45.43,47.4 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:49.14,50.39 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:50.39,51.35 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:51.35,53.5 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:56.36,58.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:59.26,60.37 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:60.37,62.4 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:65.16,67.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:69.16,71.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/smtp.go:73.16,75.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/util.go:7.42,13.26 5 0 +github.com/containrrr/watchtower/pkg/notifications/util.go:23.2,23.13 1 0 +github.com/containrrr/watchtower/pkg/notifications/util.go:13.26,15.19 2 0 +github.com/containrrr/watchtower/pkg/notifications/util.go:15.19,18.4 2 0 +github.com/containrrr/watchtower/pkg/notifications/util.go:18.9,18.26 1 0 +github.com/containrrr/watchtower/pkg/notifications/util.go:18.26,20.4 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:36.83,65.2 13 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:67.71,70.24 2 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:75.2,75.48 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:78.2,79.32 2 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:84.2,96.27 11 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:100.2,102.50 2 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:106.2,106.24 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:70.24,72.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:72.8,74.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:75.48,77.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:79.32,82.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:96.27,98.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:102.50,104.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:109.63,112.12 2 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:112.12,113.18 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:117.3,118.19 2 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:121.3,122.17 2 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:113.18,115.4 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:118.19,120.4 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:122.17,125.4 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:129.49,130.22 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:130.22,132.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:135.48,136.45 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:140.2,141.17 2 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:136.45,138.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:144.50,146.2 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:148.58,149.22 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:154.2,154.12 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:149.22,151.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:151.8,153.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/email.go:157.38,157.39 0 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:27.84,31.24 3 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:39.2,40.26 2 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:44.2,55.10 4 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:31.24,33.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:33.8,33.99 1 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:33.99,35.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:35.8,35.52 1 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:35.52,37.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:40.26,42.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:58.51,58.52 0 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:60.50,60.51 0 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:62.39,62.40 0 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:64.51,66.2 1 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:68.46,70.34 2 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:73.2,73.50 1 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:70.34,72.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:76.59,78.12 1 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:110.2,110.12 1 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:78.12,84.17 2 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:90.3,99.17 4 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:103.3,105.54 2 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:84.17,87.4 2 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:99.17,102.4 2 0 +github.com/containrrr/watchtower/pkg/notifications/gotify.go:105.54,107.4 1 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:25.87,30.26 3 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:34.2,43.10 4 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:30.26,32.3 1 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:46.52,46.53 0 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:48.51,48.52 0 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:50.40,50.41 0 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:52.52,54.2 1 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:56.60,60.12 2 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:111.2,111.12 1 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:60.12,68.57 2 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:86.3,87.17 2 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:92.3,93.17 2 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:97.3,99.53 2 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:68.57,75.33 3 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:83.4,83.56 1 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:75.33,81.5 2 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:87.17,90.4 2 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:93.17,95.4 1 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:99.53,101.24 2 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:101.24,103.19 2 0 +github.com/containrrr/watchtower/pkg/notifications/msteams.go:103.19,106.6 2 0 +github.com/containrrr/watchtower/pkg/registry/registry.go:9.71,12.16 3 0 +github.com/containrrr/watchtower/pkg/registry/registry.go:16.2,16.16 1 0 +github.com/containrrr/watchtower/pkg/registry/registry.go:19.2,24.8 2 0 +github.com/containrrr/watchtower/pkg/registry/registry.go:12.16,14.3 1 0 +github.com/containrrr/watchtower/pkg/registry/registry.go:16.16,18.3 1 0 +github.com/containrrr/watchtower/pkg/registry/registry.go:30.43,33.2 2 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:20.46,22.16 2 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:25.2,25.18 1 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:22.16,24.3 1 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:31.49,34.38 3 1 +github.com/containrrr/watchtower/pkg/registry/trust.go:43.2,43.93 1 1 +github.com/containrrr/watchtower/pkg/registry/trust.go:34.38,42.3 4 1 +github.com/containrrr/watchtower/pkg/registry/trust.go:50.52,52.16 2 1 +github.com/containrrr/watchtower/pkg/registry/trust.go:56.2,57.21 2 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:60.2,61.16 2 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:65.2,68.34 3 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:72.2,74.25 3 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:52.16,55.3 2 1 +github.com/containrrr/watchtower/pkg/registry/trust.go:57.21,59.3 1 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:61.16,64.3 2 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:68.34,71.3 2 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:78.53,81.16 2 1 +github.com/containrrr/watchtower/pkg/registry/trust.go:85.2,86.22 2 1 +github.com/containrrr/watchtower/pkg/registry/trust.go:81.16,83.3 1 1 +github.com/containrrr/watchtower/pkg/registry/trust.go:91.75,92.39 1 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:95.2,95.46 1 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:92.39,94.3 1 0 +github.com/containrrr/watchtower/pkg/registry/trust.go:99.56,101.2 1 1 +github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:13.68,17.16 3 1 +github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:21.2,22.16 2 1 +github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:25.2,31.26 3 1 +github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:17.16,19.3 1 0 +github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:22.16,24.3 1 0 +github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:34.71,37.46 3 1 +github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:45.2,45.17 1 1 +github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:37.46,41.3 3 1 +github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:41.8,44.3 2 1 +github.com/containrrr/watchtower/pkg/container/container.go:16.97,21.2 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:33.57,35.2 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:38.32,40.2 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:45.37,47.2 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:50.34,52.2 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:56.37,58.2 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:63.39,66.9 2 1 +github.com/containrrr/watchtower/pkg/container/container.go:70.2,70.39 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:74.2,74.18 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:66.9,68.3 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:70.39,72.3 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:79.43,81.9 2 1 +github.com/containrrr/watchtower/pkg/container/container.go:85.2,86.16 2 1 +github.com/containrrr/watchtower/pkg/container/container.go:90.2,90.25 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:81.9,83.3 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:86.16,88.3 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:95.41,97.9 2 0 +github.com/containrrr/watchtower/pkg/container/container.go:101.2,102.16 2 0 +github.com/containrrr/watchtower/pkg/container/container.go:106.2,106.19 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:97.9,99.3 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:102.16,104.3 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:111.43,113.9 2 0 +github.com/containrrr/watchtower/pkg/container/container.go:117.2,117.24 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:113.9,115.3 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:122.37,127.31 3 1 +github.com/containrrr/watchtower/pkg/container/container.go:132.2,132.69 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:139.2,139.14 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:127.31,130.3 2 1 +github.com/containrrr/watchtower/pkg/container/container.go:132.69,133.57 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:133.57,136.4 2 1 +github.com/containrrr/watchtower/pkg/container/container.go:144.37,146.2 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:152.40,154.2 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:161.43,168.29 5 0 +github.com/containrrr/watchtower/pkg/container/container.go:172.2,172.16 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:168.29,170.3 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:178.40,180.2 1 1 +github.com/containrrr/watchtower/pkg/container/container.go:193.60,198.49 4 0 +github.com/containrrr/watchtower/pkg/container/container.go:202.2,202.37 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:206.2,206.42 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:210.2,210.64 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:217.2,224.37 4 0 +github.com/containrrr/watchtower/pkg/container/container.go:229.2,229.57 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:233.2,234.15 2 0 +github.com/containrrr/watchtower/pkg/container/container.go:198.49,200.3 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:202.37,204.3 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:206.42,208.3 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:210.64,212.51 2 0 +github.com/containrrr/watchtower/pkg/container/container.go:212.51,214.4 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:224.37,225.47 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:225.47,227.4 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:229.57,231.3 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:239.61,242.40 2 0 +github.com/containrrr/watchtower/pkg/container/container.go:249.2,249.19 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:242.40,247.3 3 0 +github.com/containrrr/watchtower/pkg/container/container.go:253.40,255.2 1 0 +github.com/containrrr/watchtower/pkg/container/container.go:257.52,259.2 1 0 +github.com/containrrr/watchtower/pkg/container/metadata.go:19.57,21.2 1 0 +github.com/containrrr/watchtower/pkg/container/metadata.go:24.58,26.2 1 0 +github.com/containrrr/watchtower/pkg/container/metadata.go:29.58,31.2 1 0 +github.com/containrrr/watchtower/pkg/container/metadata.go:34.59,36.2 1 0 +github.com/containrrr/watchtower/pkg/container/metadata.go:40.61,43.2 2 1 +github.com/containrrr/watchtower/pkg/container/metadata.go:45.62,46.57 1 1 +github.com/containrrr/watchtower/pkg/container/metadata.go:49.2,49.11 1 1 +github.com/containrrr/watchtower/pkg/container/metadata.go:46.57,48.3 1 1 +github.com/containrrr/watchtower/pkg/container/metadata.go:52.63,55.2 2 1 +github.com/containrrr/watchtower/pkg/container/client.go:43.101,46.16 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:50.2,56.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:46.16,48.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:67.77,71.27 3 1 +github.com/containrrr/watchtower/pkg/container/client.go:77.2,84.16 3 1 +github.com/containrrr/watchtower/pkg/container/client.go:88.2,88.46 1 1 +github.com/containrrr/watchtower/pkg/container/client.go:100.2,100.16 1 1 +github.com/containrrr/watchtower/pkg/container/client.go:71.27,73.3 1 1 +github.com/containrrr/watchtower/pkg/container/client.go:73.8,75.3 1 1 +github.com/containrrr/watchtower/pkg/container/client.go:84.16,86.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:88.46,91.17 2 1 +github.com/containrrr/watchtower/pkg/container/client.go:95.3,95.12 1 1 +github.com/containrrr/watchtower/pkg/container/client.go:91.17,93.4 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:95.12,97.4 1 1 +github.com/containrrr/watchtower/pkg/container/client.go:103.60,107.27 3 1 +github.com/containrrr/watchtower/pkg/container/client.go:112.2,112.19 1 1 +github.com/containrrr/watchtower/pkg/container/client.go:107.27,110.3 2 1 +github.com/containrrr/watchtower/pkg/container/client.go:115.80,119.16 3 1 +github.com/containrrr/watchtower/pkg/container/client.go:123.2,124.16 2 1 +github.com/containrrr/watchtower/pkg/container/client.go:128.2,128.77 1 1 +github.com/containrrr/watchtower/pkg/container/client.go:119.16,121.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:124.16,126.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:131.84,134.18 3 0 +github.com/containrrr/watchtower/pkg/container/client.go:138.2,138.19 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:146.2,148.43 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:159.2,159.64 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:163.2,163.12 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:134.18,136.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:138.19,140.70 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:140.70,142.4 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:148.43,150.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:150.8,153.144 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:153.144,155.4 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:159.64,161.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:166.72,173.58 5 0 +github.com/containrrr/watchtower/pkg/container/client.go:183.2,187.16 4 0 +github.com/containrrr/watchtower/pkg/container/client.go:191.2,191.40 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:209.2,209.45 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:213.2,213.78 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:173.58,175.51 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:180.3,180.65 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:175.51,178.9 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:187.16,189.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:191.40,193.54 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:200.3,200.51 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:193.54,195.18 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:195.18,197.5 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:200.51,202.18 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:202.18,204.5 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:209.45,211.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:217.131,222.16 4 0 +github.com/containrrr/watchtower/pkg/container/client.go:225.2,225.12 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:222.16,224.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:228.79,232.2 3 0 +github.com/containrrr/watchtower/pkg/container/client.go:234.80,237.24 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:243.2,243.43 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:237.24,239.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:239.8,239.64 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:239.64,241.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:246.96,251.16 4 0 +github.com/containrrr/watchtower/pkg/container/client.go:255.2,255.35 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:260.2,261.18 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:251.16,253.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:255.35,258.3 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:264.86,275.16 6 0 +github.com/containrrr/watchtower/pkg/container/client.go:280.2,281.86 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:288.2,291.16 3 0 +github.com/containrrr/watchtower/pkg/container/client.go:296.2,298.51 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:302.2,302.12 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:275.16,278.3 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:281.86,283.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:283.8,283.18 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:283.18,286.3 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:291.16,294.3 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:298.51,301.3 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:305.61,316.2 3 0 +github.com/containrrr/watchtower/pkg/container/client.go:318.98,329.16 4 0 +github.com/containrrr/watchtower/pkg/container/client.go:333.2,337.22 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:342.2,344.16 3 0 +github.com/containrrr/watchtower/pkg/container/client.go:348.2,349.22 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:362.2,363.16 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:367.2,367.12 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:329.16,331.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:337.22,339.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:344.16,346.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:349.22,353.17 4 0 +github.com/containrrr/watchtower/pkg/container/client.go:353.17,355.4 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:355.9,355.25 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:355.25,357.4 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:363.16,365.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:370.118,374.17 3 0 +github.com/containrrr/watchtower/pkg/container/client.go:381.2,381.6 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:406.2,406.12 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:374.17,377.3 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:377.8,379.3 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:381.6,390.17 3 0 +github.com/containrrr/watchtower/pkg/container/client.go:393.3,393.34 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:397.3,397.26 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:400.3,400.31 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:404.3,404.8 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:390.17,392.4 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:393.34,395.12 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:397.26,399.4 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:400.31,403.4 2 0 +github.com/containrrr/watchtower/pkg/container/client.go:409.92,413.6 3 0 +github.com/containrrr/watchtower/pkg/container/client.go:413.6,414.10 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:424.3,424.30 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:415.18,416.14 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:417.11,418.70 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:418.70,420.5 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:420.10,420.32 1 0 +github.com/containrrr/watchtower/pkg/container/client.go:420.32,422.5 1 0 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:24.121,30.49 5 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:34.2,35.53 2 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:39.2,41.43 3 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:45.2,52.43 4 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:55.2,55.44 1 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:60.2,60.67 1 0 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:30.49,32.3 1 0 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:35.53,37.3 1 0 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:41.43,43.3 1 0 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:52.43,54.3 1 0 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:55.44,58.3 2 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:64.63,67.16 2 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:70.2,72.17 3 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:67.16,69.3 1 0 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:76.139,81.16 4 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:85.2,86.72 2 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:90.2,90.62 1 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:97.2,98.50 2 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:102.2,106.16 4 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:110.2,110.33 1 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:81.16,83.3 1 0 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:86.72,88.3 1 0 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:90.62,93.3 2 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:93.8,95.3 1 0 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:98.50,100.3 1 0 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:106.16,108.3 1 0 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:114.66,120.29 5 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:128.2,128.79 1 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:132.2,140.21 8 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:120.29,126.3 5 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:128.79,130.3 1 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:144.52,147.16 3 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:151.2,156.17 2 1 +github.com/containrrr/watchtower/pkg/registry/auth/auth.go:147.16,149.3 1 0 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:24.124,28.16 4 1 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:32.2,33.16 2 1 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:37.2,37.64 1 1 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:41.2,44.24 3 1 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:48.2,48.40 1 1 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:56.2,56.19 1 0 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:28.16,30.3 1 0 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:33.16,35.3 1 0 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:37.64,39.3 1 0 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:44.24,46.3 1 0 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:48.40,51.28 3 1 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:51.28,53.4 1 1 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:60.79,63.17 3 1 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:69.2,75.16 6 1 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:78.2,78.27 1 1 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:81.2,81.49 1 1 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:63.17,65.3 1 1 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:65.8,67.3 1 0 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:75.16,77.3 1 0 +github.com/containrrr/watchtower/pkg/registry/digest/digest.go:78.27,80.3 1 0 +github.com/containrrr/watchtower/internal/actions/check.go:24.101,28.16 3 1 +github.com/containrrr/watchtower/internal/actions/check.go:33.2,33.26 1 1 +github.com/containrrr/watchtower/internal/actions/check.go:38.2,39.62 2 1 +github.com/containrrr/watchtower/internal/actions/check.go:28.16,31.3 2 0 +github.com/containrrr/watchtower/internal/actions/check.go:33.26,36.3 2 1 +github.com/containrrr/watchtower/internal/actions/check.go:42.110,49.44 5 1 +github.com/containrrr/watchtower/internal/actions/check.go:66.2,66.64 1 1 +github.com/containrrr/watchtower/internal/actions/check.go:49.44,50.65 1 1 +github.com/containrrr/watchtower/internal/actions/check.go:57.3,57.14 1 1 +github.com/containrrr/watchtower/internal/actions/check.go:50.65,54.12 3 0 +github.com/containrrr/watchtower/internal/actions/check.go:57.14,58.62 1 1 +github.com/containrrr/watchtower/internal/actions/check.go:58.62,62.5 2 0 +github.com/containrrr/watchtower/internal/actions/check.go:69.55,70.22 1 1 +github.com/containrrr/watchtower/internal/actions/check.go:74.2,76.11 2 0 +github.com/containrrr/watchtower/internal/actions/check.go:79.2,79.11 1 0 +github.com/containrrr/watchtower/internal/actions/check.go:82.2,82.36 1 0 +github.com/containrrr/watchtower/internal/actions/check.go:70.22,72.3 1 1 +github.com/containrrr/watchtower/internal/actions/check.go:76.11,78.3 1 0 +github.com/containrrr/watchtower/internal/actions/check.go:79.11,81.3 1 0 +github.com/containrrr/watchtower/internal/actions/check.go:85.26,88.2 2 1 +github.com/containrrr/watchtower/internal/actions/update.go:17.71,20.27 2 1 +github.com/containrrr/watchtower/internal/actions/update.go:24.2,25.16 2 1 +github.com/containrrr/watchtower/internal/actions/update.go:29.2,29.45 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:41.2,42.16 2 1 +github.com/containrrr/watchtower/internal/actions/update.go:46.2,49.25 3 1 +github.com/containrrr/watchtower/internal/actions/update.go:57.2,57.27 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:63.2,63.27 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:66.2,66.12 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:20.27,22.3 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:25.16,27.3 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:29.45,31.127 2 1 +github.com/containrrr/watchtower/internal/actions/update.go:34.3,34.17 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:38.3,38.30 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:31.127,33.4 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:34.17,37.4 2 0 +github.com/containrrr/watchtower/internal/actions/update.go:42.16,44.3 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:49.25,50.45 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:50.45,51.38 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:51.38,53.5 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:57.27,59.3 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:59.8,62.3 2 1 +github.com/containrrr/watchtower/internal/actions/update.go:63.27,65.3 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:69.114,72.44 2 0 +github.com/containrrr/watchtower/internal/actions/update.go:79.2,79.20 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:72.44,73.26 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:73.26,76.4 2 0 +github.com/containrrr/watchtower/internal/actions/update.go:79.20,81.3 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:84.122,85.44 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:85.44,87.3 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:90.108,91.30 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:96.2,96.22 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:99.2,99.27 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:107.2,107.72 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:91.30,94.3 2 0 +github.com/containrrr/watchtower/internal/actions/update.go:96.22,98.3 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:99.27,100.78 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:100.78,104.4 3 0 +github.com/containrrr/watchtower/internal/actions/update.go:107.72,109.3 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:112.123,115.44 2 1 +github.com/containrrr/watchtower/internal/actions/update.go:123.2,123.20 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:115.44,116.28 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:119.3,120.44 2 1 +github.com/containrrr/watchtower/internal/actions/update.go:116.28,117.12 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:123.20,125.3 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:128.71,129.32 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:129.32,130.57 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:130.57,132.4 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:136.111,141.30 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:148.2,148.23 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:141.30,142.76 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:142.76,145.4 2 0 +github.com/containrrr/watchtower/internal/actions/update.go:148.23,149.74 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:149.74,151.4 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:151.9,151.54 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:151.54,153.4 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:157.58,159.36 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:159.36,160.25 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:164.2,165.43 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:160.25,161.12 1 1 +github.com/containrrr/watchtower/internal/actions/update.go:165.43,166.37 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:166.37,167.54 1 0 +github.com/containrrr/watchtower/internal/actions/update.go:167.54,169.20 2 0 diff --git a/docs/arguments.md b/docs/arguments.md index 64bbdfc..62e73ad 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -145,7 +145,7 @@ Poll interval (in seconds). This value controls how frequently watchtower will p Argument: --interval, -i Environment Variable: WATCHTOWER_POLL_INTERVAL Type: Integer - Default: 300 + Default: 86400 (24 hours) ``` ## Filter by enable label diff --git a/go.mod b/go.mod index 300eb31..0e37602 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/docker/go-units v0.3.3 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/gofrs/uuid v3.2.0+incompatible // indirect + github.com/golang/protobuf v1.4.2 // indirect github.com/google/certificate-transparency-go v1.0.21 // indirect github.com/gorilla/mux v1.7.0 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect @@ -39,11 +40,11 @@ require ( github.com/lib/pq v1.2.0 // indirect github.com/miekg/pkcs11 v0.0.0-20190401114359-553cfdd26aaa // indirect github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect - github.com/onsi/ginkgo v1.8.0 - github.com/onsi/gomega v1.5.0 + github.com/onsi/ginkgo v1.11.0 + github.com/onsi/gomega v1.10.0 github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect - github.com/opencontainers/runc v0.1.1 + github.com/opencontainers/runc v0.1.1 // indirect github.com/pkg/errors v0.8.1 // indirect github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 github.com/sirupsen/logrus v1.4.1 @@ -53,9 +54,14 @@ require ( github.com/stretchr/testify v1.3.0 github.com/theupdateframework/notary v0.6.1 // indirect github.com/zmap/zlint v1.0.2 // indirect - golang.org/x/net v0.0.0-20190522155817-f3200d17e092 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect + golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 + golang.org/x/sys v0.0.0-20190830141801-acfa387b8d69 // indirect + golang.org/x/text v0.3.4 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect gopkg.in/fatih/pool.v2 v2.0.0 // indirect gopkg.in/gorethink/gorethink.v3 v3.0.5 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gotest.tools v2.2.0+incompatible // indirect ) diff --git a/go.sum b/go.sum index 7285415..d1d77ae 100644 --- a/go.sum +++ b/go.sum @@ -63,7 +63,6 @@ github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BU github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v0.0.0-20190404075923-dbe4a30928d4 h1:34LfsqlE2kEvmGP9qbRoPvOWkmluYGzmlvWVTzwvT0A= github.com/docker/docker v0.0.0-20190404075923-dbe4a30928d4/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo= 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 v1.5.1-1 h1:hr4w35acWBPhGBXlzPoHpmZ/ygPjnmFVxGxxGnMyP7k= @@ -106,6 +105,13 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +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.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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= @@ -115,6 +121,9 @@ github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 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 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -191,9 +200,13 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.10.0 h1:Gwkk+PTu/nfOwNMtUB/mRUv0X7ewW5dO4AERT1ThVKo= +github.com/onsi/gomega v1.10.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -280,6 +293,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 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= @@ -297,7 +312,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20191116160921-f9c825593386 h1:ktbWvQrW08Txdxno1PiDpSxPXG6ndGsfnJjRRtkM0LQ= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -314,24 +330,31 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190830141801-acfa387b8d69 h1:Wdn4Yb8d5VrsO3jWgaeSZss09x1VLVBMePDh4VW/xSQ= +golang.org/x/sys v0.0.0-20190830141801-acfa387b8d69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= @@ -344,6 +367,13 @@ google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +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.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -364,6 +394,9 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gosrc.io/xmpp v0.1.1 h1:iMtE9W3fx254+4E6rI34AOPJDqWvpfQR6EYaVMzhJ4s= gosrc.io/xmpp v0.1.1/go.mod h1:4JgaXzw4MnEv2sGltONtK3GMhj+h9gpQ7cO8nwbFJLU= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= diff --git a/internal/actions/check.go b/internal/actions/check.go index 56a9fc4..aeff0cd 100644 --- a/internal/actions/check.go +++ b/internal/actions/check.go @@ -10,7 +10,7 @@ import ( "github.com/containrrr/watchtower/pkg/filters" "github.com/containrrr/watchtower/pkg/sorter" - "github.com/opencontainers/runc/Godeps/_workspace/src/github.com/Sirupsen/logrus" + "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus" diff --git a/internal/actions/mocks/container.go b/internal/actions/mocks/container.go index e92ee1c..1db8652 100644 --- a/internal/actions/mocks/container.go +++ b/internal/actions/mocks/container.go @@ -17,6 +17,7 @@ func CreateMockContainer(id string, name string, image string, created time.Time Created: created.String(), }, Config: &container2.Config{ + Image: image, Labels: make(map[string]string), }, } @@ -24,10 +25,40 @@ func CreateMockContainer(id string, name string, image string, created time.Time &content, &types.ImageInspect{ ID: image, + RepoDigests: []string{ + image, + }, }, ) } +// CreateMockContainerWithImageInfo should only be used for testing +func CreateMockContainerWithImageInfo(id string, name string, image string, created time.Time, imageInfo types.ImageInspect) container.Container { + content := types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + ID: id, + Image: image, + Name: name, + Created: created.String(), + }, + Config: &container2.Config{ + Image: image, + Labels: make(map[string]string), + }, + } + 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 { + 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, created time.Time, config *container2.Config) container.Container { content := types.ContainerJSON{ diff --git a/internal/flags/flags.go b/internal/flags/flags.go index c7c98b1..163eefc 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -300,10 +300,11 @@ Should only be used for testing.`) // SetDefaults provides default values for environment variables func SetDefaults() { + day := time.Hour * 24 / time.Second viper.AutomaticEnv() viper.SetDefault("DOCKER_HOST", "unix:///var/run/docker.sock") viper.SetDefault("DOCKER_API_VERSION", DockerAPIMinVersion) - viper.SetDefault("WATCHTOWER_POLL_INTERVAL", 300) + viper.SetDefault("WATCHTOWER_POLL_INTERVAL", day) viper.SetDefault("WATCHTOWER_TIMEOUT", time.Second*10) viper.SetDefault("WATCHTOWER_NOTIFICATIONS", []string{}) viper.SetDefault("WATCHTOWER_NOTIFICATIONS_LEVEL", "info") diff --git a/pkg/container/client.go b/pkg/container/client.go index a333ea5..ce694d0 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -8,6 +8,7 @@ import ( "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" @@ -275,14 +276,36 @@ func (client dockerClient) HasNewImage(ctx context.Context, container Container) func (client dockerClient) PullImage(ctx context.Context, container Container) error { containerName := container.Name() imageName := container.ImageName() - log.Debugf("Pulling %s for %s", imageName, containerName) + fields := log.Fields{ + "image": imageName, + "container": containerName, + } + + log.WithFields(fields).Debugf("Trying to load authentication credentials.") opts, err := registry.GetPullOptions(imageName) + if opts.RegistryAuth != "" { + log.Debug("Credentials loaded") + } if err != nil { log.Debugf("Error loading authentication credentials %s", err) return err } + log.WithFields(fields).Debugf("Checking if pull is needed") + + if match, err := digest.CompareDigest(container, opts.RegistryAuth); err != nil { + log.Info("Could not do a head request, falling back to regulara pull.") + log.Debugf("Reason: %s", err.Error()) + } else if match { + log.Debug("No pull needed. Skipping image.") + return nil + } else { + log.Debug("Digests did not match, doing a pull.") + } + + log.WithFields(fields).Debugf("Pulling image") + response, err := client.api.ImagePull(ctx, imageName, opts) if err != nil { log.Debugf("Error pulling image %s, %s", imageName, err) diff --git a/pkg/container/container.go b/pkg/container/container.go index 9e339c3..8a9d39e 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -253,3 +253,8 @@ func (c Container) hostConfig() *dockercontainer.HostConfig { func (c Container) HasImageInfo() bool { return c.imageInfo != nil } + +// ImageInfo fetches the ImageInspect data of the current container +func (c Container) ImageInfo() *types.ImageInspect { + return c.imageInfo +} diff --git a/pkg/registry/auth/auth.go b/pkg/registry/auth/auth.go new file mode 100644 index 0000000..56f64a2 --- /dev/null +++ b/pkg/registry/auth/auth.go @@ -0,0 +1,168 @@ +package auth + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/containrrr/watchtower/pkg/registry/helpers" + "github.com/containrrr/watchtower/pkg/types" + "github.com/docker/distribution/reference" + "github.com/sirupsen/logrus" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +// ChallengeHeader is the HTTP Header containing challenge instructions +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 { + return "", err + } + logrus.WithField("URL", URL.String()).Debug("Building challenge URL") + + var req *http.Request + if req, err = GetChallengeRequest(URL); err != nil { + return "", err + } + + client := &http.Client{} + var res *http.Response + if res, err = client.Do(req); err != nil { + return "", err + } + + v := res.Header.Get(ChallengeHeader) + + logrus.WithFields(logrus.Fields{ + "status": res.Status, + "header": v, + }).Debug("Got response to challenge request") + + challenge := strings.ToLower(v) + if strings.HasPrefix(challenge, "basic") { + if registryAuth == "" { + return "", fmt.Errorf("no credentials available") + } + + return fmt.Sprintf("Basic %s", registryAuth), nil + } + if strings.HasPrefix(challenge, "bearer") { + return GetBearerHeader(challenge, container.ImageName(), err, registryAuth) + } + + return "", errors.New("unsupported challenge type from registry") +} + +// GetChallengeRequest creates a request for getting challenge instructions +func GetChallengeRequest(URL url.URL) (*http.Request, error) { + req, err := http.NewRequest("GET", URL.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "*/*") + req.Header.Set("User-Agent", "Watchtower (Docker)") + return req, nil +} + +// GetBearerHeader tries to fetch a bearer token from the registry based on the challenge instructions +func GetBearerHeader(challenge string, img string, err error, registryAuth string) (string, error) { + client := http.Client{} + if strings.Contains(img, ":") { + img = strings.Split(img, ":")[0] + } + authURL, err := GetAuthURL(challenge, img) + + if err != nil { + return "", err + } + + var r *http.Request + if r, err = http.NewRequest("GET", authURL.String(), nil); err != nil { + return "", err + } + + if registryAuth != "" { + logrus.WithField("credentials", registryAuth).Debug("Credentials found.") + r.Header.Add("Authorization", fmt.Sprintf("Basic %s", registryAuth)) + } else { + logrus.Debug("No credentials found.") + } + + var authResponse *http.Response + if authResponse, err = client.Do(r); err != nil { + return "", err + } + + body, _ := ioutil.ReadAll(authResponse.Body) + tokenResponse := &types.TokenResponse{} + + err = json.Unmarshal(body, tokenResponse) + if err != nil { + return "", err + } + + return fmt.Sprintf("Bearer %s", tokenResponse.Token), nil +} + +// GetAuthURL from the instructions in the challenge +func GetAuthURL(challenge string, img string) (*url.URL, error) { + loweredChallenge := strings.ToLower(challenge) + raw := strings.TrimPrefix(loweredChallenge, "bearer") + + pairs := strings.Split(raw, ",") + values := make(map[string]string, len(pairs)) + + for _, pair := range pairs { + trimmed := strings.Trim(pair, " ") + kv := strings.Split(trimmed, "=") + key := kv[0] + val := strings.Trim(kv[1], "\"") + values[key] = val + } + logrus.WithFields(logrus.Fields{ + "realm": values["realm"], + "service": values["service"], + }).Debug("Checking challenge header content") + if values["realm"] == "" || values["service"] == "" { + + return nil, fmt.Errorf("challenge header did not include all values needed to construct an auth url") + } + + authURL, _ := url.Parse(fmt.Sprintf("%s", values["realm"])) + q := authURL.Query() + q.Add("service", values["service"]) + scopeImage := strings.TrimPrefix(img, values["service"]) + if !strings.Contains(scopeImage, "/") { + scopeImage = "library/" + scopeImage + } + scope := fmt.Sprintf("repository:%s:pull", scopeImage) + logrus.WithFields(logrus.Fields{"scope": scope, "image": img}).Debug("Setting scope for auth token") + q.Add("scope", scope) + + authURL.RawQuery = q.Encode() + return authURL, nil +} + +// 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 + } + + URL := url.URL{ + Scheme: "https", + Host: host, + Path: "/v2/", + } + return URL, nil +} diff --git a/pkg/registry/auth/auth_test.go b/pkg/registry/auth/auth_test.go new file mode 100644 index 0000000..16a6478 --- /dev/null +++ b/pkg/registry/auth/auth_test.go @@ -0,0 +1,98 @@ +package auth_test + +import ( + "fmt" + "github.com/containrrr/watchtower/internal/actions/mocks" + "github.com/containrrr/watchtower/pkg/registry/auth" + "net/url" + "os" + "testing" + "time" + + wtTypes "github.com/containrrr/watchtower/pkg/types" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestAuth(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Registry Auth Suite") +} +func SkipIfCredentialsEmpty(credentials *wtTypes.RegistryCredentials, fn func()) func() { + if credentials.Username == "" { + return func() { + Skip("Username missing. Skipping integration test") + } + } else if credentials.Password == "" { + return func() { + Skip("Password missing. Skipping integration test") + } + } else { + return fn + } +} + +var GHCRCredentials = &wtTypes.RegistryCredentials{ + Username: os.Getenv("CI_INTEGRATION_TEST_REGISTRY_GH_USERNAME"), + Password: os.Getenv("CI_INTEGRATION_TEST_REGISTRY_GH_PASSWORD"), +} + +var _ = Describe("the auth module", func() { + mockId := "mock-id" + mockName := "mock-container" + mockImage := "ghcr.io/k6io/operator:latest" + mockCreated := time.Now() + mockDigest := "ghcr.io/k6io/operator@sha256:d68e1e532088964195ad3a0a71526bc2f11a78de0def85629beb75e2265f0547" + + mockContainer := mocks.CreateMockContainerWithDigest( + mockId, + mockName, + mockImage, + mockCreated, + mockDigest) + + When("getting an auth url", func() { + It("should parse the token from the response", + SkipIfCredentialsEmpty(GHCRCredentials, func() { + creds := fmt.Sprintf("%s:%s", GHCRCredentials.Username, GHCRCredentials.Password) + token, err := auth.GetToken(mockContainer, creds) + Expect(err).NotTo(HaveOccurred()) + Expect(token).NotTo(Equal("")) + }), + ) + + 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"` + 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") + Expect(err).NotTo(HaveOccurred()) + Expect(res).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("getting a challenge url", 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)) + }) + It("should assume dockerhub if the image ref is not fully qualified", func() { + expected := url.URL{Host: "index.docker.io", Scheme: "https", Path: "/v2/"} + Expect(auth.GetChallengeURL("containrrr/watchtower:latest")).To(Equal(expected)) + }) + It("should convert legacy dockerhub hostnames to index.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)) + }) + }) +}) diff --git a/pkg/registry/digest/digest.go b/pkg/registry/digest/digest.go new file mode 100644 index 0000000..aae8dfb --- /dev/null +++ b/pkg/registry/digest/digest.go @@ -0,0 +1,98 @@ +package digest + +import ( + "crypto/tls" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "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/http" + "strings" +) + +// ContentDigestHeader is the key for the key-value pair containing the digest header +const ContentDigestHeader = "Docker-Content-Digest" + +// CompareDigest ... +func CompareDigest(container types.Container, registryAuth string) (bool, error) { + var digest string + + registryAuth = TransformAuth(registryAuth) + token, err := auth.GetToken(container, registryAuth) + if err != nil { + return false, err + } + + digestURL, err := manifest.BuildManifestURL(container) + if err != nil { + return false, err + } + + if digest, err = GetDigest(digestURL, token); err != nil { + return false, err + } + + logrus.WithField("remote", digest).Debug("Found a remote digest to compare with") + + for _, dig := range container.ImageInfo().RepoDigests { + localDigest := strings.Split(dig, "@")[1] + fields := logrus.Fields{"local": localDigest, "remote": digest} + logrus.WithFields(fields).Debug("Comparing") + + if localDigest == digest { + logrus.Debug("Found a match") + return true, nil + } + } + + return false, nil +} + +// TransformAuth from a base64 encoded json object to base64 encoded string +func TransformAuth(registryAuth string) string { + b, _ := base64.StdEncoding.DecodeString(registryAuth) + credentials := &types.RegistryCredentials{} + _ = json.Unmarshal(b, credentials) + + if credentials.Username != "" && credentials.Password != "" { + ba := []byte(fmt.Sprintf("%s:%s", credentials.Username, credentials.Password)) + registryAuth = base64.StdEncoding.EncodeToString(ba) + } + + return registryAuth +} + +// GetDigest from registry using a HEAD request to prevent rate limiting +func GetDigest(url string, token string) (string, error) { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + client := &http.Client{Transport: tr} + + if token != "" { + logrus.WithField("token", token).Trace("Setting request token") + } else { + return "", errors.New("could not fetch token") + } + + req, _ := http.NewRequest("HEAD", url, nil) + 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") + + logrus.WithField("url", url).Debug("Doing a HEAD request to fetch a digest") + + res, err := client.Do(req) + if err != nil { + return "", err + } + if res.StatusCode != 200 { + return "", fmt.Errorf("registry responded to head request with %d", res.StatusCode) + } + return res.Header.Get(ContentDigestHeader), nil +} diff --git a/pkg/registry/digest/digest_test.go b/pkg/registry/digest/digest_test.go new file mode 100644 index 0000000..0de6025 --- /dev/null +++ b/pkg/registry/digest/digest_test.go @@ -0,0 +1,87 @@ +package digest_test + +import ( + "fmt" + "github.com/containrrr/watchtower/internal/actions/mocks" + "github.com/containrrr/watchtower/pkg/registry/digest" + wtTypes "github.com/containrrr/watchtower/pkg/types" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "os" + "testing" + "time" +) + +func TestDigest(t *testing.T) { + + RegisterFailHandler(Fail) + RunSpecs(GinkgoT(), "Digest Suite") +} + +var DockerHubCredentials = &wtTypes.RegistryCredentials{ + Username: os.Getenv("CI_INTEGRATION_TEST_REGISTRY_DH_USERNAME"), + Password: os.Getenv("CI_INTEGRATION_TEST_REGISTRY_DH_PASSWORD"), +} +var GHCRCredentials = &wtTypes.RegistryCredentials{ + Username: os.Getenv("CI_INTEGRATION_TEST_REGISTRY_GH_USERNAME"), + Password: os.Getenv("CI_INTEGRATION_TEST_REGISTRY_GH_PASSWORD"), +} + +func SkipIfCredentialsEmpty(credentials *wtTypes.RegistryCredentials, fn func()) func() { + if credentials.Username == "" { + return func() { + Skip("Username missing. Skipping integration test") + } + } else if credentials.Password == "" { + return func() { + Skip("Password missing. Skipping integration test") + } + } else { + return fn + } +} + +var _ = Describe("Digests", func() { + mockId := "mock-id" + mockName := "mock-container" + mockImage := "ghcr.io/k6io/operator:latest" + mockCreated := time.Now() + mockDigest := "ghcr.io/k6io/operator@sha256:d68e1e532088964195ad3a0a71526bc2f11a78de0def85629beb75e2265f0547" + + mockContainer := mocks.CreateMockContainerWithDigest( + mockId, + mockName, + mockImage, + mockCreated, + mockDigest) + + When("a digest comparison is done", func() { + It("should return true if digests match", + SkipIfCredentialsEmpty(GHCRCredentials, func() { + creds := fmt.Sprintf("%s:%s", GHCRCredentials.Username, GHCRCredentials.Password) + matches, err := digest.CompareDigest(mockContainer, creds) + Expect(err).NotTo(HaveOccurred()) + Expect(matches).To(Equal(true)) + }), + ) + + It("should return false if digests differ", func() { + + }) + It("should return an error if the registry isn't available", func() { + + }) + }) + When("using different registries", func() { + It("should work with DockerHub", + SkipIfCredentialsEmpty(DockerHubCredentials, func() { + fmt.Println(DockerHubCredentials != nil) // to avoid crying linters + }), + ) + It("should work with GitHub Container Registry", + SkipIfCredentialsEmpty(GHCRCredentials, func() { + fmt.Println(GHCRCredentials != nil) // to avoid crying linters + }), + ) + }) +}) diff --git a/pkg/registry/helpers/helpers.go b/pkg/registry/helpers/helpers.go new file mode 100644 index 0000000..1469331 --- /dev/null +++ b/pkg/registry/helpers/helpers.go @@ -0,0 +1,36 @@ +package helpers + +import ( + "fmt" + url2 "net/url" +) + +// 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() + + return hostName, port, err +} + +// NormalizeRegistry makes sure variations of DockerHubs registry +func NormalizeRegistry(registry string) (string, error) { + hostName, port, err := ConvertToHostname(registry) + if err != nil { + return "", err + } + + if hostName == "registry-1.docker.io" || hostName == "docker.io" { + hostName = "index.docker.io" + } + + if port != "" { + return fmt.Sprintf("%s:%s", hostName, port), nil + } + return hostName, nil +} diff --git a/pkg/registry/helpers/helpers_test.go b/pkg/registry/helpers/helpers_test.go new file mode 100644 index 0000000..92e9116 --- /dev/null +++ b/pkg/registry/helpers/helpers_test.go @@ -0,0 +1,31 @@ +package helpers + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "testing" +) + +func TestHelpers(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Helper Suite") +} + +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()) + }) + }) + 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")) + }) + }) +}) diff --git a/pkg/registry/manifest/manifest.go b/pkg/registry/manifest/manifest.go new file mode 100644 index 0000000..837bc3f --- /dev/null +++ b/pkg/registry/manifest/manifest.go @@ -0,0 +1,64 @@ +package manifest + +import ( + "fmt" + "github.com/containrrr/watchtower/pkg/registry/helpers" + "github.com/containrrr/watchtower/pkg/types" + ref "github.com/docker/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()) + if err != nil { + return "", err + } + + host, err := helpers.NormalizeRegistry(normalizedName.String()) + img, tag := extractImageAndTag(strings.TrimPrefix(container.ImageName(), host+"/")) + + logrus.WithFields(logrus.Fields{ + "image": img, + "tag": tag, + "normalized": normalizedName, + "host": host, + }).Debug("Parsing image ref") + + if err != nil { + return "", err + } + img = strings.TrimPrefix(img, fmt.Sprintf("%s/", host)) + if !strings.Contains(img, "/") { + img = "library/" + img + } + url := url2.URL{ + Scheme: "https", + Host: host, + Path: fmt.Sprintf("/v2/%s/manifests/%s", img, tag), + } + return url.String(), nil +} + +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 = fmt.Sprintf("%s%s", parts[0], parts[1]) + tag = parts[3] + } 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 new file mode 100644 index 0000000..3b86f90 --- /dev/null +++ b/pkg/registry/manifest/manifest_test.go @@ -0,0 +1,66 @@ +package manifest_test + +import ( + "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) { + RegisterFailHandler(Fail) + RunSpecs(t, "Manifest Suite") +} + +var _ = Describe("the manifest module", func() { + mockId := "mock-id" + mockName := "mock-container" + mockCreated := time.Now() + + When("building a manifest url", 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) + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(expected)) + }) + It("should assume dockerhub for non-qualified images", func() { + 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) + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(expected)) + }) + It("should assume latest for images that lack an explicit tag", func() { + 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) + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(Equal(expected)) + }) + }) + +}) diff --git a/pkg/registry/trust.go b/pkg/registry/trust.go index 937d2c3..c2bf7da 100644 --- a/pkg/registry/trust.go +++ b/pkg/registry/trust.go @@ -66,7 +66,7 @@ func EncodedConfigAuth(ref string) (string, error) { auth, _ := credStore.Get(server) // returns (types.AuthConfig{}) if server not in credStore if auth == (types.AuthConfig{}) { - log.Debugf("No credentials for %s in %s", server, configFile.Filename) + 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) diff --git a/pkg/registry/trust_test.go b/pkg/registry/trust_test.go index 8ffe1b9..7d4d48d 100644 --- a/pkg/registry/trust_test.go +++ b/pkg/registry/trust_test.go @@ -1,59 +1,71 @@ package registry import ( - "github.com/stretchr/testify/assert" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" "os" "testing" ) -func TestEncodedEnvAuth_ShouldReturnAnErrorIfRepoEnvsAreUnset(t *testing.T) { - os.Unsetenv("REPO_USER") - os.Unsetenv("REPO_PASS") - _, err := EncodedEnvAuth("") - assert.Error(t, err) -} -func TestEncodedEnvAuth_ShouldReturnAuthHashIfRepoEnvsAreSet(t *testing.T) { - expectedHash := "eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0=" - - os.Setenv("REPO_USER", "containrrr-user") - os.Setenv("REPO_PASS", "containrrr-pass") - config, _ := EncodedEnvAuth("") - - assert.Equal(t, config, expectedHash) +func TestTrust(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Trust Suite") } -func TestEncodedConfigAuth_ShouldReturnAnErrorIfFileIsNotPresent(t *testing.T) { - os.Setenv("DOCKER_CONFIG", "/dev/null/should-fail") - _, err := EncodedConfigAuth("") - assert.Error(t, err) -} +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") -/* - * 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 - */ + _, err := EncodedEnvAuth("") + Expect(err).To(HaveOccurred()) + }) + It("encoded env auth_ should return auth hash if repo envs are set", func() { + var err error + expectedHash := "eyJ1c2VybmFtZSI6ImNvbnRhaW5ycnItdXNlciIsInBhc3N3b3JkIjoiY29udGFpbnJyci1wYXNzIn0=" -func TestParseServerAddress_ShouldReturnErrorIfPassedEmptyString(t *testing.T) { - _, err := ParseServerAddress("") - assert.Error(t, err) -} + err = os.Setenv("REPO_USER", "containrrr-user") + Expect(err).NotTo(HaveOccurred()) -func TestParseServerAddress_ShouldReturnTheRepoNameIfPassedAFullyQualifiedImageName(t *testing.T) { - val, _ := ParseServerAddress("github.com/containrrrr/config") - assert.Equal(t, val, "github.com") -} + err = os.Setenv("REPO_PASS", "containrrr-pass") + Expect(err).NotTo(HaveOccurred()) -func TestParseServerAddress_ShouldReturnTheOrganizationPartIfPassedAnImageNameMissingServerName(t *testing.T) { - val, _ := ParseServerAddress("containrrr/config") - assert.Equal(t, val, "containrrr") -} + config, err := EncodedEnvAuth("") + Expect(config).To(Equal(expectedHash)) + Expect(err).NotTo(HaveOccurred()) + }) + It("encoded config auth_ should return an error if file is not present", func() { + var err error -func TestParseServerAddress_ShouldReturnTheServerNameIfPassedAFullyQualifiedImageName(t *testing.T) { - val, _ := ParseServerAddress("github.com/containrrrr/config") - assert.Equal(t, val, "github.com") -} + err = os.Setenv("DOCKER_CONFIG", "/dev/null/should-fail") + Expect(err).NotTo(HaveOccurred()) + + _, err = EncodedConfigAuth("") + Expect(err).To(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")) + }) +}) diff --git a/pkg/types/container.go b/pkg/types/container.go new file mode 100644 index 0000000..50baac6 --- /dev/null +++ b/pkg/types/container.go @@ -0,0 +1,26 @@ +package types + +import "github.com/docker/docker/api/types" + +// Container is a docker container running an image +type Container interface { + ContainerInfo() *types.ContainerJSON + ID() string + IsRunning() bool + Name() string + ImageID() string + ImageName() string + Enabled() (bool, bool) + IsMonitorOnly() bool + Scope() (string, bool) + Links() []string + ToRestart() bool + IsWatchtower() bool + StopSignal() string + HasImageInfo() bool + ImageInfo() *types.ImageInspect + GetLifecyclePreCheckCommand() string + GetLifecyclePostCheckCommand() string + GetLifecyclePreUpdateCommand() string + GetLifecyclePostUpdateCommand() string +} diff --git a/pkg/types/registry_credentials.go b/pkg/types/registry_credentials.go new file mode 100644 index 0000000..607fa05 --- /dev/null +++ b/pkg/types/registry_credentials.go @@ -0,0 +1,7 @@ +package types + +// RegistryCredentials is a credential pair used for basic auth +type RegistryCredentials struct { + Username string + Password string // usually a token rather than an actual password +} diff --git a/pkg/types/token_response.go b/pkg/types/token_response.go new file mode 100644 index 0000000..722dde8 --- /dev/null +++ b/pkg/types/token_response.go @@ -0,0 +1,6 @@ +package types + +// TokenResponse is returned by the registry on successful authentication +type TokenResponse struct { + Token string `json:"token"` +} From f697870ed5c7ea71f9aa19499f80b8e65c3c4869 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Mon, 7 Dec 2020 20:01:26 +0100 Subject: [PATCH 04/98] fix erroneous poll interval change --- internal/flags/flags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 163eefc..356c16f 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -304,7 +304,7 @@ func SetDefaults() { viper.AutomaticEnv() viper.SetDefault("DOCKER_HOST", "unix:///var/run/docker.sock") viper.SetDefault("DOCKER_API_VERSION", DockerAPIMinVersion) - viper.SetDefault("WATCHTOWER_POLL_INTERVAL", day) + viper.SetDefault("WATCHTOWER_POLL_INTERVAL", day.Seconds()) viper.SetDefault("WATCHTOWER_TIMEOUT", time.Second*10) viper.SetDefault("WATCHTOWER_NOTIFICATIONS", []string{}) viper.SetDefault("WATCHTOWER_NOTIFICATIONS_LEVEL", "info") From 023919e0fd4f2c35fc044acd4a769e54610333db Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Mon, 7 Dec 2020 20:04:11 +0100 Subject: [PATCH 05/98] actually fix it --- internal/flags/flags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 356c16f..af62062 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -300,7 +300,7 @@ Should only be used for testing.`) // SetDefaults provides default values for environment variables func SetDefaults() { - day := time.Hour * 24 / time.Second + day := time.Hour * 24 viper.AutomaticEnv() viper.SetDefault("DOCKER_HOST", "unix:///var/run/docker.sock") viper.SetDefault("DOCKER_API_VERSION", DockerAPIMinVersion) From 33b4957d07a09e311592c39dd8a0fd382ddee89b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Mon, 7 Dec 2020 20:09:43 +0100 Subject: [PATCH 06/98] fix default interval to be the intended value (#700) Co-authored-by: Simon Aronsson --- internal/flags/flags.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/flags/flags.go b/internal/flags/flags.go index af62062..2f7a89f 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -300,11 +300,11 @@ Should only be used for testing.`) // SetDefaults provides default values for environment variables func SetDefaults() { - day := time.Hour * 24 + day := (time.Hour * 24).Seconds() viper.AutomaticEnv() viper.SetDefault("DOCKER_HOST", "unix:///var/run/docker.sock") viper.SetDefault("DOCKER_API_VERSION", DockerAPIMinVersion) - viper.SetDefault("WATCHTOWER_POLL_INTERVAL", day.Seconds()) + viper.SetDefault("WATCHTOWER_POLL_INTERVAL", day) viper.SetDefault("WATCHTOWER_TIMEOUT", time.Second*10) viper.SetDefault("WATCHTOWER_NOTIFICATIONS", []string{}) viper.SetDefault("WATCHTOWER_NOTIFICATIONS_LEVEL", "info") From 9ac1bf6ca2fe8114fab961ce7bb02e34aa1c034f Mon Sep 17 00:00:00 2001 From: rg9400 <39887349+rg9400@users.noreply.github.com> Date: Tue, 8 Dec 2020 11:40:27 -0600 Subject: [PATCH 07/98] fix typo (#702) --- pkg/container/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/container/client.go b/pkg/container/client.go index ce694d0..2063332 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -295,7 +295,7 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e log.WithFields(fields).Debugf("Checking if pull is needed") if match, err := digest.CompareDigest(container, opts.RegistryAuth); err != nil { - log.Info("Could not do a head request, falling back to regulara pull.") + log.Info("Could not do a head request, falling back to regular pull.") log.Debugf("Reason: %s", err.Error()) } else if match { log.Debug("No pull needed. Skipping image.") From 0f065399b01a36a7c782f5fe735612a5907f9e80 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 8 Dec 2020 18:40:43 +0100 Subject: [PATCH 08/98] docs: add rg9400 as a contributor (#703) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 67b4092..3365865 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -693,6 +693,15 @@ "contributions": [ "doc" ] + }, + { + "login": "rg9400", + "name": "rg9400", + "avatar_url": "https://avatars2.githubusercontent.com/u/39887349?v=4", + "profile": "https://github.com/rg9400", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index da9dfcf..25e6474 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Chander Ganesan

📖
yrien30

💻
ksurl

📖 +
rg9400

💻 From ea16683c46bf563acb3980fd510a22667c3c3d83 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Wed, 9 Dec 2020 16:18:07 +0100 Subject: [PATCH 09/98] add defered closer calls for the http clients (#705) --- pkg/registry/auth/auth.go | 2 +- pkg/registry/digest/digest.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/registry/auth/auth.go b/pkg/registry/auth/auth.go index 56f64a2..1596aca 100644 --- a/pkg/registry/auth/auth.go +++ b/pkg/registry/auth/auth.go @@ -37,7 +37,7 @@ func GetToken(container types.Container, registryAuth string) (string, error) { if res, err = client.Do(req); err != nil { return "", err } - + defer res.Body.Close() v := res.Header.Get(ChallengeHeader) logrus.WithFields(logrus.Fields{ diff --git a/pkg/registry/digest/digest.go b/pkg/registry/digest/digest.go index aae8dfb..389f059 100644 --- a/pkg/registry/digest/digest.go +++ b/pkg/registry/digest/digest.go @@ -91,6 +91,8 @@ func GetDigest(url string, token string) (string, error) { if err != nil { return "", err } + defer res.Body.Close() + if res.StatusCode != 200 { return "", fmt.Errorf("registry responded to head request with %d", res.StatusCode) } From 4d17cf1a296c88bfec41983b634033d8d95ce0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 20 Dec 2020 18:16:32 +0100 Subject: [PATCH 10/98] fix cleanup for rolling updates (#706) --- internal/actions/update.go | 1 + internal/actions/update_test.go | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/internal/actions/update.go b/internal/actions/update.go index 61f6433..e37e671 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -73,6 +73,7 @@ func performRollingRestart(containers []container.Container, client container.Cl if containers[i].Stale { stopStaleContainer(containers[i], client, params) restartStaleContainer(containers[i], client, params) + cleanupImageIDs[containers[i].ImageID()] = true } } diff --git a/internal/actions/update_test.go b/internal/actions/update_test.go index d599cde..1a53aad 100644 --- a/internal/actions/update_test.go +++ b/internal/actions/update_test.go @@ -80,6 +80,14 @@ var _ = Describe("the update action", func() { Expect(client.TestData.TriedToRemoveImageCount).To(Equal(2)) }) }) + When("performing a rolling restart update", func() { + It("should try to remove the image once", func() { + + err := actions.Update(client, types.UpdateParams{Cleanup: true, RollingRestart: true}) + Expect(err).NotTo(HaveOccurred()) + Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) + }) + }) }) When("watchtower has been instructed to monitor only", func() { From 8c9545b336462c3e295ae4695a985539a0eef8df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 20 Dec 2020 18:23:06 +0100 Subject: [PATCH 11/98] documentation theme updates (#713) Co-authored-by: Zois Pagoulatos --- docs/images/favicon.ico | Bin 0 -> 53108 bytes docs/images/logo-450px.png | Bin 0 -> 36798 bytes docs/index.md | 11 +++++++---- docs/stylesheets/theme.css | 16 ++++++++++++++++ mkdocs.yml | 7 +++++++ 5 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 docs/images/favicon.ico create mode 100644 docs/images/logo-450px.png create mode 100644 docs/stylesheets/theme.css diff --git a/docs/images/favicon.ico b/docs/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b9099b44fc39d2564727d7f3938c9b79ae5d0c80 GIT binary patch literal 53108 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 literal 0 HcmV?d00001 diff --git a/docs/images/logo-450px.png b/docs/images/logo-450px.png new file mode 100644 index 0000000000000000000000000000000000000000..30495268d7ddc4e83f8c528c5221b630ce01560e GIT binary patch literal 36798 zcmY(rWmH^Sur3NAxVr{-m&V-_+$~t+?ry=|Ex5Zk5InfM1Oma`69^vcF7`S1zVUuE zjMYr3S^8DgTtulT$)X|=AwfYwp?>-(r3M8B-T(GMfCIkqBN&qgenGpc$x1-gO_Ll0 z5AQ9-6~&>TAPLA%U|8T8(dnbED-;ykz}p9U#IeE*3MxVUla#oIm*IIXd=Bp7&4_L+ zi%g1+X&i?PRh#2R<8_Og$L8d%KbNB*v->gm_a?WbvUw>VcxY&7%!Z%KiY8~TgIJ%> z449o>4?Qh6e)~H9>3{dxkiTc-K5&@lxFN zovOnCiL64&Pel*H1JJ)eTu?8+mufkmhz&!2?g;<=eoc5^6RNQgGc)YD)p?P7@9;qE z;=(%U*zY*tIOsU!=n)_sGh_nMk=4wZITYNt@t-}8%JuL0-fj9Kt$2MV+Udqcv9Wu8 z##tl8nGda3#of1mIbHtRJ#McfxYqGNp1G)Qru0gV?Q5_w-T;{AXXt;qf=B*L z$D0S+t@Dm237e$#*|`lY%d~dHEtnQj_QNf!oq=zZ&aq`taQ}T!>eOQskd<#@b*DMh zuP^lyG;WcW;S9DgX-$l>qaY=h^bUG?;=L?1mDn68Ihrv#+KlImWZy-|+{An!KHvQJ z{c07Q^IG=BH!t=ofb)jMA%Q#JP*i_fZ=ZR|F4jNTKg55q=@VYeP_*69_L6Zi^`;{h zCl{M@`JPpshWyWYzfv;7Ru1t|GHm$>NxMIl%2~ME%6{|vCUzvh--(3! zAKx11>xl^E4|b!m%K~JZEEeuu_{F^Y3#-GNyanOJZfF?1sB=DE!e$-ti+MRmUL&GL zV|w>ICSH~XHSa>BIDaK=0!NRb$b~Gua7H=D+!{^PrKFLT<{o0=s{0zP>>y zD7VTy4eWt`%6`4^ViSSM51SdZ+!e$+{!hD};9mdqIX1;9}jOo2w`plEt&2gg+%*5s=M+@}!Z?S5xd}DaR{H0cl0*VZrzs%K5 zK+ScZikT#SWP}c}??6S|30LWe!1(*8@DJOo{y#K7{BG>cxXr-L;LXs@@Xg3gM#aM$krP`7@XSzE*|rH0Vf9 z-wW|8(l?~fqP&mML9+>e_%isF&ewM#Z*G63iH{R;ve}queh=vh&N4e2)Z)-mxpLrc zYNp~)5U@+VP-5TT0wylio#!!KEmfWGZczMNg7O#PLw^35snkexJYoEY=exRFNHc39 zva{FlD4_Hdj7+v(-|F(Q|3K;%W7q6je`g=ePklG@a#Rs}J_(IeCNiwE+94oyVB2Xi zJ$+KMMmXn4K?Vt=6-sdg6c38@b|=z)dW3sKctmcyF2b*SlCKT;PNJ`mdCN z&+6-y7U>hCop3Ym-tLTm*6Y&HrsqWyzQb#s&40GKZAhQ|BaM`vKW?cyV(s^G=#jAk znqb9aVb0p#zrnIcejHzoxSeF}w{X7+1ASbJ{y=^A;g;gIG)H{C`pJ~6Cl%Kwy#PY7L@4q&24cA^>KH$^&z01Qc`|EJ|&gCHTFk!?t_VyhK4)VPy z5wfRkho>JlRe7<(&#liD%y>+4)Y@J=N@E=){MMk=Y`LbuM~3_Fhdc@z%9Xb@%0xyo z-CV1utd85@q)7dO{xWSxrC(ZJjY`{%BKOSP-59vyV?g?4=05mKzt1 z;Y@q1c2T2FO(eF1Gxz15zCs^MX7H*Tk{!Cb%41@%{x?T&6OTD+VP%keY<(O^B=0;- zaKA}LK#R|U&*L`svJG!%xm5-A6ee@vqv@#q9<`~$<+(au3~o2z;yx+JiEfr!wwCsC zPn&z8Cq8^PvcGc8yQIKJZ(U7fmvL4)uW?S=22Nr>Ij(8#c53DyBYO(HcK zQlvQwy2iUPdS{<2VfG=z93zksf{HqZ`DKZzpRJ<=t+tg~WIsN-)6nIIL5@)5-b})O z`xYl}y-nBx54LP5Yb543vXOeAz(Fwn>eFBI7a}dn7D*zQnSUnOmMxLZsIjoIQ8sKH z5d2?$isAR3M7}XQ!dkS^;jUx6crlvU(USI`SQZaGjt>x!#O~(K{(Mo2&lk|)|9CYZ zf$ylj`V{>*arkk3YjlwLwrS`kp25Q%Z;fOX=bYoCsVXlzEx`u(ce6gKCnLMXdEo?L zJ9HG+HMXfIkCNB}3L|F;b=|Mh(1(IZVzNCq43+`jxLl&lZpXt1BN_?D>Vpapy;kKp z&0Rr$8Q}vHmPNTv5vpCv@;{V-NBxkGB`>e+Zm#pi<#Z#j0wEaJ&n$E#sjJ`yV9-T0QsPjlhu6aRuKquxBu6muC1_2 z!G_uJVUl4(ZpU0oR2b8Lu(6l-)BG&;_PO3X!`=Q!4LUW;8oIiYd#3F|?W7$h-9R|! z1q}$;4B@|?q04Ws;@$0j&R6(O<(P|igUwP)x|m5*gmF*7IrP)oqb+pUFM9IR-p(eo zR!wD1Art>$znAbxQD8$@@W2-U4lqy~zYoF_wnAdE{9@pJI}%EfK{LOc+9V|;U&2vM zG!d|uahLH*!_$dUf4o$S+~!FcgL}hh8_DC%`+$#FnOMcHa z^H%85l=A(ft_c~x$G-hSF-u*cYoJ9qS-ab*($s|o9W8FoO!%mYbnZA&ivna?@nX+x zKB_xP=^uI&BdW^A%OriGJEikP)B4~AWBxR<CXQ%rW@;~)VgoH3?zIN5#jC^R$NmZvjiXD9jDr8)myFw#m-o7)@rO@2`8SSvd z-v;ocdfFyF|LTEoWc$cpppRHp1IviYo`RYPjwkM+I7p@-HBV#6^>qnjfHXTtuXL1} z=N{*j0e+1qw5<4&^i;3aA#D!v-ez$IdhWGe41g>+=WTHG137$dzqd#vK8fe3pIl7^ z1bjT)V`3f1IpX|Vw++|FtTiRS0T*|d0b5a|TtHUvH#9ZPc+$ZGnVLr|yb{xibvwrf zoX%%oka|c)s_0kkJsDxpk(S_!cUb3-)TC2`ji(hd?CX*>i%W;}{>xF_UmmMEt8{hd z2{~sPg7C@@P7d271MMEEtECa#8WoorF$Nu(fv(H^xw_0tmz6c!Hy^Ip=G`0_rl9g^ zV4hUn=L>I8^1#jGr{~wu8u`5_13yUke(nVm3EaiczZ@qXK=KM>mN)2Kli#QHs)1?R(XDhQM3lc71~ksW9Yh<=FQA}MqAX2EOY zR}9i+*3x8TVzOQ4qd9f?y8Qvj9KL9NBY>ls%ku3MfQ76;5tfN0&7dFa5AsL3s=H4M z#8?};cy|pX-o?1C^PlT7ryFt7y!$4O>I$C=GMd|Dnt7+5TeR#)oC@wod-Sk&^p4kZ z?z~3GT{W7Q{%zop(0XN_W7B7n##9$YcWBb;5KnGcUM*wpi2x!Tissgs?I{$Tc}GYq z1nJZ-KsQNik`%nVa0e(`qF=m&AA}ObY0%-I*Chkdbn@qtoLbXX)S&FvBQ10kq}K6y zbJGtinieyo(gfqf2Ls&n*{BPB&6#&e*vs=LtiMb- zVt33;323{l6NW%;XsZT)neuntUS2~rx9c<<@mUv!qX{1XLs0SVKZhJFe7c~RdY{%L z)*KFTefPivY{$tLVg4Ju*fcN@35(dlj}L+7=tsn7*1G=08aem95_E))HO`9J$cbVR zRDoK>S4j{arQl=_xQn_VR^!wU7lX?kEq|2rB})D-u8?r+wOte0WUr}L zVy-OEG6)5`c61!%7O$NdsSFY4n&jmk5Uh$SKQb9a6AW$8!DPnSTug3SbNkk+bXBoH zsX|)Y-YP|VSfxZ74lY8jVIJZ)lHMGk!eQ(FfnpmcNF%%ZxwIxyYa)Fq^RHF~SVTeF zXB^8qitxcBn|d1AEFSGl_$^Dmtf7V z-GR%vKA1zsf2$M{2C@&R^ea$0gkG7bq`!L{%Q7Q!7KoNcih=YF&pToD%<{QP~T zCgxTLky=8n()Zq99LTeLU7|qY;M6TG>q}LKG|HBvjdCotE?Sd#>;Rpbe_ubhvd*e~ zr?M{SmX^_Z@HWNNJCcCXf>~b8}Q{+T}>N z9f1ys$Fg}bMR%+{K*QyDZ3TOMGd>^M=J6wW=2H3gZVO?aaDyRzcQlrgDKjhJlSs)M6(kVc6zLi(I zKfC>S_rvWJk+Uyf5}xMw@kBhP0l#=2I3o*&0~R8Ev!*gkx$d`?W^x>cTYe}3zb*=Vt_{b5pvmk$cCo#xKX9v;YrqghDBHo|? zVf$sn>6KDH4932j6X;3JhX+_!J?bYJ%^k^K=p0$FY}TDtaEKD^n0@O@=Lf*C+BVv} zoEU2-#p)_Gg@sAVxt0dDY(T2+FnDt+lb=eKv5688GPY&2e&_z8=e|LY1bfh%LX;Go zFMX!XK@|=Z*iVh~f%4x{_EvEzJ{fZ85Qc+ol3H+lF+wz$%%Xd zniGb)sJuH%qK{10JUW#YR??*ehJ@qn3eO=rKl*Emuux4h99Sf|payva2Z7>=_JCJ{ zd+kKS(!%18yTTA8nA)^@_<9s`0@@le1qp3pbDuB=f7wgB+P4ggV;aq!Ib9ExJ7FaQ zZdz-qApqfnf>jdb0#yeDkrM?wfSl*s_-UoxVQ7X-cpEGS8e|&Hn^~&>@I%cS`Pp#e ztgS`aLOuvB>9nIvl7!)iMi|9sM5S_ELVKK(AAUEJ#076KC|#REKAHtP2$vJ0Cj0)N z&0y0zygRr?2nSQX6a3lWsTp-Aisiez;1?C?uY_YZ)cizSkqsXu=l|Xwdr!(67=~gU z4y-9vQ;hujJdZNbrsEk~T~p{P=$3Dx!6UBWAhz$xBMywiiZ+M@S22P(59TIkbAwk) z?7|^c;dOgCr^i)RUu|eJGE_gpRIs7a{x>?aYD#5;_9jDcRt=3~4lS-mN0mg4`e@5h zQy5!eQi@DGqrgUm17$#pw2_+FU@Kx+a0f#XuB9#3u0+cz4;)Jm$mQ8`53vKaNiAB+ z!~*(Fb5x`3!^c-kL=y???8iUdaTM}RrMQ=_UeuL@7&7P@ieO90dJjRF0L~Wio; z9)H$PnORxk&LBv~IzmCK`4ir(%(;r@3J3+IojpANO$du9n6=1qX%XvR6xYrpeH_Bj$f9Q52c6{7xd?R)BBiH0P zgQ+k2U{$fmM4R)nc@OhsZ0QM#lsxjN8 zRoiTvkUpZ|BN#k3B7R|BE02BkofeGPg zUJxeSW;2USXjhxU(~o9x!HIAYciXzP3BPgOf_7hkcKFS*J^dH(??@QL*}k3QI)ns* zxjG%4&WC5G7^dec6G)%`Gc<#~nnjc=m93!nQ;nUG9tLoX@K6KnD6UjPqtG}^F(D>& ze_ZI+8v9j2*j>Hnm(z6EPqf$MJX4G~_A!r4=m2+7)Wv}U5M`2yg%wHZ zr(xc~wf{VI!vqo?II8~OGT!a#K1%d<*2IKw6+qq0W6Dm%Xe^^cRb3un1z zXbKiKklLN{i?sjX-79cjBmjnC2bx%D#`!ZWz0gq%+5VNAZ%FzskqU4|TaO|dgs7BZ zNf6FYaYiy=n6|)(Ve5X%;7h}B$5G=rH01f$u=IGzAPWpvi!p??mEQf^jhtUG7))>= zC9Vf+VqMyd6y2T$wqFqP)~3W;rO4Dgv`IC|tv)^w$|{R+OSyq+S*a(=k2f?p`+pvbz5j_JZ6scUv40p0jOK=S~-J^Q(vPWx{H6bLh^s}aJUUnk|r zvIu)Lkh$enda_syO)RF2u|7Sx3ldyY%KFn6WBRr(^q`@6qwtW;)NXFd(be-3;%-ff zs)_Y+WwU8#hx^q)?+4VZ{jGL#x+yLoa^T-yNl35Ku0M&%bH`u~I;T8$711wfp57NWRJ5GmLCBGO8SfV$EwNz8@Zi|MNL@o}PgGzG0R727Ob`d{wb0!08R+i)jI zlrr$cb`t5v^0QM)k>^UOmLKHf)Eikni4V}S)Zk0ys7v;vmOiE(O}9$DEuTQaDMO_N zKkPs_;cnZRZeef|>b*2~JyMd)Wi^6Y&JPBT>!sBf8x$FH8qg3m=dc@s200B-k_4cashAjfVGVsiF>#S+qD8uSs|YhoNr z%WnLy1M>9wXcE?ae zod~r^g?b?&%bMydhGRaV9+TU>xuGK3=h6Jmd4SC4#hL5ZI%lOOztA77au>!x1Q$Qc z$lM!jq4k40+zlP~(aZskQN~f=Ecj}6OUpqv%`>n~+n@AET;RCZ{kWY*png=#Gj89? z0jHmPXVOuHS9!o;q;z3M)yjU0H$7_6+2Kh@2v(UkHj(MJ8aYn`mMe;9{Kfrzv{bwL zt)^y^{}zJ^s5cfeIHP55J&Fsy=MU*7T{DaVs3S`HaPF%y&O*)QH*?O>Lp|%PSCH%Q=CN{tj!lOJePP5HD5qTs! z>F-kF+^UmI@#h${UHA-ls~DOR`DT{i^=qmCI~A_yTXd3?z!{?RH5a!BK6Cu>{!(#79dS8kF8@4(c`_D& z^d4;TwQ8OL<*L?e{Y+{DQ@BY&(0fC_lyGS#h&bk!WX=6e${yC6-&{VZt@Qpht&1$9 zehhb5&z3`b-Rwmj!i7qot?~eNL&0VOV26vZRu-;J$3xx3?oDEZ)JqMh4CO%Yg37}K#933DW*IUhL<~R!Fe7*dDJCRV z#V69i!TEFvHvYR*C4yCsH4 zq1C<+%aEx?Z=N?5(TFoh{LH}wnnWFyaI&L4!;M%&|92_w9fD~6KU>A4cD>8PJfbiQ z|1+OIc0sUe)35W>!XmG>RIk-G99KiVS(m3C=gnk*krGMTkv3d0#54~9yUU8pjI4d8 z-{yh1(Tkd>=-D3q#&dPWEyBVnU%-;q)9hLTe=etEhs((^@`ormg|D)s!@k^?Wrh{` z^O8C7h}Au@AJs{uw9g z(w0b54SAjsw)l^O^YiLsm9n#T7puKwxf!bDzi`o29Dz^?EDGHPLyC38u*b34CbY!v z6>loKU3?Gvqq#SqFpn;bbzl5hXI&a=1`hDZ8N2T{=K9Y_jf0bcm@QJ{{kP8Tj816p z>OsjFs7jlK8ohtnqCNI5vR4(|2h!L9_DM)!M6dk2@34l&r8?`|!ttn!Jw!lQ5nEVX zcUTwFzclqPYS*PqmV;yBjR#B*91km0(%wqFjDUL8P?qfpG^W|y>?$^J4>>2@bP2e( zB4|tI>>3*CT?XDHTGr9ba;DDEmlt~xa_a8LC^Xtq-`@9MJ(k4Yc>$(5lCtNj2s`;2xMc?rj1K*u<$eP6+^Xu{_5V?|dI`gzcHtS0& zoaC8&_^78Q0bH;87E6#}h?}q?Zv-vi+{YoM9ok)au7uB#k4NyDB?^hAREWvyv;?#S z1je=m1+ATJDnte4YfzHbAH(m72~2DXt+If^GdpKme^sin^2V-kL8oHv^7x1@_qd5{DLO9$J{Lf@1BQr6L;`lv41*^bW86{2-Y+@+ih*0|h5X zoIqOt(`czL*Vd;N?#053KsBz(p}#@qy$Lqy3u*Xc>?jnd4PJp(sSAoZn;fz^a&Q%# zohML|LNO?pNX25*$rLTSuCY<#TPznDVlx2Rr~5jhpYaC7M)FRSDwwJWmqg!v!MGza zBY4C^(+V_%hdG0SQBciVjZ$%l^y#)r)*cTZ9VF3oYxn}0K24y^7VW#ZDP?;RxEQ&h zB&1EhB;GP=k{|T`&^S~%WjhTe8AS2Y;a=6IKVvFsW)kgTfD>ckc4e!0m%e0OnrV^S zIO9sZ(Rnlx!Z_w+ph}C^m*IF!u+`3M7Gtzn1RaibNU&p`CzO;p5k4v8&7rh{WGeryG>wWFUAbDWLGN6 zT<+@UW^+liq70rr?u#5TbV{nH1i?kSb6^d*md~7%&RO7RjC-mqbNmnfr}7( zpbb+#!DVRlz!5})2DQT5sA;sVL^ucQ>>?Gef#x##T&fV=Y{)1=kLU3Q$Ec&{S=H^2 z!4y}>gz1L@HrAOBQY}4p-K)8dF`RQTT{Mz#_g^PjcRaxs|IrmlLHjt0;hRM9SDF!9 zddmb@t8G5kTWaC^9S_g|?}Jq9spqVYep+(7dX8q3{cYTBHPIfTkh;U$Tg^2JwQ3pu z4LaO}xQLZisU>vcWDH;ts)rIdAP`fI>&X#&-~^$*k);~X4QLfB#S0G9W{biF@Z&FA z6k!q=U-bv<%p4Exw4QUBJ#eM1NWEhpT=;ylgPyyBk$KcR&49u>s3_wVDP&_qmH!7c z(qd!yh75lS9SmHD^|eZ0`7nF|N}iNQP}{t{uD_&P09jc@UYbSH|Ev84kAjWZ>nnle zOy+!i5b{meg=KpG(g(Eu#J(jjO{u833OKu$kKs|&#)0&1{+fEKKM0NK10<#99C2P{ zFr5}fz!^?(8!+fd*VI$&qPWR)`T9 z2N*e<+Z3K%W5~l1AVmibV5s7BIV*YUQtN-4A)sATStq4IBOmC4)Mu#!UFf2fEnjFO z+AOoJ7OOA&38ooNuyB$6i$_O+(`XR8z{2-LCR79_mtyT!AHR_i(vk$iYZ?mMJZsC} zcsQL^TZB^kuqk9SL*8tD5l-iOR^sL9EY4^TP5+f!Dn^ui1p&e=~!F zS^HXLr>I!hf2C!l26UU6(y_4&+0+ppylVPI^+2<2;+gm314>{ObVxZ+bdVoLSRf@y9chS_4!dU9>%=pd1RhsTahZl-rjS+=d@XV!1cKBGR4 zRb)f#H{zXjA?Al+6GH8Ff$mTM0VAq@B+{b*6+R)uq(TfO+zV_D3Evu%X6}ebrj_(B zGnc-Z1+!^okn6GTkMj_WlMS-Fql|~O`+qkZ3FG=q%*m^S+m`tYIy{o72H>CnJdh|{ zmx+OdVP(R!igbj68`nRt5t4Le5CE5%dp12YZY zDI=Zh*ApI$Ad)g+8(Mt+g;-H#!#OpD)y*>ggP2|H>!Q4V&RI%r_%hT0a%!1m(%)*X zX!9v89;q_cUcL}sSRdT3(~{~*a{J)2bl+nb$$45IrY{HsKWJN^Z&P85Rq0Yt*}H-$ ziY!j^7h*~%iV}lP+Bg*yp&gNGIg1IG)}xN;l4LOdjM*0*9}q*6+1!yb?=&Np{kB># zPmJXCjgAq>whP3Jw2wUAVdNaNfCY#?2PP{P7wB%E>Q&90FaL6uD^tv%0o6V+UP#W7 zidKexZe{-_j}`Q8_9)f~OxFWl1;c_(*o&e1gJfuGZa)MysnU8&QL#yKfqs%L%EaZ{ z?iZzuR1FsZ?l=~r|Kq=q{QR$D0sk+Qg$+CbIg*IcpD<`2^L6<=Xc>HZ`v}UzFLErY z(ldbgh(d0GnS5%@|CNEqsr9IsW6_Du(wT!$DBq>h(OD~^`!8?3T48RB-Pyp)x&M>eWz`4sPP zBSgqR@Ub|yf4s%)z8pE4oWdwa#=(=~PI!L6aQ(SSBeU5eaZR61<>HV-E#xmVPFC`- z5Cx{;N4xta)XECf2ef4Z4Zshr&ehW*Op=ObH#OBk4PZ=Ib_8?)cb>L3s$N5~O8!apG-eN_KazfxAsFS^QQ{m% zeIJX>S4s9j-{ne35FRcO;SL(imFlan1MtNDdN+Y$&}WPIhmLbKC_w$hl$MNEzz z2^w6gG>i0Zv=XoxsT#(@_U)AwNVzmeA)$*Np2U%KFk~)c*`&sE?3Arpy-RGndKnoy z2uX+*NwI(}RZaZ*EcInNxRsyB(~jT%>+OT+`{shmgFWf$!4~-Z8ne*CEpE?^*HE1= ze+G7LJ8z>ht6L%qUDKmkcO9@YLq_D`Elp!tp5X3(GIRD4+renSiicmqNJ>rV;3zvL zPlOZ1eB3?d)-ORNVEQYoh!5kaM_;xE5ohjlP0;b332`yOJNb0~nu1q{GZo^PK$=wh zer%N_Td*aIK&z`PyB^LjvaPV7T&Ka>au#0;f0cmjYdq>JjYOIWr^;Z`0S#C!)O%FMZwGF#SRTMm9j}vZ%l3OyWzcNa= z&t;pmwCmkUYb_5B5T8FVCRhFmnzo!CtHU35!k_YjWFl9K*=$LhAf~P(>4MyXF>6;T zz35{nTpBeSuK9oE6Y%Xu@i=Ap1aka2`JVzakxO^WZ^+~YgX1{ngP@C`uDHztk5qc$ zMst|wF+LOS%%|$}4AQ?tW`S(Y6Y=P#D&?{bv+1!otTfBl{BrGC_G|DN#viX*zl)mO z;DsYDbO_jyyS3os@pOBuJw$J?|FUoG_(~S&TPr-QfSZjbQuQ>S0!xfk9~|ys<+n`&@37us0>V^cV+;ICK_0O(yg-Vg;WHWrkrnq618vp|%ul`dfMO&5 znf+8`xuVZu(I?lw7ay?JC6VcTXi4U9C}Ijq1HpVB%C8dFMrt!*{}-Pu*cf60&vgTj zg7E?VMFQ@A^)|CHI+8JxLqTzyR|>~S79ZXGY_O%g*ozyITLn?OE?`2`SKS&9b|pAP*MPNFT{iL{GKMibfgzxjyC@_O zjBW(bLE%Sk!^~gso_`w1IbEHks*%>ut?@pdVcDZ-lb)_d0*)_)2G$xS zHcn`!=P)o*vO%Kd_!DR_c7Y21*#E&Q`(=#N_dS%aakq(P%+C;nGJ)Ekw;oC-#>*fhoL~SeT2A1(03|DOAC#g zQ|24&iglXZ6LjQ7g2EDSfK3bTW4MjY5;2oo{qunP$X>bj-bmoW*j4tCIbyVj&B%zj zQfx=8&%b>Y9?ZAH?PC)rm@xH|w#MIA5`F9*#FghPx5>d^7jr{tW32t?fdTNm{{!Ho zmx_E5quYv8$@0vrhk4bxBfsj588MWCCw}kBSy$LzbowOZV|E#bccI3JZ@b5zYvPeo z?tm#X!QO@EWrPnVlTWJyE|S$S0j+OG#np$96Jl2q!F~iDi1b$)IJ8j5^$W@W=FcOU z2buAR(awwQQxZ@{w-;9k67S3FihO(I-8ZHmgvQv$4qIwA?)VYtM;k0=G zSHkFWEH|RGXl(i(Km$p+&;bjyig+?wK;bqD2f(h@z5yd7x_h~(b8t)-=lUWgpwy_m zl%)9vxyjXr0VQj`$kg!B9E9~|lVYoLR6};jl<)A_UM{R2cJ+;G4E#jjk8XN--5FVN zTOC)cpOUShw0YY4`_jhqLjw-bp+By zye|pdlA)KS*AR804yw}arG1mz4*SL^3Hwse4|w^O-*~yvKJY+--W`5o4RBmb!X{pc zt3Lr|Gj-iFL#OF3VE?NWRomY_+(f{nBxBp{OBMbNrm2C-Z3^Vz^|tiEd#iBEsoC3{ zb8$HoJjzK!oAp$ZQY>p$+)_0qBmI3feb`bzFL`6YSBditn#}8vO(pBVOX;KC`eH~p zEnh`6-jTIg=if@W;O|OHN7|qV14S@&D9}oeFpsxsQtk-pFd) z7BOeUbmL4_@xoM(Jd7w9xG!u!ugrr?0fFO*!&2jfyxMwv(jcHD{WF>xF1{=6EG9Q_ zzIr!ifpC*Huky3Evm-tO$2fQ%p{G>Tm)nY*v7W?!?<%+K_+YYNN#45M_eD2O)N%vhk|{YGpF`-W>WJ|$v?0y=m18hXRe>uPtIFzp!Ov-oPBB^8UCPVgK{Di z&WHF?S``vyi@ztj#}p;^qfBEh@29w4&Aac8GFk{ z?K^9vO=4V=_3w=I`P{me+g`!@5nOZ$>kcQXkYH9Gr&3;zJ~{7wOEi`g?O@IbRpf^TDWm$`^GPm+24n95Iw&k^yVchy zIzdv(tVaG#+Zz$meQ~?Nv58&3<-cxEh1{*+eUvDca2dtC4lc0h@~5mAwgO>fRB`y8 zd#tn~kJYtF45pd*@3HT!91P677&Pe2QN~*8Z&eQa+6Q+EHwt5_5e=_XhflT}OzO1j zt_6G*Ero9PJ6Z0{;cG+I764nQOsR7AlVQ2JTB)RLyj@3W1#bX34%-WUQQ>yv3bMJA zmOr2R<8mcY!YGpzbNjIGaZ=kg`00Iykr8gqK(jTcVQq6Vr9uKOU9Rz37PK>+kO=hAMdibClI83c8J1 z=o%S;OwxjyyA1UMSk5Mbp9DkqLR(2ZMnH(>xrD{`KR~r@8fPveK%&$lgsi7=jU(cq4W1X zs9*DQMQyTUY|pQpY}rZbxq5yyW&qWKRKkRCAU%mt&0A@LGpL z*UM7pbo$D_uvp{GV&$klUKW*ZIv*YKr44^El7E+UmJ{~hAGcoz_h@W!>Jx;@HX(+7 z_hXj8lqv?Q!G|!)=Y0m$g`Cvzsm{TZ?>X}rN|Rau4-Y@My%qMCjYHt#Zh%*RgqM(m z*iKYt<(WIK9QGqPx8are^quS451}D}bz&2VRU9ytL<>rcnv&+Bua9TQa|ud5i>Fkm zYZd`7X+LvWn!*U@vJ4)JTk2}aVj~DAcq0E;-x62++zimhnT?gX$@^Grwx+mqekbbE zGPx8*RM;b&1fY())CbN|@QaL+HG(Ie!wQYV=pRCx(3zof?|K)z2633L4z$44gv}tf zdHH`a`+|Qo1L(1V9x00A;*sRRKC!c3C6Qg|$(=OJ+t2qV<)B!s4Pv?|QgaWv0TQ}W zFt!@e@;zm+x!^6L@~0kSD$peFIzxMwX$UIOpl?_1*auM*KUJkz_uNPibP4~mW53A&X!%e)N4x}`e@%3p zvwcEYY0$bURJg{>sRDZfIM;bDgK!6eZ|EWs$->irM6MNg0XckSm>@|n2NlS}l!K(W zv(ZHued4o8bL2nf9di2lZwH|!CnpD#deOHGC}ZK>8*4P`Ngr4XBi&? z|7+~JgIgK-QyCezCkHx)%-ZuZ*Ut9@QFn>RTDI;NnKgwb8GY4qbQ1if^RjqYVz3(_ zMGGPWYUT5AP|1+(U_?G#xw5g4sebjaAa<^?>cZ#>a%t;>KU$t_N-sZW=t3qELu5hDe!){M$5Rp$P=kZh>0{FSx70)L}jmKK%Dn=c{QQtuUV^GkEr z3rrPjM|CE)%z#=P#%<>UKmw(9cDp~=Gf`|k&l`Y!+l4~lyX;J?j3m~D6fD|ENSDb) zSkCW2!fWEMDu$Y6=1P!^cE6CnFqz9mj8uLX?O(@@Ld3QBHFvc#tAdzZ^$R1aIT|r# z@-X}F&Ey7VIp><}>InQ#kpxS(e}|;05uwe(u4->WFgd>zL%I!{Ct0i5B|1AMJy@7d zJ&z>4DELiPG&5Veu{a#pwe}-8Akyx!>^Ro3@MJ)oJR0{SF(}-X`=EW+=29P1N3<9g z$Nm(DG18YmKWo2Z$6@lq>3o${`JMNAJ7-E60;E{yU2#E8)9H@RJzSyWl%$ealHq?K5}y7GE(x5J zh)Z)cpt2DUR5qr+8%*kdlV<*od>%zAQl~HSwo9<0ptPD{f(n&iqqE%7KB_)&0Rt$x zQ(cg?a|!R3)5fx={sl6UBH6~j7v7$u8mRi0<)a?z?w-VHaSbnV7(Fn^b)0bV*wUt! z(S5vZcnj%{STU{ZuJa72cA>Fkn&m!U9lDq4hdMgMyTC&!Ox>jS33sJ zPXMN8mnpF!k0KnK{AeTN;(D-k3F0D9tv)YXMmXI-xcp9!l+R;a!;B?reDIFrA^yf> zdr8>D9Vo<@8)r9bd>y3^eWHhe?W}q zwT@vIIV>*mvgzb^2xaYI0R~m`hC}j>yxr`w>4umLfxcz)rV}LnG?r7!fEY}VkeESX zTo$MiHzxmcg7E#n?~77ZGKvIXwN?sn)JZFx+Gm-gKl8?X45+2*hcV7ES}_mAo?~f4 z?kjnbz%C)bRw@cV9JyEU66I&Db_X4deWIdzqP`%zf=j?Wy%Vl@VyA7XaHK+kg~kcO zdERg+B^-L6rE44>v}ZlZ7tgD?YZlj_D{kcME(2*}{7zr<)^@R4a8LZp`&jvjYOp)` z&2O=L1IoQB$KkndU=l`ITF45fGJC3_Ql9t#5*S37N-(HQ-yjEWRL#FWFkg_xggf`y z57Vywr_oAPaLE-MSG_3jxiD88P6#YKIAt=V^s_Z70~UpJ2f5Elw4Q+8l2z8G_Cx1$ zPAsO$rq{n%9DYgKag%c@M_pP6!%oFAkR3`f>;I_8w!K%rg;x_X@N$y4)nQZ})x;6w zZ_NK}s14vJyUe5*J9cJMfZ&vZ)@X?>Q2OXE{_5rqFG? zxAlnn>-ktYjUE(6C7_?in+*( zYMt)mF7uU9JHe!reK#AZ{}syIuV`;Z0s(a ztN2)y&?T@&1m1(+hCV3(g2i*CG*mI9cv2l#UfflcUUKxgNj2uetjdw5M)KfyKio1GLMiz zf?3~nWAyOHqiPt2^TBsM`CHLu?5dd>pBi^!o|8wX+uOn0416=`l&QbQCo-UZr=7tz z#jC+`{rLxj>36rXoxpuX&ZY3xc(w~12|bSlEm2T8I2qq0G=iPbPHdq+YA=#v{rFIJ za=@v~Bg&kr>vX$3*wD7zMu<`Cce~MAqjUL@WZC^A+KL<%5nhP>L7^^V6#{s#LI=HE%|yA^;sbw|9Yk44GSEik;4M@d!nc_X{I$|FsZ@+mikN zoxMi;!DoWg*&wH&Q{3}R_TV$Avc zgZYu(xMsin8hJG9X{JNGeazxM_j)j-*&=2_rO^`+r4*kf(ua66IcE$!I-YmKOJCK& zq`~2^7F>AzY~*k3uCgIsMp*m*)(Aii@H_b@tf;XnV0KO*!*hqr1( zDs8h3x{afR{H5a^eQ$fR&mjucj$_&CyJhqLhjv*u0utI7t7GN^`(f8w6iNAH?1Ty& zDu@5)WE#3|pIhOaiD?fi-Wb8_xWMWRq0_b&+kSTv*YcY~s;2XfVxQ;jZv#?vvW^3T zao(B#abfdX^YfrL_*XCkhPHU6&bPyW|8JQ6|Uf1+>7bYB=g4F?{6L_7!XSL zrTkK4)2b!?6kX1}yi$}-$FAZ3oA;li3@6&gXC27?N#1AbGln3F{4`Po-W?i#MrRTQ zT*)#5QxCkiH}A9&m%d&qF)%_FHs00H*rH@f1>U^cuVx)nwWLzx2CAJu?aFy|M;K2=#!^Q-Y|D>ZqXjaah#OjI`zy= z|I6MQ3gwAoLFQ!y_tWS5A8WU$Yd#KKoc*5fUOB!W8bfUE#NPrA0BZN|04vYxDF7hiLj zn!vviqucWec9uMjjAWN^8#xlREo_`50u)AA!L8Bn@H=IxZ4U+&$ms1qBW(7Xn;8Lp zM$!Mn)L8~p8FXt~M7pKByQQS1yE_DFP(ZpvkZx4Eq&6KJkPaoJJEXh28~Fy`_nhrepGtJx7dq_KQVN-U9G&?#aICG!5+*ac4Tf zT1-`isNkg?l^8Nc{Ab_lRoWED_U&Pr2Z8YT45&0ah395%9w|%10EeT2ad>i_eaY#+ zbyIojjb}H{~JHUv?mGn{df2MqRK3cGgT-`^04T(?HvP`N@tP1!=|Fd$q63g&et0SLBABREG z{-r;TMseJ*q+*pj?M<0HqNG~khRH=1+i&-FqT2=~EOEJRSe`tY)iorU^5H#J?fuq@ zvHvzkeq0rfk{D^30*5PIm=@ZLnA8DFYzSx`Uy+7#ACDs+;lg)3O(*8NY=cmVO?vhm zm80~U_3t5X8^>JQ<4dXNX#qJ@M zXq<3%HVTR6IKSX%|KM<6(+JmT#LvdD_D2f^L-+-?vTCWyubqs*rf7hiuJZlIyuoJB zdok&AG@u$Ad%S~1oUHp1hmI30(QwS$Ax&`W^-bq2Is*%9WAgsH!tbE`|NC)<`V4Y) z2jUF+Ro8xcCCrE`55W(V7t+;t=wE-#Xp;9mjpwh5B}tvc-^(XhYFfXCQ|#2cMhp_Q zyf*)HO!$`0IJfoea&h+{+MHemB<>z8Pg4vcx^n%pOMb+^rfSJ+W0A|fRi$%#M~&(y z+}KjLC#(peEmWi@;V%o8`8L$7Wb6AW8q<_IN;vUgnqG*OasyNuD7nkWZ2nJNW1Rcs z<|qEbQ(R-VO5rA3asQ_@!Tqh4_Yppl)Wf$B?DAz^`;{QQ1iZHY4k`-Y(HGqD=fW{j z#|=+udH)-_Ni|s@zos%@9@eXt`rn2G-45`<>Bo#lZ)h`JTHQmyD`(rv?r}o=Z-<_9 zdSBp;{@w>xyU|lsJK?0H!N>*up_}zQc4N{)-yAoBQ9;|8%_Is8_5CfJ!BrUZ7;?f> zr#vk~TdBK#!^Xa_L;25Uhrvi|_Q^T>c0q?Hd22+cu%!GpW7MRHvb-DGTX zi>lii2()1nS7O1p`CZ;Nbq4n)jybgB3Ju#Xx_;VE#j5{z_H&HF3E5tk!mZoj@&~<2 zG2+|mGxuuq0p$Q}3nbZuQy72osg`nvJ5x**dJb@&_C2cK!#CWt&LjICbt2{0G2 zHpy)*EEP4F_o^icaz2zUj(RkD~_*bH$2Tf>L{@kb3V~CuQ zu6n|sYY`x?&1T?F9xy`Ff%AKbKptK-_b1{6Tps!NY9sRn4-@Kwp+*#A1M$&uYWd3b zXh#xA832U?Iy2}BE-Awb6{?6c+;-YnZBbjy-Tvj*>K8=gzmNC9E;rqX^rADVyOn(l*nYOR;m>z=lOe%QU&m;&Q9ED2PV2Mp^ zKSTK`YcME1)BBIhG2}CjOX7#!Qz2T<39G?I2}PvShJ9;Anq1icXFIM z`$bwv+|I)5Ew@n+GO_V0dgu-V83d6rIM=Yz&&<&>Rqd#46{I~(=qtvo+cQGABOeqj z@#7_{^&c;vZfVj=D6?*ldyZ+kM}qoBZ(Io7zY=(iQ*oN~>c8qHU5&}O>$>$OPeh4q z?;@E&G$Z!-^WEmxK@xYzeXm{e_&=<~KD(*~8qb0O(R2*bx2SX$-e8*5S5S%9gxO2y zaRl~hiwmZ^Rnex~^K(*Ha@8PERb}UeQ>q0iSoMgiR7wh1ozLr0$aq$taaSoZoM%Q`gnfR?9nPOUK!r@Y52uS*UmgA(Q3{%SMO3IG@`I&++07}y!Gw% zL~?{yB;2M36sdi6V`&DZBbd2!ZE!c4uMJlGctiK9B|oP-j~^HE$6NA;l1kSJfl0|K z%xbzDp?ofqdz_|!3cb$a;>AZ5C4y{ZA2v4WcQHoEYu9?FBZKBKbrslWeET^4Zlr}= z&gRrncIJP=$B1DSRf$#}xrGJ0pe=pkwD#bMgzOrk+?8+IJ0;x5ZjzJL$kC2G?yXr$ zHE!UrWzXr`MaOBoNSY%vW4J;rBK4F(^2NkUI5Yx1V%a-~Bo>R8eV^WMflkYYJN~Cs zhSF#`dX#GL63K}z-sZGx`=IIZpu&1!z5(ySHPr9OCFbzTSD~jV?c;;P>Gp0Vrr2sfG&)r9U=9<@4Q4HbzOp8UrM99<1Zi2U=P08 zCTn~@M9a8Sl8@djh-o}c48r#u!skhrvZxs^@-7{$3Y-sf=bB(B-nSa9PT-N{_ewr* zv&&-qcywD=&>F28%-y}%A;pwJjVEC83mZ?w1O^9BoV0?!3hOspA*aCmA=6QAVfLSh zgB_{ehAZ;QBK=@)Ciz?*eVBB&A1GU~G z<8+?H<6KR&$Vpl{hySvZU<%ev*WK0n2p4F6}6BIF3)=*(c}AzdkY0Pk)#{|4x*9%&WvF6FNnJrj>3`=fll> zPqiG~&gbw=w~oEuNf?`0(<10f^eonC+@RSGI5fA)oV!ySIp95k>f<_hyci@&U0|%W zWk!N@xjkT)h_blvDX>JutDv2#NaG;QEWtf^q|vGrZyLmu$A3viIXFrrqQ~}~h}XR+ zxz^`vy-)bQ~t1nh1Af}U&r_*S`}^pt8QleHZ}pJv~pw@qLtS5C@#NkB6Q?+ZnNQGl@ppj zcNMYD<{KEL$~E9mPhhFheORo$_N}zP7m*yp$N57wGkbD4%zYgn-?$=1%O2i%*KnD+ zN5%aXDSgni_`4m^hp8|wVO%hoz>|NzF^L0@&A41^->j!*$vPXQDphw<-8pyUzYfAi z{truYriYCOPOh&q->wnzW?JZMrBGw@59l|jv0b8`zZ$G0i~i%x=Dc4HGHpRxh)$Dh ziRmvzuS}oq9wo2bP_UU5Qx{*p5_X{>sD78QEqI`<#^DvxL&9yu=IX!R;QyMk6ECWA zsm9=20R32DITqEy^6GjIOT{LF#Z);8Lp2_XwC<}zh`e;zNxD)uX~|cpJRwS5<);t7 z`&ggCw__xHP=0CdFgt6OSopIIY1vF^>H3$E;-pO_`nntv>||1w1QU66-N{9#DRH|B zDaY_qUq(NpW5oVB^j4j3_039daZ4kpb4b|cd*D^$fQRsjxzSX9{HRS+SfEkqaXr%} zNZF~)LS!;m{<#y5l)Ab(mHeBY#1*Q)Yl<$9c+^(SU$a1?P^%f;o?xv3-Ra2>eL|e4 zi5pJ8{^XQ-ckK#e^jvpu5)dxyx)KZ1dSpi_XF}4bV}7VY43Qf7B*x2B`b0my2~L^x zuIDqU*XTVz1g5XC8F{2BwuLW*rtyXBp252xR! zCVIDkyt)^DJ!m(=8_$s@lU`kX<`x!!iJuMR<+o1yT+6?PUxD>t#fB-Bps3>8P7eH% zJ6G1o+sNuU2s0X;`p>gL1r4W-Gi6)sC+AcK*6cY-$Geyt>VjT4v)qpcl=k|*1%|>c zzd2>*Q_Y-9MeXP6KMR$}6k@eTwKl~Jk{Z}AG=8!Sx_d5~C#9XuU{Lb~!(RBMt=FA} zU^)^AC%|ywN325mdSjk>E$t4T7Xu&op%GMqbXFM1*n?X+zFRTFzYF>B$tquW6iDv) zTey=mTw~Cc49DC{QWrrxcZl`j(DRK*q?L0iab@hNNF~3g_NP!%N|m{<$wm&XAf;~# zq+$vvQEt8XVJ?3W?q|X0Z0bvDlD4>3wM9HT_x;ObC+&x>pK0I-SZCvFG2#J6UsL+} zWXm{=vKp_?Dk5I_@R)J6>(8P~?EqesLr3PF`CG-uJ%MEHFR&ljR zYg?OIY0QKx#7Fl!E`)*_+8*I9UdrjLi#OlhK*%;d*3Y>!usU#bTxw~jmVdE1WWe!G zq~T5UAk^)rS@u(wrZ71T`yJ!2O8ciYO&!)Qn?^lFjb8Q#@)rYtsm;IS?DnT%BhWur z#K657t+gam+EXj?A1RfpDpMMY=4ctMT2Qh}%|uSXqH+*9-52dFM<9#!xD>wSCcH}m zPON~>b(qINnR{0`LKcJnLI#30UaN1cZu7xmm}S?H?`ruS`H#B2U)^2cO`I)SR5i=* zCSh8vLa*uWP3LF3`_sLT>8#aSbJB(FA7kpD^^yn~;lyN8-VW9)vFUcS^9c1Dqd4bL zunnqV(I@Xh-Hu?}=RQg{e1tizW?v|3ef}>o7Uv6kFXAKnafbljC0=AL2nISqyky6l zmOgqRm>Z-5FE#S%S?F!|@RuFr3yY_@KqTdNqIs{1^vHAW8k+lQ#O^s0sfds6{Y$vz z96HSllYa;Cw`Qr>VB0Yg{4%`iKm42Z740NiCqYF6?W|3bq3uSL($*~?Bim+m+Tdc6 z1%PLqMn9iZ%G{GrQ8mdvB@`5#PST8=k62p%l?E=6xmR^k4=?v3asT=O--H?}!nlT7 z0#=o7e6a&Sl!U>0{66CA&f(7vQsczW>6jT0h zqO%;JA=aav;oZhXYz5ggQ z;Cr$JrPdm+iL;U0z-h`h1&kwi?#bIP{>*46hI@N>@HW5B(%f}=&o+<@R*St5;~CQ{78N3$ns zVVrv4T(c6ZDn}{a^4~dG%+dXn^b0OnUr=^8Zw5~mL-RO0zwlH25pEi;bZUF@eHtB%l%K^s@^oCoKSer@?3`Zs0BdQDozIoa&r;P}N*>fDm?UD*gh<378ZmQ5Mmm@Q|?LoPzaWuMD%JvM_)zRw;{xL_z6$xAVX8o7HI)6=X# zF304~TQP$=;`cD>B18`AsJKz9QVBlb#+2yKir?RrE3HnH3YJ)R7B18--tLw5xXssc zY(2y-XQmBIq$T6fckKEDSZ)6M_xU+`(th8u$VO4m4mBPp!%CKgNbIXuB5f>d6K(m zyv6LV>qPJ@%N^n(EZZIJA~rEsGKu{5Il7B2PQIh$JhA-z(3*42?kQziM^KJ2<-%2X zX#GAm!1QLwJ3*9=1c|#v%li-;=YL1&o3JFhcy3(vNl$xg+JM)079 z>ilW~f>n&jE%S8%3k3#Bfr$bOeS?Vt2bCxLz_2@qa{K^7jhJOEln)55XZ-G|SL`tv z8TBJsahAa!tRwUmvp6_~bx>1zbq=3R#uhW*Lqg|jN_x-n;&-y``U(va=OBD9A$-cU zIz?qEMy^Oa;`2FT-N&F1%LL{2&y7F3J$uFMDI8M{-6TC&dj!iH>^ox9hE z<}nf`G5x%K!9lhBBI;Y4VJW*HF(YuC&gBe$(5&2gy+r!)mswHiQ~hxF{vPkZ$9v;v z%?~mYHbh;u2%|<}iboVFi;D>6#5gq)gsywYY67mUJ;eM<=Ae{d?=HJ8d6!~}ehd*d zg4gW3aniUF&E^G?!hGxEsymEbDCQe_*0E4 zZ6;|R`X{rpE7@Cr*#s_#DbXl2E3tfyc_x+K<#u%?HKqb&>J*cm|%T=RD zW%%v)dW5Dz)3AIi>Cxy@7niPZaRmECxLpjjxjeG==v6Ye3~E}PCL4EWU`N8nrmnZE zRy=R~_(zWhQJBHX)+zOYMRQX$Qb9p}8%FK35vLDwp2D51#E0sC3sWh8U0*cc+!eHF z`YJMRk9Cv#T&W^Q;>~MVYSewBPZSg1{-*8zSb1YjJzG7OAIal=siXIWdG>s?AS*J) zx0gHpK(tPxcKU96$@(q>U-EnF-$QSc=uCAo!{PU7blOLY?EOVjO>qJNT2;U^<+}e+ zEid`SbUze!oA4$nifyd&#G4bH9bJQ36UMo7t)K?RY@@v+(}JU0u(M_(t<%oHtsgEz zw^~i3g=DK@iETDCPbm(ob(E$nX?uFQmzh)e!RK~l;e2Jo21kkO6yz;Oe#nC-6M;>b z9xXTTz4U%cQnJXuY&G{PoR2|1j5qgUnnt8nuniie6$S@T86+bcaeIrliz%jxjzDfA| zzj090hgO6H(Q+oR*~DzTtQB4*Dpo9=&-E!`^p?ll{{O#tOw$AC)d?R2D=Qs;M+`b; zQ{xGluB?p?IFfp*E-rIY+9eH;8y2;4hKJK9@v6xoHO};wr5)Y0A-b?^yH1o}_>D_= z$IqE~eW(kEqUgwtOXR!OP>VV0B`=1%sKkOA=KuM&9$>K$Vz52UV_FIZ@Ik5on`P-d z6`oxghs-s~09L|~KpZ}t7xqbcqUGZ9w9X)7w(}+}QivzHuh`9jn)z{Tiht9+SfN#D z4mcL`-4C#<|JWxTNZtBc%SX z@7ofoW)&+rKPqItBbmkzPy)o^uWFLJVzsUYAtMamUW5SSS)meHmqW)~J1_2ht%rBu zpBU<8baP`6q%TboAl<|kF!9>ODy7;dw1(`6$ivWNNmz;G?!yd(PS{L-2WNZ zT+~@nR$t-#E6rpI`$+veeV2i@9%#ud_Lsed5OOUEnD+iv~r4iY@0IRe z5431D2_V^NDJOT=rphXqqefnarv!6bkQUybKwqHaMCE~?-$uQ&H^K$A(C5G9H(udW z=O4eU!hF$2*2lZq@z>N3Qnwy&uFOh(KBANA!e9-1cK3-_(dvgBu0~P?Hhc3W#N*_G zZzBu*72@%ClCRUbqM0(&cf<8$zu&Dtt>E#Z*!JM8&cBr^^M0#;HQXQ5|1rtq{)j!i z+sH>w=(b(7i+cISzvuEbhVY&H$#OM&1VW~#*u2X8)%n@#vc_;Etcox_`u%|(^s(2< z!XVFAMTqs#^PaJHu{-n2@0z=K`mTlL75dKdP%?bMa8e8rxMhpXIJ1)LOwy8@{7RCm?Msqd4wXSNG#=-NK|D^gNof$uWdaq4@|-7^Jgbn&w2ihk zR}bOEP(0OZRG-Yp>J#B>5uYQ|Tfcks`%Dmz=~;3|co+idsqv-LKU7#2-Ri7o^NnwK zGZy9XTOfn@AFfE^=3*WyPZsaG=w|7sMWy{o1>-Pe>*Zmfe zth0`C9%9!%twHZb={KOiINPDKSJ&gG*9&*+T&KEZ*K^J;K3QM8370{%6}BeA>-wgx z9~!_J2o2=?4E@Y`P7|HqHk#aiEv~>3#S_IJB^)IdB^4#-?K5k3(e>w1rB#LeHKZMJ zXP8+7Wuwb_d%BxHTgaH?eoJfn1|mTJ89Hn>$$u#N_;B*<@j%5USWMbOjb5X*jj9|q zLe9)qW-c>XP4!TacT?xwirwR@nT9JhYlm$q;nWhWzoNK)xpjtd@zbhRMm|Ts16BHC zia+@eUIh{_j4cfV0t0w;(Kwx>coq25TY{f>sKIrAY_Q@B! z{sHkj2fB9_{~ha#^R&{Rap^0emsS+lrB3?0Ptnwq5~TWIv4?c&$U@ID=z4 z8alIj2FwOg4YscEkcx28JZKwlOvT?Xy2c1kW<1q0HMo3;2h}HVf4^JuzcmvnsI&pe zFC1PYa@~NAGM>IfDYNLS-j!|LIYhcoWFB}-F?h@-tElq$!%PKW9$v&I$Z3qlKKI8t z6`1YvP&P3&Dr)Z2K?7XNtd^yZt!tJCTWC`uSM z+OMPh2I^o$2aLF}sS#x0%18dD8_{H8Uz58%!MSie5>c_EK=Je?=c}Dh1wRd*w}dDj z8z|M;msZdJD$dKlowr}O)me^}z|s^N(X>-6{q!pw*IEaH*Au#D8;07E&Z4{TLPogk z%D=GQH6;B zAAHFuIVD>4s6~t$`7WFbZxpz<{j{@By#WLi3U%7@!CfZ(-``y+0O1_HmBVFuu0(4<;L|ffLF_Y*ow)! zPqL+L0@aZ$16cj=+XWHd!iRZcD-1GpzVIg+5>91V7vCa2^h9VP2kym^ke3YN9sopvf^ttStMVa+#Em&q3)*eyC-)kTCWN%_2b>r-nWfC6cSz(j2Pvg1>l43< za0uaYNRh+2(&*JQ& zDpCZkYbvF3d>^?y1Oq@=@Sp;+;!OZrjh`v35btr+$O|u!K3{`V!r%P6Hl1VgM@ov- zhmeLmr$FC$+lq?WcbnVaJsG^d@$~J`mBE+VewWJM@vyfx2F2rS~(gW?2M6M4P4iS1mfQDCmTK?S8}= zsuv$lf+vVXb5VhxajtZF{vdeB{n5%J%8tjR{6DysO-q3Hds$C8SQY81tqc1TGv3_W zPCMLo?55-sd==PnJ^8-26Ih6izVkLrl~+YN1sy|Ks&JsZgaP#d#smV7(4IQyJOS?GQa7=2Dyckbi&`05t(01c?EK@UJFl8F7GLR1K3! zbI^oddTtA_R0!?_Whf-x;S{5#s$9)82@#H8$LpxNbq-!^u79GWV!V1Hqolj-u zF3aOWXenSfa2G(ll~^r4#9V6kE=+dzK|?=Vadl(!C#tjQHQI5`5Tx|J6JGc1C$@M= z&j`(~aNVI&aME<*%j_p;q=Iz4GFNF9EMJ^euq(iO$TiDuN-1N5@6f~`pkNZrUN-^! zq#6_0QrvZ04leGGn3*Q!F{KGPS|yZrQQ13FO##G=AL33+E0x7wu{))fbL{`?el9$` z2NU5k9BIocD1u}0EEFh6PJI_J&bf_I2atN%;gSm$|lXPj$Q3 zGv+yS=Xb&?_o?2LuN3(M;KRm33b%v$l#9>8iU?=j2`OGme25_0RAE~%%$%ONGU%4h z5l34f`)HMl+e%swt$+T;)$veKO%$xMv!HqU^*2z;SI`LVpPP(TL6q`@^04dmns*~2 z+OEkC#G3|e9lNrNSftfrZOi*LjtUkn^`Fp`2YT=U9JIv!r3O7{1vYr|=R9jxIBg%= zafq$sTQ|PZVvlFU??}gL>$9xI$uPH(*0DvusIb_S<7cc*%V;vPEt=KlM*FaOz-oF5 z%R(NUk357c+)8`^Rd*b8E;I@Oz$`0H*Vs1xV{(DH!2m6>AQLKF!{zMf<3IG?&OF;`15z9|1P`$g{$7%FNMa2~JQZjSwS+Kt3v1j_Ooj3;1k+nLR&WCnw`Mn6j)8uv zE8`&Y5jty22yM+lql17{eH@=@gvZ9x$4F|K7{LE?xIV^$9zhF&N8M|j=DB{LnEF^Z z2Aw}&m`#aG@een{=A_6v!qJOsQs3cGE*hF5`}`Kb)%Rfaf*a=}maSvRSN3BmXxjgVnIDv`(C5A-geHZk{K zQCUp}((35(h;*KYk z;V(Tp^dOM}Wczl9iU_tZWI!Xv7vlCt@uM_Un0v9wb3gl9=cUF}PB0 z|1$HF^lMy*^5uMWrO3|2hvfQ)p@r3gEPqFFgh*WB+tajJU42>{U(EKKcNF0srun zi3=I+hKhn)s>?B7>SWIp5(JZq-j-%ttddte&LI)AXuBY}3%H4%2aViCsmJ(3Hs7FP zw9v|9XGwsaTxgNdx&Az*7)M5sHlQ3cGdlxE(@~oDcw@d@puqh8UzMNB6X@R8YqiTi zd>~-$+gcVd2d;z^a|dw0T>?#;Z)yOWUShwF^%HGy1ebYGOSnp)8<7)A*`eXLrWit( z@#iPKTgC#EpSvPNTJDePEJhJ0udS5HI`{E#nk1Y73-i7+-`ksNQ*b23HzR*Vyv4Z-#cKH+Zq4r+({riYfsGA{f4iu1KAY$t0fqJsZAldaoX$*=Hj z>zT6?h4va$`yDEM3}C%xDVg$HRW{z5srM3cyOPQ4pc}& zg5PZ1Z=ng1o0jaR6Te z@{cV1KvGXV+EnZcqaE+WIyOu5W69caIo#?+0cKT>9s?fKWzSwuNZgf-gT5WpD#3c2 zRc+5I70q6v1CD;6I;Nm>t`@Y`k(=2xx<R55Ty;vU>y-jP35=DrSIeId4Qw^NeNL}{?MvpM*ryXV^e zx&$64)J>`8^VL^E;Bs|XOcLg4RuWDjdM&A$UUjHA-O*|*ZUa+;hr)M&nc?Mx+~p|2 ziC^?Gp}1Q$Y6|tt=;=w~33Hqb0Lt?rHY4bMU)|hB&e}T%#7AqxG??B8g577qX^O{; z7nFmQcI2xHOpa=r$qB7hAw{!ld4Gg8d;UC#|4 zAeUjmcrriw%^_g}tAOGzLgumS89(bEb?WnOLi55fP7!m*z5}ZyqjInqjn-wTIS8`& z!6x*Pz~h-Y5^pu}7JeoTgZTYYtcY9ti%8rDuayM8psU~EeP;nhQ7qZg&{}mQo~G{Q z-8L@(y8z&5Jlrki-bw2MQJmXm)lK}(%hfI}`d`QV3g%Crp?#ipC4$*yX2md!Ku6Fy zrQIOR%r`5=GBt>>hQ>_xA5e9|u&0qM!m<|C>oyKy;~K{O{zxn3XQkW+_}F=AA>hj*A}-AVI=zDE(2rq6jTn>Z%Vu|rK|MgN|EO|Ek*+3zS4U?*US zA*4xFQ$&lY4pN5d@O_L?QMhZtPSm^j4p{f@v~S~WsRIpoDQ7b8@X7ui?Fd<(@y>{P zoV0nWr3=1qXs*c0h1g9=Sz^Rk%D!>O+MhML7Hsx5zJyMr_m3p`DvTK5#LG$}-vTr7Gw!F9neaxhUE9Gs~5{VBVx-F=TALIrOIIuGEF*Vu;)*ep5~<5aKh%YVt`n z4KB3zJD3`r3HzZuZ4sPHlz&$~g8BjNL}FV^QtrFk+j`VA#N$M>gx?Q66lHiE`k>s`)aJv5P&}LYuCg zP0?_L2(=#;Q_gtTl?R%b>QizNBbYl#{y&I5W8z=Q>R(M2Dkpz&Ij7KnlpbsAr5MWX zjtzEO>|}j{egB8%V2DRf;%7vIZ>;EG5q4?v1bZ6^9<$p#^_^q!P^|9vVxpX|>{NT< z;(Wb9#`;s_QQ4Q;Tn*|h8IaS*yw8N1A!rH#2De^Sgz#}Z*3E|e-KJeR4lwn5zvzPs zhGjQyunu|*#f)S7Y+iF%y2H_k_o?B@ukI`XlYp)W;isct3S`;zol)z@)5&sTLOHlas4T~9 z9Vo8wqF!dBXIds}%r^!@;>8e}r3>kaB^NLp9A3Irl=kj1Q^Th*`@ms&d%x#lzLp z2_Hu(eVC3;x+1uCfEcA=9EvaeJmjhPOp`8*KyCnNr~Z~wzBZSNU#;QZQ`af}5mxRd z-l!EvXt^HA3+HPc*O^IJ%S_0^CU+7JAD%%#XK6ECQ+@61rG_fRM2XG9kif~#jjnEc z4;)04m#@y)9arg?k#UpafIP|D1C~E7drc!^pPa;N35(%X&c`7AZyXoVX8M+YazH!t z85>U9Rb4YB5hTo`G*Z4}V;{-x?>9Y%j-jWNT5B1OgYpIQkiGfBZEV z2o*<=DorsNzZY2@&<=S8uq-T&%1x-?YW3+n`2UKZt)I?lC9R@k!9a_F%Bn$ z1)1WT>gJrLP?%;m+<|}{CEhG$JZ>)22ZF3kg}eZ$u|>#erv7MV?H&kdQQixd0yjB6m6 zP+BDut3a7Ukz>m}Y#s->lMZJ@x)eFuILxMVGibt=CCUWboKy?#N$S95vL@zQ#D!yq zHQh^638g?1*p_4pDxTn+U2S;}I%{8<>R&wRRKEnsy1J-$7CCh4qx&|dU#dG>I4@am zyBt+{IqtsbcyYWpRg_~Bl&?yGH>2droJP@A`Eh$#(2y{lWmv$k*U8p?L0?iwBLUep zH`2K}y*vj<4)z5QBK1s!vpoIc8*^m1w-m)gDGqtQNieVQg544kTsR+bq2IBRl*}@M z_Qe6*+$YJzk5Hgzp8Gh}5LCG^d;XWig=oIhQy-`_7^fpd( z=N7iI7q_HCwys91QkHj&9%)Bd4EChk`# z6L)-~2YZvUOOF~nY)H7)D&qQ7$OnAn6lz&0B%(1}1~6_h-qvG`e5o&DE5TcTN|(0t zK(YDWBB^5-DuH0)S#At}~ksMYGYM5T-GGsm@R_FgVVTG#Kr=qMDk zXPy08G?aGx{jK98lTOH(6n_`RnLVJ!#!RmVG2)f5X-;uuJ1@G*s^^z*tna+QP%dQ{ zY^Nd;NNGQApw#Ys+h2r@n!B!!v4i-+x83{vF#*K%@nYSspjidVecqLGrGA<*Uyy=z#{08?sb(^E=H9(I050aHb)< zp5F~|%#t6c)CaS}<#x6NO~Sg0{Y?@QnE`7+YgVIAin~Z;RFQ7!DmTFBu}&UJb=XY& zG$fi*EdYOWLM2V1UC7+^PA%$&FM^BT1?KMu`uj@VmE6wGF%4)~2>dQ@U5+%P>_AP^ z;-5bW5b~$xo$G8Ax-X!V%`l9SN01#ss;oG(JLQ~8OGlTv8$^o7%rZHW5Pw1sR7E6U zul%_km}!@&5eoA$81XLLOSm~>!NhJ_FN;IV)fv{yR3ngzFXdizVV+SZ*)K-jc877g z6#^MG14mYkQfF+`YId|`9bo=wT^tY=naW~kD1j{)0P!DFy8_kadC}c6)lT#UAE`54 zU)6=aaCR2$^+F4Uy@4Q21^2xv3GPDX4dOt_Ql`N@+S#Ou6;^`4r=Q6%Frb@8G|PXK zxKs^;je6gLb_zi|c75-ugLB@VJo6kXxkKp7nK8yJS$%TCJCW#Fv?ZC@p3*8(S4i^- zH`BO(GA}SCAYB8z{trW0jV9*FCwmx!#)jkgOZRImn}huts1hr;&oKWBoN(gvs}&_7 zetc@TY099kh?gb})hKoHXdgre(c3TtU`Q(CewS%@Jo5i-{GF9zT2Ic;~Thg~Tt{@|i zPymUEb{RIsGGK=Ctz5BkCAO4XhIJ9MiVu)Ucs+v?arqX$$GN^(k0NC$);vgM+_sVd zT76U(hUOLq+-%A4{QLqski!3}-4#4ir7NR$i4zG4vU~}z<wpYOXw0yO zKkCn*vi*WLeoFSM!RI9dFP{WZn-^WWJWYpL_b0-iKaKeNPdLbiueIrepKs+2ph$+- zFj)q?u2TyfwODs)jvDgZI=6!A`XwX)IaN?0@U>b3L{^juHmW5IZKx1-B#XzUi?9<< z^Q;Kyl6Uh|mzNyaatPgVJp&3c&#+I1Jr)|(fZlul*Npx=pt&|q{uQiE5ZTM1xmVei zEEW=^87U~upX^ZXtbkCO!jU{3lNSy3&bW*?E<-Ex)%YEndZh5xZ_d#SeOtQU2RqO4 z4+8m3DDCQeqieui&yG~cHEM=x-%C5(I)%~utLZug=<}GT@xJZBiHJ*>+t?T_mXE5- zcAoN?1%akjL&(?EEVwFN`kW{8t6w#&L_bEA;$CQdkK&*py-9{gaaFlHng7Z)unBbK z2a;m;&xT;<(qgKaPnMtlHvbGh{AL)f6I7+kllxjT(;djf5UxtNeDRf`Pd{~IoUO0n zP>`Hk{oA)ilrHd#qZ@&ZByBx0qSid*bzQM_!i(*6?l!4x+d;EUDjkCV>W~ttsjH7w z0J%ya5z~umoXtE$j!U1LCkJ$2b~?^F0yc&*pr;3o`}V5?yhW$$mSFQ!_^y^}H(I9~ zIn-=@dc~PJaAYxPrX(v1E&tuHn_l}*SydB7$O8!P9JoAhm4Orv37?CK4jj;yjK2%9 zMrX#Zvu_0Xe!hGi99oqQ{mPt|wH7%NbugE4f%sLpcDnd2|FAkL{k zJ+R9Z>4Yl-<;edWD#Yg%lS(t|(WLo+kbGw_5H1cC5RzJA#!^So<+3>kj)-DVnwk8 zFPGQ>>@(XrS02{o%W9d<&V1^ZxKv-#3U~`>yM|-cR`%fs1^WF1`wkz4DIU%AiPtl8 zH}aB?NaJ0HcVvmbp6PV4XtQi@W!b?t-?fHHww;Vi3YO%(hvjBzEaZOty!9z{#qP5+ z>Q<_6A5^#0L5oDck!IVH9wc#o$G%?3B{cOm=1Zki#x=Oy$N%_v<$II99)``KTWXE< z`#hW6|K-O~f~BfEBYxPEz~NB}ET>ik2KdC)GI*gE7|Oe+Kt)Yl_c4@a9GR`IMh__wmO z{@pYH=sVD)&y&bbvc*{)zlWM;s#2{VKwz_;k`U3y_J%!ez*;&3=!fC=>J z@p%4e$G8_OHcYQhmBnrs*U3xRrpD8&H(lUcoZ*LuK31!vBmD>KdX$l z37Fs#nO_TBm{Fr^h;X@XLW8P<&F^>jxrJ$jH5*#BTpGxnf-F1mpa% zL~%gIPitj))N~~^dbJ_R2+!E>g6C}jxm!C{q*h(~bO%8v>pffvUs$_XuGC(!^Rr)A z{J&k#cp3=|*OmJZqMScD!vIxZ!(5uBkA^@FG|65$_sxkAT*OB#Zt*}_evlIytTv_+ zmCA3tF8~rbUM^vS_m42E@KXJxmg|kU^ao0OJhPGQS)Ii`ooFotoX)=?zb~EHgsoB& zO?kSLyQk|^V1VR%6>jM7Y;Hl@{Zo%nwGofx^3*m84b57==T3>%Zdf;;rTe;gVny!) zudl0zyHrFU2N(%TiLlEnI3OZ_&z(;QqB&XoddXj(TI0U6<#2P|bFN#F`jeP_ekJX% zC@=18$omE_($yvVeF)HmRp%-_G!Jth&pH~<(h`w*p&K{SP?@yN=hvwF!?nQUWJ9NG z%h{hOO1OUzL^5&)U{J-Q{4q=x(ZKPx)wdz__K6e ztTDw2ch;f%D|w~{R6Dxt+M2lR(RXWt2MoARb6HDc1P}CtD}f#z+=jjU_@=0W9VuO= zP;f(eQdfQHGA@Oh|K0gh;E;^l4WotXm*rZm{(bTInGbP3iPb5JC;5LCF^zpLey})q znTiph5MV#%K?6_<;<>iM2QojePxJ4VsfaHYjSR+G3mp~^E=LpO5|j{BUyfJ+lR$)* z8uK3HYFdj*z#vw4;sZ}H^>68^9@TN0saGtF6Qtg+o6NI+@9VHgU4P?=cv>=63{MPy zCVks+h0eO}&*Y!e)@o?r0^s>1gWU>Ga;)yPgR4Zf;Q`4er*~0==a+Y}60T(bbCt-i9yIfbMxH2w^J)F5r3Z8A0UpahD279Fz~Qu<_de#- z@77aa%U_=r3|9Mfvd`hYY)8awaP z4DbP(`Au;dIUTR>m~l~3QW@X+WU*KgrMN~u-)ZrB&SezXP`zd{1-66qcbX|yUBM<4 zlz2lk;f$UGW$(Co%hrnB_dhHb-Td#9&%}2>{2*7O@}#lugnf;Dj(z{pC!fkiF1zo0 zh>pz~d5mzZkY9>pr#hwzXe?k%n7E5@z^z+lZclD!Jd4H{!dSA06pO`TMXsbNHu2z!4lf97B05QK86VPL^pmY2LrcTAy6(n#E$VDk(0plxB-b z1?Ow2K6irdE2cm?&#hbgaSP2hMp^3ZxQr2C_JYz}t#XQFi=J#U$nGO1h~d-cirGt7 ziPhV7$ps!yzVLVP${YU{@4o+`oE3fj?RVmbLx}p*<9mmizH9~n)Tp<_kq*ePPj@IO{~~_mjr*uf5X4wHFz!FgZJVy@LBjwe0J%W$@07Ko%nA22K*NM zCj2)1M*LR%X4Uqb5ca;rKK&f~nD+OhZo|8h{@ZXZ)k-51Y=mRN#>Zl@SoI(jRmStC z#wl5sWAROb;WcGazhSPR8RG+ZK1G50fAr_K^xsbCw(7|sXPjxdaz!N!3Lu#r{qurw z{!K!!#d}U_#&%*jw>M?+YT9SktJsIca$+?0Ef%*RB?kMP3tF>SELM0b4mBqcsIe5k ziDAW45KnP?tX^)9Ih}(0dIJ6FULnw43eO#XpuZ!9qfSk-o2;X2Whs9ZJej2nnP*3^Ab|M?XfSgPp-}Eakibq$E!J6EEX%46o*@Uc)rz~ zOcIMcnx=WLa;N2LT54_wG6|o{F#EaZq)t zyxmH(CG@(}va)^$Q<2XlQ>?4C46`wwPsV%9cCJ)QCo{^%Y>N!3t7iCmyL4-iOE*W5 zQH^$c%yGm-GO)>HUQ;FXclRWz;CgC pQ}S8(jrMI!p4G8f$5w*|{|}Z@(1*gUIYIyc002ovPDHLkV1md+=ym`A literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index c0bab4c..1b997bf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,6 @@ +

+ +

Watchtower

@@ -29,12 +32,12 @@ Codacy Badge - - Join the chat at https://gitter.im/containrrr/watchtower - All Contributors + + Pulls from DockerHub +

## Quick Start @@ -46,4 +49,4 @@ $ docker run -d \ --name watchtower \ -v /var/run/docker.sock:/var/run/docker.sock \ containrrr/watchtower -``` \ No newline at end of file +``` diff --git a/docs/stylesheets/theme.css b/docs/stylesheets/theme.css new file mode 100644 index 0000000..34f507d --- /dev/null +++ b/docs/stylesheets/theme.css @@ -0,0 +1,16 @@ +[data-md-color-scheme="containrrr"] { + --md-primary-fg-color: #406170; + --md-primary-fg-color--light:#acbfc7; + --md-primary-fg-color--dark: #003343; + --md-accent-fg-color: #003343; + --md-accent-fg-color--transparent: #00334310; +} + +.md-header-nav__button.md-logo { + padding: 0; +} + +.md-header-nav__button.md-logo img { + width: 1.6rem; + height: 1.6rem; +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 696f87d..1d1506b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,10 +3,17 @@ site_url: http://containrrr.github.io/watchtower/ repo_url: https://github.com/containrrr/watchtower/ theme: name: 'material' + palette: + scheme: containrrr + logo: images/logo-450px.png + favicon: images/favicon.ico +extra_css: + - stylesheets/theme.css markdown_extensions: - toc: permalink: True separator: "_" + - codehilite nav: - 'Home': 'index.md' - 'Introduction': 'introduction.md' From 9b998fb751c8a8b716c0d1e6dc2c16da2c3e118c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 20 Dec 2020 18:24:16 +0100 Subject: [PATCH 12/98] Fix syntax highlight and typo in docs (#712) --- docs/arguments.md | 2 +- docs/http-api-mode.md | 2 +- docs/running-multiple-instances.md | 2 +- docs/usage-overview.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/arguments.md b/docs/arguments.md index 62e73ad..a36c438 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -39,7 +39,7 @@ Environment Variable: N/A ## Time Zone Sets the time zone to be used by WatchTower's logs and the optional Cron scheduling argument (--schedule). If this environment variable is not set, Watchtower will use the default time zone: UTC. -To find out the right value, see [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), find your location and use the value in _TZ Database Name_, e.g _Europe/Rome_. The timezome can alternatively be set by volume mounting your hosts /etc/timezone file. `-v /etc/timezone:/etc/timezone:ro` +To find out the right value, see [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), find your location and use the value in _TZ Database Name_, e.g _Europe/Rome_. The timezone can alternatively be set by volume mounting your hosts /etc/timezone file. `-v /etc/timezone:/etc/timezone:ro` ``` Argument: N/A diff --git a/docs/http-api-mode.md b/docs/http-api-mode.md index 7d14d09..def90d5 100644 --- a/docs/http-api-mode.md +++ b/docs/http-api-mode.md @@ -6,7 +6,7 @@ Watchtower provides an HTTP API mode that enables an HTTP endpoint that can be r To enable this mode, use the flag `--http-api`. For example, in a Docker Compose config file: -```json +```yaml version: '3' services: diff --git a/docs/running-multiple-instances.md b/docs/running-multiple-instances.md index 82cd955..641f4e4 100644 --- a/docs/running-multiple-instances.md +++ b/docs/running-multiple-instances.md @@ -8,7 +8,7 @@ To define an instance monitoring scope, use the `--scope` argument or the `WATCH For example, in a Docker Compose config file: -```json +```yaml version: '3' services: diff --git a/docs/usage-overview.md b/docs/usage-overview.md index 2fa8fe7..b5737c3 100644 --- a/docs/usage-overview.md +++ b/docs/usage-overview.md @@ -41,7 +41,7 @@ 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 5 minutes. -```json +```yaml version: "3" services: cavo: From cbe9ab87fa03d284ae1935eabf4d357c3de3abfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 20 Dec 2020 19:23:49 +0100 Subject: [PATCH 13/98] move secret value "credentials" to trace log (#707) --- pkg/registry/auth/auth.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/registry/auth/auth.go b/pkg/registry/auth/auth.go index 1596aca..3b3d088 100644 --- a/pkg/registry/auth/auth.go +++ b/pkg/registry/auth/auth.go @@ -89,7 +89,8 @@ func GetBearerHeader(challenge string, img string, err error, registryAuth strin } if registryAuth != "" { - logrus.WithField("credentials", registryAuth).Debug("Credentials found.") + logrus.Debug("Credentials found.") + logrus.Tracef("Credentials: %v", registryAuth) r.Header.Add("Authorization", fmt.Sprintf("Basic %s", registryAuth)) } else { logrus.Debug("No credentials found.") From ff8cb884a0852ce35a18f59802826f669c740236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Mon, 21 Dec 2020 15:17:45 +0100 Subject: [PATCH 14/98] feat(config): swap viper and cobra for config (#684) --- cmd/root.go | 136 ++++++++------------ internal/flags/config.go | 31 +++++ internal/flags/flags.go | 191 ++++++++++++----------------- internal/flags/flags_test.go | 19 +-- pkg/container/client.go | 13 +- pkg/notifications/email.go | 26 ++-- pkg/notifications/gotify.go | 11 +- pkg/notifications/msteams.go | 17 ++- pkg/notifications/notifier.go | 7 +- pkg/notifications/shoutrrr.go | 13 +- pkg/notifications/shoutrrr_test.go | 6 +- pkg/notifications/slack.go | 14 +-- 12 files changed, 229 insertions(+), 255 deletions(-) create mode 100644 internal/flags/config.go diff --git a/cmd/root.go b/cmd/root.go index 1e61308..d0fa413 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,9 +1,10 @@ package cmd import ( + "fmt" + "github.com/spf13/viper" "os" "os/signal" - "strconv" "syscall" "time" @@ -21,17 +22,9 @@ import ( ) var ( - client container.Client - scheduleSpec string - cleanup bool - noRestart bool - monitorOnly bool - enableLabel bool - notifier *notifications.Notifier - timeout time.Duration - lifecycleHooks bool - rollingRestart bool - scope string + client container.Client + notifier *notifications.Notifier + c flags.WatchConfig ) var rootCmd = &cobra.Command{ @@ -46,10 +39,11 @@ More information available at https://github.com/containrrr/watchtower/. } func init() { - flags.SetDefaults() flags.RegisterDockerFlags(rootCmd) flags.RegisterSystemFlags(rootCmd) flags.RegisterNotificationFlags(rootCmd) + flags.SetEnvBindings() + flags.BindViperFlags(rootCmd) } // Execute the root func and exit in case of errors @@ -60,10 +54,10 @@ func Execute() { } // PreRun is a lifecycle hook that runs before the command is executed. -func PreRun(cmd *cobra.Command, args []string) { - f := cmd.PersistentFlags() +func PreRun(cmd *cobra.Command, _ []string) { - if enabled, _ := f.GetBool("no-color"); enabled { + // First apply all the settings that affect the output + if viper.GetBool("no-color") { log.SetFormatter(&log.TextFormatter{ DisableColors: true, }) @@ -74,75 +68,55 @@ func PreRun(cmd *cobra.Command, args []string) { }) } - if enabled, _ := f.GetBool("debug"); enabled { + if viper.GetBool("debug") { log.SetLevel(log.DebugLevel) } - if enabled, _ := f.GetBool("trace"); enabled { + if viper.GetBool("trace") { log.SetLevel(log.TraceLevel) } - pollingSet := f.Changed("interval") - schedule, _ := f.GetString("schedule") - cronLen := len(schedule) + interval := viper.GetInt("interval") - if pollingSet && cronLen > 0 { - log.Fatal("Only schedule or interval can be defined, not both.") - } else if cronLen > 0 { - scheduleSpec, _ = f.GetString("schedule") - } else { - interval, _ := f.GetInt("interval") - scheduleSpec = "@every " + strconv.Itoa(interval) + "s" + // If empty, set schedule using interval helper value + if viper.GetString("schedule") == "" { + viper.Set("schedule", fmt.Sprintf("@every %ds", interval)) + } else if interval != flags.DefaultInterval { + log.Fatal("only schedule or interval can be defined, not both") } - flags.GetSecretsFromFiles(cmd) - cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd) + // Then load the rest of the settings + err := viper.Unmarshal(&c) + if err != nil { + log.Fatalf("unable to decode into struct, %v", err) + } - if timeout < 0 { + flags.GetSecretsFromFiles() + + if c.Timeout <= 0 { log.Fatal("Please specify a positive value for timeout value.") } - enableLabel, _ = f.GetBool("label-enable") - lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks") - rollingRestart, _ = f.GetBool("rolling-restart") - scope, _ = f.GetString("scope") + log.Debugf("Using scope %v", c.Scope) - log.Debug(scope) - - // configure environment vars for client - err := flags.EnvConfig(cmd) - if err != nil { - log.Fatal(err) + if err = flags.EnvConfig(); err != nil { + log.Fatalf("failed to setup environment variables: %v", err) } - noPull, _ := f.GetBool("no-pull") - includeStopped, _ := f.GetBool("include-stopped") - includeRestarting, _ := f.GetBool("include-restarting") - reviveStopped, _ := f.GetBool("revive-stopped") - removeVolumes, _ := f.GetBool("remove-volumes") - - if monitorOnly && noPull { + if c.MonitorOnly && c.NoPull { log.Warn("Using `WATCHTOWER_NO_PULL` and `WATCHTOWER_MONITOR_ONLY` simultaneously might lead to no action being taken at all. If this is intentional, you may safely ignore this message.") } - client = container.NewClient( - !noPull, - includeStopped, - reviveStopped, - removeVolumes, - includeRestarting, - ) + client = container.NewClient(&c) notifier = notifications.NewNotifier(cmd) } // Run is the main execution flow of the command -func Run(c *cobra.Command, names []string) { - filter := filters.BuildFilter(names, enableLabel, scope) - runOnce, _ := c.PersistentFlags().GetBool("run-once") - httpAPI, _ := c.PersistentFlags().GetBool("http-api") +func Run(_ *cobra.Command, names []string) { + filter := filters.BuildFilter(names, c.EnableLabel, c.Scope) - if runOnce { - if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage { + if c.RunOnce { + if !c.NoStartupMessage { log.Info("Running a one time update.") } runUpdatesWithNotifications(filter) @@ -151,14 +125,12 @@ func Run(c *cobra.Command, names []string) { return } - if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup, scope); err != nil { + if err := actions.CheckForMultipleWatchtowerInstances(client, c.Cleanup, c.Scope); err != nil { log.Fatal(err) } - if httpAPI { - apiToken, _ := c.PersistentFlags().GetString("http-api-token") - - if err := api.SetupHTTPUpdates(apiToken, func() { runUpdatesWithNotifications(filter) }); err != nil { + if c.HTTPAPI { + if err := api.SetupHTTPUpdates(c.HTTPAPIToken, func() { runUpdatesWithNotifications(filter) }); err != nil { log.Fatal(err) os.Exit(1) } @@ -166,20 +138,20 @@ func Run(c *cobra.Command, names []string) { api.WaitForHTTPUpdates() } - if err := runUpgradesOnSchedule(c, filter); err != nil { + if err := runUpgradesOnSchedule(filter); err != nil { log.Error(err) } os.Exit(1) } -func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { +func runUpgradesOnSchedule(filter t.Filter) error { tryLockSem := make(chan bool, 1) tryLockSem <- true - cron := cron.New() - err := cron.AddFunc( - scheduleSpec, + runner := cron.New() + err := runner.AddFunc( + viper.GetString("schedule"), func() { select { case v := <-tryLockSem: @@ -189,7 +161,7 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { log.Debug("Skipped another update already running.") } - nextRuns := cron.Entries() + nextRuns := runner.Entries() if len(nextRuns) > 0 { log.Debug("Scheduled next run: " + nextRuns[0].Next.String()) } @@ -199,11 +171,11 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { return err } - if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage { - log.Info("Starting Watchtower and scheduling first run: " + cron.Entries()[0].Schedule.Next(time.Now()).String()) + if !viper.GetBool("no-startup-message") { + log.Info("Starting Watchtower and scheduling first run: " + runner.Entries()[0].Schedule.Next(time.Now()).String()) } - cron.Start() + runner.Start() // Graceful shut-down on SIGINT/SIGTERM interrupt := make(chan os.Signal, 1) @@ -211,7 +183,7 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { signal.Notify(interrupt, syscall.SIGTERM) <-interrupt - cron.Stop() + runner.Stop() log.Info("Waiting for running update to be finished...") <-tryLockSem return nil @@ -221,12 +193,12 @@ func runUpdatesWithNotifications(filter t.Filter) { notifier.StartNotification() updateParams := t.UpdateParams{ Filter: filter, - Cleanup: cleanup, - NoRestart: noRestart, - Timeout: timeout, - MonitorOnly: monitorOnly, - LifecycleHooks: lifecycleHooks, - RollingRestart: rollingRestart, + Cleanup: c.Cleanup, + NoRestart: c.NoRestart, + Timeout: c.Timeout, + MonitorOnly: c.MonitorOnly, + LifecycleHooks: c.LifecycleHooks, + RollingRestart: c.RollingRestart, } err := actions.Update(client, updateParams) if err != nil { diff --git a/internal/flags/config.go b/internal/flags/config.go new file mode 100644 index 0000000..ef0a40f --- /dev/null +++ b/internal/flags/config.go @@ -0,0 +1,31 @@ +package flags + +import ( + "time" +) + +// WatchConfig is the global watchtower configuration created from flags and environment variables +type WatchConfig struct { + Interval int + Schedule string + NoPull bool `mapstructure:"no-pull"` + NoRestart bool `mapstructure:"no-restart"` + NoStartupMessage bool `mapstructure:"no-startup-message"` + Cleanup bool + RemoveVolumes bool `mapstructure:"remove-volumes"` + EnableLabel bool `mapstructure:"label-enable"` + Debug bool + Trace bool + MonitorOnly bool `mapstructure:"monitor-only"` + RunOnce bool `mapstructure:"run-once"` + IncludeStopped bool `mapstructure:"include-stopped"` + IncludeRestarting bool `mapstructure:"include-restarting"` + ReviveStopped bool `mapstructure:"revive-stopped"` + LifecycleHooks bool `mapstructure:"enable-lifecycle-hooks"` + RollingRestart bool `mapstructure:"rolling-restart"` + HTTPAPI bool `mapstructure:"http-api"` + HTTPAPIToken string `mapstructure:"http-api-token"` + Timeout time.Duration `mapstructure:"stop-timeout"` + Scope string + NoColor bool `mapstructure:"no-color"` +} diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 2f7a89f..2e37fb6 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -8,7 +8,6 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -16,12 +15,15 @@ import ( // use watchtower const DockerAPIMinVersion string = "1.25" +// DefaultInterval is the default time between the start of update checks +const DefaultInterval = int(time.Hour * 24 / time.Second) + // 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", "unix:///var/run/docker.sock", "daemon socket to connect to") + flags.BoolP("tlsverify", "v", false, "use TLS and verify the remote") + flags.StringP("api-version", "a", DockerAPIMinVersion, "api version to use by docker client") } // RegisterSystemFlags that are used by watchtower to modify the program flow @@ -30,126 +32,126 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { flags.IntP( "interval", "i", - viper.GetInt("WATCHTOWER_POLL_INTERVAL"), + DefaultInterval, "poll interval (in seconds)") flags.StringP( "schedule", "s", - viper.GetString("WATCHTOWER_SCHEDULE"), + "", "the cron expression which defines when to update") flags.DurationP( "stop-timeout", "t", - viper.GetDuration("WATCHTOWER_TIMEOUT"), + time.Second*10, "timeout before a container is forcefully stopped") flags.BoolP( "no-pull", "", - viper.GetBool("WATCHTOWER_NO_PULL"), + false, "do not pull any new images") flags.BoolP( "no-restart", "", - viper.GetBool("WATCHTOWER_NO_RESTART"), + false, "do not restart any containers") flags.BoolP( "no-startup-message", "", - viper.GetBool("WATCHTOWER_NO_STARTUP_MESSAGE"), + false, "Prevents watchtower from sending a startup message") flags.BoolP( "cleanup", "c", - viper.GetBool("WATCHTOWER_CLEANUP"), + false, "remove previously used images after updating") flags.BoolP( "remove-volumes", "", - viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"), + false, "remove attached volumes before updating") flags.BoolP( "label-enable", "e", - viper.GetBool("WATCHTOWER_LABEL_ENABLE"), + false, "watch containers where the com.centurylinklabs.watchtower.enable label is true") flags.BoolP( "debug", "d", - viper.GetBool("WATCHTOWER_DEBUG"), + false, "enable debug mode with verbose logging") flags.BoolP( "trace", "", - viper.GetBool("WATCHTOWER_TRACE"), + false, "enable trace mode with very verbose logging - caution, exposes credentials") flags.BoolP( "monitor-only", "m", - viper.GetBool("WATCHTOWER_MONITOR_ONLY"), + false, "Will only monitor for new images, not update the containers") flags.BoolP( "run-once", "R", - viper.GetBool("WATCHTOWER_RUN_ONCE"), + false, "Run once now and exit") flags.BoolP( "include-stopped", "S", - viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"), + false, "Will also include created and exited containers") flags.BoolP( "revive-stopped", "", - viper.GetBool("WATCHTOWER_REVIVE_STOPPED"), + false, "Will also start stopped containers that were updated, if include-stopped is active") flags.BoolP( "enable-lifecycle-hooks", "", - viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"), + false, "Enable the execution of commands triggered by pre- and post-update lifecycle hooks") flags.BoolP( "rolling-restart", "", - viper.GetBool("WATCHTOWER_ROLLING_RESTART"), + false, "Restart containers one at a time") flags.BoolP( "http-api", "", - viper.GetBool("WATCHTOWER_HTTP_API"), + false, "Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request") flags.StringP( "http-api-token", "", - viper.GetString("WATCHTOWER_HTTP_API_TOKEN"), + "", "Sets an authentication token to HTTP API requests.") // https://no-color.org/ flags.BoolP( "no-color", "", - viper.IsSet("NO_COLOR"), + false, "Disable ANSI color escape codes in log output") flags.StringP( "scope", "", - viper.GetString("WATCHTOWER_SCOPE"), + "", "Defines a monitoring scope for the Watchtower instance.") } @@ -160,178 +162,177 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) { flags.StringSliceP( "notifications", "n", - viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"), + []string{}, " notification types to send (valid: email, slack, msteams, gotify, shoutrrr)") flags.StringP( "notifications-level", "", - viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"), + "info", "The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug") flags.StringP( "notification-email-from", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"), + "", "Address to send notification emails from") flags.StringP( "notification-email-to", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"), + "", "Address to send notification emails to") flags.IntP( "notification-email-delay", "", - viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"), + 0, "Delay before sending notifications, expressed in seconds") flags.StringP( "notification-email-server", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"), + "", "SMTP server to send notification emails through") flags.IntP( "notification-email-server-port", "", - viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"), + 25, "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"), + false, `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"), + "", "SMTP server user for sending notifications") flags.StringP( "notification-email-server-password", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"), + "", "SMTP server password for sending notifications") flags.StringP( "notification-email-subjecttag", "", - viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"), + "", "Subject prefix tag for notifications via mail") flags.StringP( "notification-slack-hook-url", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"), + "", "The Slack Hook URL to send notifications to") flags.StringP( "notification-slack-identifier", "", - viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"), + "watchtower", "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"), + "", "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"), + "", "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"), + "", "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"), + "", "The MSTeams WebHook URL to send notifications to") flags.BoolP( "notification-msteams-data", "", - viper.GetBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"), + false, "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"), + "", "The Gotify URL to send notifications to") flags.StringP( "notification-gotify-token", "", - viper.GetString("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"), + false, `Controls whether watchtower verifies the Gotify server's certificate chain and host name. Should only be used for testing.`) flags.StringP( "notification-template", "", - viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"), + "", "The shoutrrr text/template for the messages") flags.StringArrayP( "notification-url", "", - viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"), + []string{}, "The shoutrrr URL to send notifications to") } -// SetDefaults provides default values for environment variables -func SetDefaults() { - day := (time.Hour * 24).Seconds() +// SetEnvBindings binds environment variables to their corresponding config keys +func SetEnvBindings() { + if err := viper.BindEnv("host", "DOCKER_HOST"); err != nil { + log.Fatalf("failed to bind env DOCKER_HOST: %v", err) + } + if err := viper.BindEnv("tlsverify", "DOCKER_TLS_VERIFY"); err != nil { + log.Fatalf("failed to bind env DOCKER_TLS_VERIFY: %v", err) + } + if err := viper.BindEnv("api-version", "DOCKER_API_VERSION"); err != nil { + log.Fatalf("failed to bind env DOCKER_API_VERSION: %v", err) + } + viper.SetEnvPrefix("WATCHTOWER") + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) viper.AutomaticEnv() - viper.SetDefault("DOCKER_HOST", "unix:///var/run/docker.sock") - viper.SetDefault("DOCKER_API_VERSION", DockerAPIMinVersion) - viper.SetDefault("WATCHTOWER_POLL_INTERVAL", day) - viper.SetDefault("WATCHTOWER_TIMEOUT", time.Second*10) - viper.SetDefault("WATCHTOWER_NOTIFICATIONS", []string{}) - viper.SetDefault("WATCHTOWER_NOTIFICATIONS_LEVEL", "info") - viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT", 25) - viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "") - viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower") +} + +// BindViperFlags binds the cmd PFlags to the viper configuration +func BindViperFlags(cmd *cobra.Command) { + if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { + log.Fatalf("failed to bind flags: %v", err) + } } // EnvConfig translates the command-line options into environment variables // that will initialize the api client -func EnvConfig(cmd *cobra.Command) error { +func EnvConfig() error { var err error - var host string var tls bool var version string - flags := cmd.PersistentFlags() - - if host, err = flags.GetString("host"); err != nil { - return err - } - if tls, err = flags.GetBool("tlsverify"); err != nil { - return err - } - if version, err = flags.GetString("api-version"); err != nil { - return err - } + host := viper.GetString("host") + tls = viper.GetBool("tlsverify") + version = viper.GetString("api-version") if err = setEnvOptStr("DOCKER_HOST", host); err != nil { return err } @@ -344,32 +345,6 @@ func EnvConfig(cmd *cobra.Command) error { return nil } -// ReadFlags reads common flags used in the main program flow of watchtower -func ReadFlags(cmd *cobra.Command) (bool, bool, bool, time.Duration) { - flags := cmd.PersistentFlags() - - var err error - var cleanup bool - var noRestart bool - var monitorOnly bool - var timeout time.Duration - - if cleanup, err = flags.GetBool("cleanup"); err != nil { - log.Fatal(err) - } - if noRestart, err = flags.GetBool("no-restart"); err != nil { - log.Fatal(err) - } - if monitorOnly, err = flags.GetBool("monitor-only"); err != nil { - log.Fatal(err) - } - if timeout, err = flags.GetDuration("stop-timeout"); err != nil { - log.Fatal(err) - } - - return cleanup, noRestart, monitorOnly, timeout -} - func setEnvOptStr(env string, opt string) error { if opt == "" || opt == os.Getenv(env) { return nil @@ -390,9 +365,7 @@ func setEnvOptBool(env string, opt bool) error { // GetSecretsFromFiles checks if passwords/tokens/webhooks have been passed as a file instead of plaintext. // If so, the value of the flag will be replaced with the contents of the file. -func GetSecretsFromFiles(rootCmd *cobra.Command) { - flags := rootCmd.PersistentFlags() - +func GetSecretsFromFiles() { secrets := []string{ "notification-email-server-password", "notification-slack-hook-url", @@ -400,25 +373,19 @@ func GetSecretsFromFiles(rootCmd *cobra.Command) { "notification-gotify-token", } for _, secret := range secrets { - getSecretFromFile(flags, secret) + getSecretFromFile(secret) } } // getSecretFromFile will check if the flag contains a reference to a file; if it does, replaces the value of the flag with the contents of the file. -func getSecretFromFile(flags *pflag.FlagSet, secret string) { - value, err := flags.GetString(secret) - if err != nil { - log.Error(err) - } +func getSecretFromFile(secret string) { + value := viper.GetString(secret) if value != "" && isFile(value) { file, err := ioutil.ReadFile(value) if err != nil { log.Fatal(err) } - err = flags.Set(secret, strings.TrimSpace(string(file))) - if err != nil { - log.Error(err) - } + viper.Set(secret, strings.TrimSpace(string(file))) } } diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index b659a96..55697f6 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -1,6 +1,7 @@ package flags import ( + "github.com/spf13/viper" "io/ioutil" "os" "testing" @@ -12,10 +13,11 @@ import ( func TestEnvConfig_Defaults(t *testing.T) { cmd := new(cobra.Command) - SetDefaults() RegisterDockerFlags(cmd) + SetEnvBindings() + BindViperFlags(cmd) - err := EnvConfig(cmd) + err := EnvConfig() require.NoError(t, err) assert.Equal(t, "unix:///var/run/docker.sock", os.Getenv("DOCKER_HOST")) @@ -26,13 +28,14 @@ func TestEnvConfig_Defaults(t *testing.T) { func TestEnvConfig_Custom(t *testing.T) { cmd := new(cobra.Command) - SetDefaults() RegisterDockerFlags(cmd) + SetEnvBindings() + BindViperFlags(cmd) err := cmd.ParseFlags([]string{"--host", "some-custom-docker-host", "--tlsverify", "--api-version", "1.99"}) require.NoError(t, err) - err = EnvConfig(cmd) + err = EnvConfig() require.NoError(t, err) assert.Equal(t, "some-custom-docker-host", os.Getenv("DOCKER_HOST")) @@ -71,11 +74,11 @@ func TestGetSecretsFromFilesWithFile(t *testing.T) { func testGetSecretsFromFiles(t *testing.T, flagName string, expected string) { cmd := new(cobra.Command) - SetDefaults() RegisterNotificationFlags(cmd) - GetSecretsFromFiles(cmd) - value, err := cmd.PersistentFlags().GetString(flagName) - require.NoError(t, err) + SetEnvBindings() + BindViperFlags(cmd) + GetSecretsFromFiles() + value := viper.GetString(flagName) assert.Equal(t, expected, value) } diff --git a/pkg/container/client.go b/pkg/container/client.go index 2063332..36ea7c1 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -10,6 +10,7 @@ import ( "github.com/containrrr/watchtower/pkg/registry" "github.com/containrrr/watchtower/pkg/registry/digest" + "github.com/containrrr/watchtower/internal/flags" t "github.com/containrrr/watchtower/pkg/types" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -41,7 +42,7 @@ type Client interface { // * DOCKER_HOST the docker-engine host to send api requests to // * DOCKER_TLS_VERIFY whether to verify tls certificates // * DOCKER_API_VERSION the minimum docker api version to work with -func NewClient(pullImages bool, includeStopped bool, reviveStopped bool, removeVolumes bool, includeRestarting bool) Client { +func NewClient(config *flags.WatchConfig) Client { cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv) if err != nil { @@ -50,11 +51,11 @@ func NewClient(pullImages bool, includeStopped bool, reviveStopped bool, removeV return dockerClient{ api: cli, - pullImages: pullImages, - removeVolumes: removeVolumes, - includeStopped: includeStopped, - reviveStopped: reviveStopped, - includeRestarting: includeRestarting, + pullImages: !config.NoPull, + removeVolumes: config.RemoveVolumes, + includeStopped: config.IncludeStopped, + reviveStopped: config.ReviveStopped, + includeRestarting: config.IncludeRestarting, } } diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index 6079de7..5134178 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "fmt" "github.com/spf13/cobra" + "github.com/spf13/viper" "net/smtp" "os" "strings" @@ -33,18 +34,17 @@ type emailTypeNotifier struct { delay time.Duration } -func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - flags := c.PersistentFlags() +func newEmailNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - from, _ := flags.GetString("notification-email-from") - to, _ := flags.GetString("notification-email-to") - server, _ := flags.GetString("notification-email-server") - user, _ := flags.GetString("notification-email-server-user") - password, _ := flags.GetString("notification-email-server-password") - port, _ := flags.GetInt("notification-email-server-port") - tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify") - delay, _ := flags.GetInt("notification-email-delay") - subjecttag, _ := flags.GetString("notification-email-subjecttag") + from := viper.GetString("notification-email-from") + to := viper.GetString("notification-email-to") + server := viper.GetString("notification-email-server") + user := viper.GetString("notification-email-server-user") + password := viper.GetString("notification-email-server-password") + port := viper.GetInt("notification-email-server-port") + tlsSkipVerify := viper.GetBool("notification-email-server-tls-skip-verify") + delay := viper.GetInt("notification-email-delay") + subjecttag := viper.GetString("notification-email-subjecttag") n := &emailTypeNotifier{ From: from, @@ -81,13 +81,13 @@ func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte { // We don't use fields in watchtower, so don't bother sending them. } - t := time.Now() + now := time.Now() header := make(map[string]string) header["From"] = e.From header["To"] = e.To header["Subject"] = emailSubject - header["Date"] = t.Format(time.RFC1123Z) + header["Date"] = now.Format(time.RFC1123Z) header["MIME-Version"] = "1.0" header["Content-Type"] = "text/plain; charset=\"utf-8\"" header["Content-Transfer-Encoding"] = "base64" diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go index 789f778..a86f5c0 100644 --- a/pkg/notifications/gotify.go +++ b/pkg/notifications/gotify.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "encoding/json" "fmt" + "github.com/spf13/viper" "net/http" "strings" @@ -24,10 +25,10 @@ type gotifyTypeNotifier struct { logLevels []log.Level } -func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - flags := c.PersistentFlags() +func newGotifyNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { + flags := viper.Sub(".") - gotifyURL, _ := flags.GetString("notification-gotify-url") + gotifyURL := flags.GetString("notification-gotify-url") if len(gotifyURL) < 1 { log.Fatal("Required argument --notification-gotify-url(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_URL(env) is empty.") } else if !(strings.HasPrefix(gotifyURL, "http://") || strings.HasPrefix(gotifyURL, "https://")) { @@ -36,12 +37,12 @@ func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifi log.Warn("Using an HTTP url for Gotify is insecure") } - gotifyToken, _ := flags.GetString("notification-gotify-token") + gotifyToken := flags.GetString("notification-gotify-token") if len(gotifyToken) < 1 { log.Fatal("Required argument --notification-gotify-token(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN(env) is empty.") } - gotifyInsecureSkipVerify, _ := flags.GetBool("notification-gotify-tls-skip-verify") + gotifyInsecureSkipVerify := flags.GetBool("notification-gotify-tls-skip-verify") n := &gotifyTypeNotifier{ gotifyURL: gotifyURL, diff --git a/pkg/notifications/msteams.go b/pkg/notifications/msteams.go index ab33966..5b96eaa 100644 --- a/pkg/notifications/msteams.go +++ b/pkg/notifications/msteams.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "github.com/spf13/cobra" + "github.com/spf13/viper" "net/http" t "github.com/containrrr/watchtower/pkg/types" @@ -22,16 +23,14 @@ type msTeamsTypeNotifier struct { data bool } -func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +func newMsTeamsNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - flags := cmd.PersistentFlags() - - webHookURL, _ := flags.GetString("notification-msteams-hook") + webHookURL := viper.GetString("notification-msteams-hook") if len(webHookURL) <= 0 { log.Fatal("Required argument --notification-msteams-hook(cli) or WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL(env) is empty.") } - withData, _ := flags.GetBool("notification-msteams-data") + withData := viper.GetBool("notification-msteams-data") n := &msTeamsTypeNotifier{ levels: acceptedLogLevels, webHookURL: webHookURL, @@ -85,19 +84,19 @@ func (n *msTeamsTypeNotifier) Fire(entry *log.Entry) error { jsonBody, err := json.Marshal(webHookBody) if err != nil { - fmt.Println("Failed to build JSON body for MSTeams notificattion: ", err) + fmt.Println("Failed to build JSON body for MSTeams notification: ", err) return } - resp, err := http.Post(n.webHookURL, "application/json", bytes.NewBuffer([]byte(jsonBody))) + resp, err := http.Post(n.webHookURL, "application/json", bytes.NewBuffer(jsonBody)) if err != nil { - fmt.Println("Failed to send MSTeams notificattion: ", err) + fmt.Println("Failed to send MSTeams notification: ", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode > 299 { - fmt.Println("Failed to send MSTeams notificattion. HTTP RESPONSE STATUS: ", resp.StatusCode) + fmt.Println("Failed to send MSTeams notification. HTTP RESPONSE STATUS: ", resp.StatusCode) if resp.Body != nil { bodyBytes, err := ioutil.ReadAll(resp.Body) if err == nil { diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index dedb21a..3e1b539 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -5,6 +5,7 @@ import ( "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" ) // Notifier can send log output as notification to admins, with optional batching. @@ -16,9 +17,7 @@ type Notifier struct { func NewNotifier(c *cobra.Command) *Notifier { n := &Notifier{} - f := c.PersistentFlags() - - level, _ := f.GetString("notifications-level") + level := viper.GetString("notifications-level") logLevel, err := log.ParseLevel(level) if err != nil { log.Fatalf("Notifications invalid log level: %s", err.Error()) @@ -27,7 +26,7 @@ func NewNotifier(c *cobra.Command) *Notifier { acceptedLogLevels := slackrus.LevelThreshold(logLevel) // Parse types and create notifiers. - types, err := f.GetStringSlice("notifications") + types := viper.GetStringSlice("notifications") if err != nil { log.WithField("could not read notifications argument", log.Fields{"Error": err}).Fatal() } diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index d16808d..ae24ba4 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "github.com/containrrr/shoutrrr/pkg/types" + "github.com/spf13/viper" "strings" "text/template" @@ -34,9 +35,8 @@ type shoutrrrTypeNotifier struct { } func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - flags := c.PersistentFlags() - urls, _ := flags.GetStringArray("notification-url") + urls := viper.GetStringSlice("notification-url") r, err := shoutrrr.CreateSender(urls...) if err != nil { log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error()) @@ -126,12 +126,11 @@ func (e *shoutrrrTypeNotifier) Fire(entry *log.Entry) error { return nil } -func getShoutrrrTemplate(c *cobra.Command) *template.Template { +func getShoutrrrTemplate(_ *cobra.Command) *template.Template { var tpl *template.Template + var err error - flags := c.PersistentFlags() - - tplString, err := flags.GetString("notification-template") + tplString := viper.GetString("notification-template") funcs := template.FuncMap{ "ToUpper": strings.ToUpper, @@ -141,7 +140,7 @@ func getShoutrrrTemplate(c *cobra.Command) *template.Template { // If we succeed in getting a non-empty template configuration // try to parse the template string. - if tplString != "" && err == nil { + if tplString != "" { tpl, err = template.New("").Funcs(funcs).Parse(tplString) } diff --git a/pkg/notifications/shoutrrr_test.go b/pkg/notifications/shoutrrr_test.go index 47334af..15c1252 100644 --- a/pkg/notifications/shoutrrr_test.go +++ b/pkg/notifications/shoutrrr_test.go @@ -32,6 +32,7 @@ func TestShoutrrrDefaultTemplate(t *testing.T) { func TestShoutrrrTemplate(t *testing.T) { cmd := new(cobra.Command) flags.RegisterNotificationFlags(cmd) + flags.BindViperFlags(cmd) err := cmd.ParseFlags([]string{"--notification-template={{range .}}{{.Level}}: {{.Message}}{{println}}{{end}}"}) require.NoError(t, err) @@ -55,6 +56,7 @@ func TestShoutrrrTemplate(t *testing.T) { func TestShoutrrrStringFunctions(t *testing.T) { cmd := new(cobra.Command) flags.RegisterNotificationFlags(cmd) + flags.BindViperFlags(cmd) err := cmd.ParseFlags([]string{"--notification-template={{range .}}{{.Level | printf \"%v\" | ToUpper }}: {{.Message | ToLower }} {{.Message | Title }}{{println}}{{end}}"}) require.NoError(t, err) @@ -77,8 +79,8 @@ func TestShoutrrrStringFunctions(t *testing.T) { func TestShoutrrrInvalidTemplateUsesTemplate(t *testing.T) { cmd := new(cobra.Command) - flags.RegisterNotificationFlags(cmd) + flags.BindViperFlags(cmd) err := cmd.ParseFlags([]string{"--notification-template={{"}) require.NoError(t, err) @@ -108,7 +110,7 @@ type blockingRouter struct { sent chan bool } -func (b blockingRouter) Send(message string, params *types.Params) []error { +func (b blockingRouter) Send(_ string, _ *types.Params) []error { _ = <-b.unlock b.sent <- true return nil diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go index 5f96390..9129f57 100644 --- a/pkg/notifications/slack.go +++ b/pkg/notifications/slack.go @@ -5,6 +5,7 @@ import ( "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" ) const ( @@ -15,14 +16,13 @@ type slackTypeNotifier struct { slackrus.SlackrusHook } -func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - flags := c.PersistentFlags() +func newSlackNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - hookURL, _ := flags.GetString("notification-slack-hook-url") - userName, _ := flags.GetString("notification-slack-identifier") - channel, _ := flags.GetString("notification-slack-channel") - emoji, _ := flags.GetString("notification-slack-icon-emoji") - iconURL, _ := flags.GetString("notification-slack-icon-url") + hookURL := viper.GetString("notification-slack-hook-url") + userName := viper.GetString("notification-slack-identifier") + channel := viper.GetString("notification-slack-channel") + emoji := viper.GetString("notification-slack-icon-emoji") + iconURL := viper.GetString("notification-slack-icon-url") n := &slackTypeNotifier{ SlackrusHook: slackrus.SlackrusHook{ From eb8580f7f2d701bd7a613765d2e57d5eed415e79 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Mon, 21 Dec 2020 18:06:14 +0100 Subject: [PATCH 15/98] make sure all different ref formats are supported --- pkg/registry/auth/auth.go | 31 +++++++++++++++++++++++++++---- pkg/registry/auth/auth_test.go | 22 ++++++++++++++++++++++ pkg/registry/digest/digest.go | 5 +++-- pkg/registry/manifest/manifest.go | 4 +++- 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/pkg/registry/auth/auth.go b/pkg/registry/auth/auth.go index 3b3d088..8e7be4a 100644 --- a/pkg/registry/auth/auth.go +++ b/pkg/registry/auth/auth.go @@ -139,10 +139,9 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) { authURL, _ := url.Parse(fmt.Sprintf("%s", values["realm"])) q := authURL.Query() q.Add("service", values["service"]) - scopeImage := strings.TrimPrefix(img, values["service"]) - if !strings.Contains(scopeImage, "/") { - scopeImage = "library/" + scopeImage - } + + scopeImage := GetScopeFromImageName(img, values["service"]) + scope := fmt.Sprintf("repository:%s:pull", scopeImage) logrus.WithFields(logrus.Fields{"scope": scope, "image": img}).Debug("Setting scope for auth token") q.Add("scope", scope) @@ -151,6 +150,30 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) { return authURL, nil } +func GetScopeFromImageName(img, svc string) string { + parts := strings.Split(img, "/") + scopeImage := "" + if len(parts) > 2 { + if strings.Contains(svc, "docker.io") { + fmt.Printf("Identified dockerhub image") + scopeImage = fmt.Sprintf("%s/%s", parts[1], strings.Join(parts[2:], "/")) + } else { + scopeImage = strings.Join(parts, "/") + } + } else if len(parts) == 2 { + if strings.Contains(parts[0], "docker.io") { + scopeImage = fmt.Sprintf("library/%s", parts[1]) + } else { + scopeImage = strings.Replace(img, svc + "/", "", 1) + } + } else if strings.Contains(svc, "docker.io") { + scopeImage = fmt.Sprintf("library/%s", parts[0]) + } else { + scopeImage = img + } + return scopeImage +} + // GetChallengeURL creates a URL object based on the image info func GetChallengeURL(img string) (url.URL, error) { diff --git a/pkg/registry/auth/auth_test.go b/pkg/registry/auth/auth_test.go index 16a6478..6ad2307 100644 --- a/pkg/registry/auth/auth_test.go +++ b/pkg/registry/auth/auth_test.go @@ -95,4 +95,26 @@ var _ = Describe("the auth module", func() { 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")) + }) + }) }) diff --git a/pkg/registry/digest/digest.go b/pkg/registry/digest/digest.go index 389f059..59f4d9b 100644 --- a/pkg/registry/digest/digest.go +++ b/pkg/registry/digest/digest.go @@ -73,13 +73,14 @@ func GetDigest(url string, token string) (string, error) { } client := &http.Client{Transport: tr} + req, _ := http.NewRequest("HEAD", url, nil) + if token != "" { logrus.WithField("token", token).Trace("Setting request token") } else { return "", errors.New("could not fetch token") } - req, _ := http.NewRequest("HEAD", url, nil) 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") @@ -94,7 +95,7 @@ func GetDigest(url string, token string) (string, error) { defer res.Body.Close() if res.StatusCode != 200 { - return "", fmt.Errorf("registry responded to head request with %d", res.StatusCode) + return "", fmt.Errorf("registry responded to head request with %v", res) } return res.Header.Get(ContentDigestHeader), nil } diff --git a/pkg/registry/manifest/manifest.go b/pkg/registry/manifest/manifest.go index 837bc3f..aac7762 100644 --- a/pkg/registry/manifest/manifest.go +++ b/pkg/registry/manifest/manifest.go @@ -2,6 +2,7 @@ package manifest import ( "fmt" + "github.com/containrrr/watchtower/pkg/registry/auth" "github.com/containrrr/watchtower/pkg/registry/helpers" "github.com/containrrr/watchtower/pkg/types" ref "github.com/docker/distribution/reference" @@ -31,7 +32,8 @@ func BuildManifestURL(container types.Container) (string, error) { if err != nil { return "", err } - img = strings.TrimPrefix(img, fmt.Sprintf("%s/", host)) + img = auth.GetScopeFromImageName(img, host) + if !strings.Contains(img, "/") { img = "library/" + img } From cf6a71de810f2903bf24f7035c4250004a828dc4 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Mon, 21 Dec 2020 18:11:08 +0100 Subject: [PATCH 16/98] fix linting issues --- pkg/registry/auth/auth.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/registry/auth/auth.go b/pkg/registry/auth/auth.go index 8e7be4a..ae708b6 100644 --- a/pkg/registry/auth/auth.go +++ b/pkg/registry/auth/auth.go @@ -150,6 +150,7 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) { 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, "/") scopeImage := "" @@ -164,7 +165,7 @@ func GetScopeFromImageName(img, svc string) string { if strings.Contains(parts[0], "docker.io") { scopeImage = fmt.Sprintf("library/%s", parts[1]) } else { - scopeImage = strings.Replace(img, svc + "/", "", 1) + scopeImage = strings.Replace(img, svc+"/", "", 1) } } else if strings.Contains(svc, "docker.io") { scopeImage = fmt.Sprintf("library/%s", parts[0]) From 89119515af3aa2eff87d195c33208bd15439a48c Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Mon, 21 Dec 2020 18:20:38 +0100 Subject: [PATCH 17/98] clean up scope builder and remove fmt print --- pkg/registry/auth/auth.go | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/pkg/registry/auth/auth.go b/pkg/registry/auth/auth.go index ae708b6..99e307c 100644 --- a/pkg/registry/auth/auth.go +++ b/pkg/registry/auth/auth.go @@ -153,26 +153,25 @@ func GetAuthURL(challenge string, img string) (*url.URL, error) { // GetScopeFromImageName normalizes an image name for use as scope during auth and head requests func GetScopeFromImageName(img, svc string) string { parts := strings.Split(img, "/") - scopeImage := "" + if len(parts) > 2 { if strings.Contains(svc, "docker.io") { - fmt.Printf("Identified dockerhub image") - scopeImage = fmt.Sprintf("%s/%s", parts[1], strings.Join(parts[2:], "/")) - } else { - scopeImage = strings.Join(parts, "/") + return fmt.Sprintf("%s/%s", parts[1], strings.Join(parts[2:], "/")) } - } else if len(parts) == 2 { - if strings.Contains(parts[0], "docker.io") { - scopeImage = fmt.Sprintf("library/%s", parts[1]) - } else { - scopeImage = strings.Replace(img, svc+"/", "", 1) - } - } else if strings.Contains(svc, "docker.io") { - scopeImage = fmt.Sprintf("library/%s", parts[0]) - } else { - scopeImage = img + return strings.Join(parts, "/") } - return scopeImage + + 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 From 8b81fbd48d2fcbae853d8af582c7da9b3b391457 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Mon, 21 Dec 2020 23:08:23 +0100 Subject: [PATCH 18/98] Revert "feat(config): swap viper and cobra for config (#684)" This reverts commit ff8cb884a0852ce35a18f59802826f669c740236. --- cmd/root.go | 136 ++++++++++++-------- internal/flags/config.go | 31 ----- internal/flags/flags.go | 191 +++++++++++++++++------------ internal/flags/flags_test.go | 19 ++- pkg/container/client.go | 13 +- pkg/notifications/email.go | 26 ++-- pkg/notifications/gotify.go | 11 +- pkg/notifications/msteams.go | 17 +-- pkg/notifications/notifier.go | 7 +- pkg/notifications/shoutrrr.go | 13 +- pkg/notifications/shoutrrr_test.go | 6 +- pkg/notifications/slack.go | 14 +-- 12 files changed, 255 insertions(+), 229 deletions(-) delete mode 100644 internal/flags/config.go diff --git a/cmd/root.go b/cmd/root.go index d0fa413..1e61308 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,10 +1,9 @@ package cmd import ( - "fmt" - "github.com/spf13/viper" "os" "os/signal" + "strconv" "syscall" "time" @@ -22,9 +21,17 @@ import ( ) var ( - client container.Client - notifier *notifications.Notifier - c flags.WatchConfig + client container.Client + scheduleSpec string + cleanup bool + noRestart bool + monitorOnly bool + enableLabel bool + notifier *notifications.Notifier + timeout time.Duration + lifecycleHooks bool + rollingRestart bool + scope string ) var rootCmd = &cobra.Command{ @@ -39,11 +46,10 @@ More information available at https://github.com/containrrr/watchtower/. } func init() { + flags.SetDefaults() flags.RegisterDockerFlags(rootCmd) flags.RegisterSystemFlags(rootCmd) flags.RegisterNotificationFlags(rootCmd) - flags.SetEnvBindings() - flags.BindViperFlags(rootCmd) } // Execute the root func and exit in case of errors @@ -54,10 +60,10 @@ func Execute() { } // PreRun is a lifecycle hook that runs before the command is executed. -func PreRun(cmd *cobra.Command, _ []string) { +func PreRun(cmd *cobra.Command, args []string) { + f := cmd.PersistentFlags() - // First apply all the settings that affect the output - if viper.GetBool("no-color") { + if enabled, _ := f.GetBool("no-color"); enabled { log.SetFormatter(&log.TextFormatter{ DisableColors: true, }) @@ -68,55 +74,75 @@ func PreRun(cmd *cobra.Command, _ []string) { }) } - if viper.GetBool("debug") { + if enabled, _ := f.GetBool("debug"); enabled { log.SetLevel(log.DebugLevel) } - if viper.GetBool("trace") { + if enabled, _ := f.GetBool("trace"); enabled { log.SetLevel(log.TraceLevel) } - interval := viper.GetInt("interval") + pollingSet := f.Changed("interval") + schedule, _ := f.GetString("schedule") + cronLen := len(schedule) - // If empty, set schedule using interval helper value - if viper.GetString("schedule") == "" { - viper.Set("schedule", fmt.Sprintf("@every %ds", interval)) - } else if interval != flags.DefaultInterval { - log.Fatal("only schedule or interval can be defined, not both") + if pollingSet && cronLen > 0 { + log.Fatal("Only schedule or interval can be defined, not both.") + } else if cronLen > 0 { + scheduleSpec, _ = f.GetString("schedule") + } else { + interval, _ := f.GetInt("interval") + scheduleSpec = "@every " + strconv.Itoa(interval) + "s" } - // Then load the rest of the settings - err := viper.Unmarshal(&c) - if err != nil { - log.Fatalf("unable to decode into struct, %v", err) - } + flags.GetSecretsFromFiles(cmd) + cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd) - flags.GetSecretsFromFiles() - - if c.Timeout <= 0 { + if timeout < 0 { log.Fatal("Please specify a positive value for timeout value.") } - log.Debugf("Using scope %v", c.Scope) + enableLabel, _ = f.GetBool("label-enable") + lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks") + rollingRestart, _ = f.GetBool("rolling-restart") + scope, _ = f.GetString("scope") - if err = flags.EnvConfig(); err != nil { - log.Fatalf("failed to setup environment variables: %v", err) + log.Debug(scope) + + // configure environment vars for client + err := flags.EnvConfig(cmd) + if err != nil { + log.Fatal(err) } - if c.MonitorOnly && c.NoPull { + noPull, _ := f.GetBool("no-pull") + includeStopped, _ := f.GetBool("include-stopped") + includeRestarting, _ := f.GetBool("include-restarting") + reviveStopped, _ := f.GetBool("revive-stopped") + removeVolumes, _ := f.GetBool("remove-volumes") + + if monitorOnly && noPull { log.Warn("Using `WATCHTOWER_NO_PULL` and `WATCHTOWER_MONITOR_ONLY` simultaneously might lead to no action being taken at all. If this is intentional, you may safely ignore this message.") } - client = container.NewClient(&c) + client = container.NewClient( + !noPull, + includeStopped, + reviveStopped, + removeVolumes, + includeRestarting, + ) notifier = notifications.NewNotifier(cmd) } // Run is the main execution flow of the command -func Run(_ *cobra.Command, names []string) { - filter := filters.BuildFilter(names, c.EnableLabel, c.Scope) +func Run(c *cobra.Command, names []string) { + filter := filters.BuildFilter(names, enableLabel, scope) + runOnce, _ := c.PersistentFlags().GetBool("run-once") + httpAPI, _ := c.PersistentFlags().GetBool("http-api") - if c.RunOnce { - if !c.NoStartupMessage { + if runOnce { + if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage { log.Info("Running a one time update.") } runUpdatesWithNotifications(filter) @@ -125,12 +151,14 @@ func Run(_ *cobra.Command, names []string) { return } - if err := actions.CheckForMultipleWatchtowerInstances(client, c.Cleanup, c.Scope); err != nil { + if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup, scope); err != nil { log.Fatal(err) } - if c.HTTPAPI { - if err := api.SetupHTTPUpdates(c.HTTPAPIToken, func() { runUpdatesWithNotifications(filter) }); err != nil { + if httpAPI { + apiToken, _ := c.PersistentFlags().GetString("http-api-token") + + if err := api.SetupHTTPUpdates(apiToken, func() { runUpdatesWithNotifications(filter) }); err != nil { log.Fatal(err) os.Exit(1) } @@ -138,20 +166,20 @@ func Run(_ *cobra.Command, names []string) { api.WaitForHTTPUpdates() } - if err := runUpgradesOnSchedule(filter); err != nil { + if err := runUpgradesOnSchedule(c, filter); err != nil { log.Error(err) } os.Exit(1) } -func runUpgradesOnSchedule(filter t.Filter) error { +func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { tryLockSem := make(chan bool, 1) tryLockSem <- true - runner := cron.New() - err := runner.AddFunc( - viper.GetString("schedule"), + cron := cron.New() + err := cron.AddFunc( + scheduleSpec, func() { select { case v := <-tryLockSem: @@ -161,7 +189,7 @@ func runUpgradesOnSchedule(filter t.Filter) error { log.Debug("Skipped another update already running.") } - nextRuns := runner.Entries() + nextRuns := cron.Entries() if len(nextRuns) > 0 { log.Debug("Scheduled next run: " + nextRuns[0].Next.String()) } @@ -171,11 +199,11 @@ func runUpgradesOnSchedule(filter t.Filter) error { return err } - if !viper.GetBool("no-startup-message") { - log.Info("Starting Watchtower and scheduling first run: " + runner.Entries()[0].Schedule.Next(time.Now()).String()) + if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage { + log.Info("Starting Watchtower and scheduling first run: " + cron.Entries()[0].Schedule.Next(time.Now()).String()) } - runner.Start() + cron.Start() // Graceful shut-down on SIGINT/SIGTERM interrupt := make(chan os.Signal, 1) @@ -183,7 +211,7 @@ func runUpgradesOnSchedule(filter t.Filter) error { signal.Notify(interrupt, syscall.SIGTERM) <-interrupt - runner.Stop() + cron.Stop() log.Info("Waiting for running update to be finished...") <-tryLockSem return nil @@ -193,12 +221,12 @@ func runUpdatesWithNotifications(filter t.Filter) { notifier.StartNotification() updateParams := t.UpdateParams{ Filter: filter, - Cleanup: c.Cleanup, - NoRestart: c.NoRestart, - Timeout: c.Timeout, - MonitorOnly: c.MonitorOnly, - LifecycleHooks: c.LifecycleHooks, - RollingRestart: c.RollingRestart, + Cleanup: cleanup, + NoRestart: noRestart, + Timeout: timeout, + MonitorOnly: monitorOnly, + LifecycleHooks: lifecycleHooks, + RollingRestart: rollingRestart, } err := actions.Update(client, updateParams) if err != nil { diff --git a/internal/flags/config.go b/internal/flags/config.go deleted file mode 100644 index ef0a40f..0000000 --- a/internal/flags/config.go +++ /dev/null @@ -1,31 +0,0 @@ -package flags - -import ( - "time" -) - -// WatchConfig is the global watchtower configuration created from flags and environment variables -type WatchConfig struct { - Interval int - Schedule string - NoPull bool `mapstructure:"no-pull"` - NoRestart bool `mapstructure:"no-restart"` - NoStartupMessage bool `mapstructure:"no-startup-message"` - Cleanup bool - RemoveVolumes bool `mapstructure:"remove-volumes"` - EnableLabel bool `mapstructure:"label-enable"` - Debug bool - Trace bool - MonitorOnly bool `mapstructure:"monitor-only"` - RunOnce bool `mapstructure:"run-once"` - IncludeStopped bool `mapstructure:"include-stopped"` - IncludeRestarting bool `mapstructure:"include-restarting"` - ReviveStopped bool `mapstructure:"revive-stopped"` - LifecycleHooks bool `mapstructure:"enable-lifecycle-hooks"` - RollingRestart bool `mapstructure:"rolling-restart"` - HTTPAPI bool `mapstructure:"http-api"` - HTTPAPIToken string `mapstructure:"http-api-token"` - Timeout time.Duration `mapstructure:"stop-timeout"` - Scope string - NoColor bool `mapstructure:"no-color"` -} diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 2e37fb6..2f7a89f 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -8,6 +8,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/spf13/viper" ) @@ -15,15 +16,12 @@ import ( // use watchtower const DockerAPIMinVersion string = "1.25" -// DefaultInterval is the default time between the start of update checks -const DefaultInterval = int(time.Hour * 24 / time.Second) - // RegisterDockerFlags that are used directly by the docker api client func RegisterDockerFlags(rootCmd *cobra.Command) { flags := rootCmd.PersistentFlags() - flags.StringP("host", "H", "unix:///var/run/docker.sock", "daemon socket to connect to") - flags.BoolP("tlsverify", "v", false, "use TLS and verify the remote") - flags.StringP("api-version", "a", DockerAPIMinVersion, "api version to use by docker client") + 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") } // RegisterSystemFlags that are used by watchtower to modify the program flow @@ -32,126 +30,126 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { flags.IntP( "interval", "i", - DefaultInterval, + viper.GetInt("WATCHTOWER_POLL_INTERVAL"), "poll interval (in seconds)") flags.StringP( "schedule", "s", - "", + viper.GetString("WATCHTOWER_SCHEDULE"), "the cron expression which defines when to update") flags.DurationP( "stop-timeout", "t", - time.Second*10, + viper.GetDuration("WATCHTOWER_TIMEOUT"), "timeout before a container is forcefully stopped") flags.BoolP( "no-pull", "", - false, + viper.GetBool("WATCHTOWER_NO_PULL"), "do not pull any new images") flags.BoolP( "no-restart", "", - false, + viper.GetBool("WATCHTOWER_NO_RESTART"), "do not restart any containers") flags.BoolP( "no-startup-message", "", - false, + viper.GetBool("WATCHTOWER_NO_STARTUP_MESSAGE"), "Prevents watchtower from sending a startup message") flags.BoolP( "cleanup", "c", - false, + viper.GetBool("WATCHTOWER_CLEANUP"), "remove previously used images after updating") flags.BoolP( "remove-volumes", "", - false, + viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"), "remove attached volumes before updating") flags.BoolP( "label-enable", "e", - false, + viper.GetBool("WATCHTOWER_LABEL_ENABLE"), "watch containers where the com.centurylinklabs.watchtower.enable label is true") flags.BoolP( "debug", "d", - false, + viper.GetBool("WATCHTOWER_DEBUG"), "enable debug mode with verbose logging") flags.BoolP( "trace", "", - false, + viper.GetBool("WATCHTOWER_TRACE"), "enable trace mode with very verbose logging - caution, exposes credentials") flags.BoolP( "monitor-only", "m", - false, + viper.GetBool("WATCHTOWER_MONITOR_ONLY"), "Will only monitor for new images, not update the containers") flags.BoolP( "run-once", "R", - false, + viper.GetBool("WATCHTOWER_RUN_ONCE"), "Run once now and exit") flags.BoolP( "include-stopped", "S", - false, + viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"), "Will also include created and exited containers") flags.BoolP( "revive-stopped", "", - false, + viper.GetBool("WATCHTOWER_REVIVE_STOPPED"), "Will also start stopped containers that were updated, if include-stopped is active") flags.BoolP( "enable-lifecycle-hooks", "", - false, + viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"), "Enable the execution of commands triggered by pre- and post-update lifecycle hooks") flags.BoolP( "rolling-restart", "", - false, + viper.GetBool("WATCHTOWER_ROLLING_RESTART"), "Restart containers one at a time") flags.BoolP( "http-api", "", - false, + viper.GetBool("WATCHTOWER_HTTP_API"), "Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request") flags.StringP( "http-api-token", "", - "", + viper.GetString("WATCHTOWER_HTTP_API_TOKEN"), "Sets an authentication token to HTTP API requests.") // https://no-color.org/ flags.BoolP( "no-color", "", - false, + viper.IsSet("NO_COLOR"), "Disable ANSI color escape codes in log output") flags.StringP( "scope", "", - "", + viper.GetString("WATCHTOWER_SCOPE"), "Defines a monitoring scope for the Watchtower instance.") } @@ -162,177 +160,178 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) { flags.StringSliceP( "notifications", "n", - []string{}, + viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"), " notification types to send (valid: email, slack, msteams, gotify, shoutrrr)") flags.StringP( "notifications-level", "", - "info", + viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"), "The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug") flags.StringP( "notification-email-from", "", - "", + viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"), "Address to send notification emails from") flags.StringP( "notification-email-to", "", - "", + viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"), "Address to send notification emails to") flags.IntP( "notification-email-delay", "", - 0, + viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"), "Delay before sending notifications, expressed in seconds") flags.StringP( "notification-email-server", "", - "", + viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"), "SMTP server to send notification emails through") flags.IntP( "notification-email-server-port", "", - 25, + viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"), "SMTP server port to send notification emails through") flags.BoolP( "notification-email-server-tls-skip-verify", "", - false, + viper.GetBool("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"), "SMTP server user for sending notifications") flags.StringP( "notification-email-server-password", "", - "", + viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"), "SMTP server password for sending notifications") flags.StringP( "notification-email-subjecttag", "", - "", + viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"), "Subject prefix tag for notifications via mail") flags.StringP( "notification-slack-hook-url", "", - "", + viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"), "The Slack Hook URL to send notifications to") flags.StringP( "notification-slack-identifier", "", - "watchtower", + viper.GetString("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"), "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"), "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"), "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"), "The MSTeams WebHook URL to send notifications to") flags.BoolP( "notification-msteams-data", "", - false, + viper.GetBool("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"), "The Gotify URL to send notifications to") flags.StringP( "notification-gotify-token", "", - "", + viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"), "The Gotify Application required to query the Gotify API") flags.BoolP( "notification-gotify-tls-skip-verify", "", - false, + viper.GetBool("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.StringP( "notification-template", "", - "", + viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"), "The shoutrrr text/template for the messages") flags.StringArrayP( "notification-url", "", - []string{}, + viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"), "The shoutrrr URL to send notifications to") } -// SetEnvBindings binds environment variables to their corresponding config keys -func SetEnvBindings() { - if err := viper.BindEnv("host", "DOCKER_HOST"); err != nil { - log.Fatalf("failed to bind env DOCKER_HOST: %v", err) - } - if err := viper.BindEnv("tlsverify", "DOCKER_TLS_VERIFY"); err != nil { - log.Fatalf("failed to bind env DOCKER_TLS_VERIFY: %v", err) - } - if err := viper.BindEnv("api-version", "DOCKER_API_VERSION"); err != nil { - log.Fatalf("failed to bind env DOCKER_API_VERSION: %v", err) - } - viper.SetEnvPrefix("WATCHTOWER") - viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) +// SetDefaults provides default values for environment variables +func SetDefaults() { + day := (time.Hour * 24).Seconds() viper.AutomaticEnv() -} - -// BindViperFlags binds the cmd PFlags to the viper configuration -func BindViperFlags(cmd *cobra.Command) { - if err := viper.BindPFlags(cmd.PersistentFlags()); err != nil { - log.Fatalf("failed to bind flags: %v", err) - } + viper.SetDefault("DOCKER_HOST", "unix:///var/run/docker.sock") + viper.SetDefault("DOCKER_API_VERSION", DockerAPIMinVersion) + viper.SetDefault("WATCHTOWER_POLL_INTERVAL", day) + viper.SetDefault("WATCHTOWER_TIMEOUT", time.Second*10) + viper.SetDefault("WATCHTOWER_NOTIFICATIONS", []string{}) + viper.SetDefault("WATCHTOWER_NOTIFICATIONS_LEVEL", "info") + viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT", 25) + viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "") + viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower") } // EnvConfig translates the command-line options into environment variables // that will initialize the api client -func EnvConfig() error { +func EnvConfig(cmd *cobra.Command) error { var err error + var host string var tls bool var version string - host := viper.GetString("host") - tls = viper.GetBool("tlsverify") - version = viper.GetString("api-version") + flags := cmd.PersistentFlags() + + if host, err = flags.GetString("host"); err != nil { + return err + } + if tls, err = flags.GetBool("tlsverify"); err != nil { + return err + } + if version, err = flags.GetString("api-version"); err != nil { + return err + } if err = setEnvOptStr("DOCKER_HOST", host); err != nil { return err } @@ -345,6 +344,32 @@ func EnvConfig() error { return nil } +// ReadFlags reads common flags used in the main program flow of watchtower +func ReadFlags(cmd *cobra.Command) (bool, bool, bool, time.Duration) { + flags := cmd.PersistentFlags() + + var err error + var cleanup bool + var noRestart bool + var monitorOnly bool + var timeout time.Duration + + if cleanup, err = flags.GetBool("cleanup"); err != nil { + log.Fatal(err) + } + if noRestart, err = flags.GetBool("no-restart"); err != nil { + log.Fatal(err) + } + if monitorOnly, err = flags.GetBool("monitor-only"); err != nil { + log.Fatal(err) + } + if timeout, err = flags.GetDuration("stop-timeout"); err != nil { + log.Fatal(err) + } + + return cleanup, noRestart, monitorOnly, timeout +} + func setEnvOptStr(env string, opt string) error { if opt == "" || opt == os.Getenv(env) { return nil @@ -365,7 +390,9 @@ func setEnvOptBool(env string, opt bool) error { // GetSecretsFromFiles checks if passwords/tokens/webhooks have been passed as a file instead of plaintext. // If so, the value of the flag will be replaced with the contents of the file. -func GetSecretsFromFiles() { +func GetSecretsFromFiles(rootCmd *cobra.Command) { + flags := rootCmd.PersistentFlags() + secrets := []string{ "notification-email-server-password", "notification-slack-hook-url", @@ -373,19 +400,25 @@ func GetSecretsFromFiles() { "notification-gotify-token", } for _, secret := range secrets { - getSecretFromFile(secret) + getSecretFromFile(flags, secret) } } // 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(secret string) { - value := viper.GetString(secret) +func getSecretFromFile(flags *pflag.FlagSet, secret string) { + value, err := flags.GetString(secret) + if err != nil { + log.Error(err) + } if value != "" && isFile(value) { file, err := ioutil.ReadFile(value) if err != nil { log.Fatal(err) } - viper.Set(secret, strings.TrimSpace(string(file))) + err = flags.Set(secret, strings.TrimSpace(string(file))) + if err != nil { + log.Error(err) + } } } diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index 55697f6..b659a96 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -1,7 +1,6 @@ package flags import ( - "github.com/spf13/viper" "io/ioutil" "os" "testing" @@ -13,11 +12,10 @@ import ( func TestEnvConfig_Defaults(t *testing.T) { cmd := new(cobra.Command) + SetDefaults() RegisterDockerFlags(cmd) - SetEnvBindings() - BindViperFlags(cmd) - err := EnvConfig() + err := EnvConfig(cmd) require.NoError(t, err) assert.Equal(t, "unix:///var/run/docker.sock", os.Getenv("DOCKER_HOST")) @@ -28,14 +26,13 @@ func TestEnvConfig_Defaults(t *testing.T) { func TestEnvConfig_Custom(t *testing.T) { cmd := new(cobra.Command) + SetDefaults() RegisterDockerFlags(cmd) - SetEnvBindings() - BindViperFlags(cmd) err := cmd.ParseFlags([]string{"--host", "some-custom-docker-host", "--tlsverify", "--api-version", "1.99"}) require.NoError(t, err) - err = EnvConfig() + err = EnvConfig(cmd) require.NoError(t, err) assert.Equal(t, "some-custom-docker-host", os.Getenv("DOCKER_HOST")) @@ -74,11 +71,11 @@ func TestGetSecretsFromFilesWithFile(t *testing.T) { func testGetSecretsFromFiles(t *testing.T, flagName string, expected string) { cmd := new(cobra.Command) + SetDefaults() RegisterNotificationFlags(cmd) - SetEnvBindings() - BindViperFlags(cmd) - GetSecretsFromFiles() - value := viper.GetString(flagName) + GetSecretsFromFiles(cmd) + value, err := cmd.PersistentFlags().GetString(flagName) + require.NoError(t, err) assert.Equal(t, expected, value) } diff --git a/pkg/container/client.go b/pkg/container/client.go index 36ea7c1..2063332 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -10,7 +10,6 @@ import ( "github.com/containrrr/watchtower/pkg/registry" "github.com/containrrr/watchtower/pkg/registry/digest" - "github.com/containrrr/watchtower/internal/flags" t "github.com/containrrr/watchtower/pkg/types" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -42,7 +41,7 @@ type Client interface { // * 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(config *flags.WatchConfig) Client { +func NewClient(pullImages bool, includeStopped bool, reviveStopped bool, removeVolumes bool, includeRestarting bool) Client { cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv) if err != nil { @@ -51,11 +50,11 @@ func NewClient(config *flags.WatchConfig) Client { return dockerClient{ api: cli, - pullImages: !config.NoPull, - removeVolumes: config.RemoveVolumes, - includeStopped: config.IncludeStopped, - reviveStopped: config.ReviveStopped, - includeRestarting: config.IncludeRestarting, + pullImages: pullImages, + removeVolumes: removeVolumes, + includeStopped: includeStopped, + reviveStopped: reviveStopped, + includeRestarting: includeRestarting, } } diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index 5134178..6079de7 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -4,7 +4,6 @@ import ( "encoding/base64" "fmt" "github.com/spf13/cobra" - "github.com/spf13/viper" "net/smtp" "os" "strings" @@ -34,17 +33,18 @@ type emailTypeNotifier struct { delay time.Duration } -func newEmailNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { + flags := c.PersistentFlags() - from := viper.GetString("notification-email-from") - to := viper.GetString("notification-email-to") - server := viper.GetString("notification-email-server") - user := viper.GetString("notification-email-server-user") - password := viper.GetString("notification-email-server-password") - port := viper.GetInt("notification-email-server-port") - tlsSkipVerify := viper.GetBool("notification-email-server-tls-skip-verify") - delay := viper.GetInt("notification-email-delay") - subjecttag := viper.GetString("notification-email-subjecttag") + from, _ := flags.GetString("notification-email-from") + to, _ := flags.GetString("notification-email-to") + server, _ := flags.GetString("notification-email-server") + user, _ := flags.GetString("notification-email-server-user") + password, _ := flags.GetString("notification-email-server-password") + port, _ := flags.GetInt("notification-email-server-port") + tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify") + delay, _ := flags.GetInt("notification-email-delay") + subjecttag, _ := flags.GetString("notification-email-subjecttag") n := &emailTypeNotifier{ From: from, @@ -81,13 +81,13 @@ func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte { // We don't use fields in watchtower, so don't bother sending them. } - now := time.Now() + t := time.Now() header := make(map[string]string) header["From"] = e.From header["To"] = e.To header["Subject"] = emailSubject - header["Date"] = now.Format(time.RFC1123Z) + header["Date"] = t.Format(time.RFC1123Z) header["MIME-Version"] = "1.0" header["Content-Type"] = "text/plain; charset=\"utf-8\"" header["Content-Transfer-Encoding"] = "base64" diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go index a86f5c0..789f778 100644 --- a/pkg/notifications/gotify.go +++ b/pkg/notifications/gotify.go @@ -5,7 +5,6 @@ import ( "crypto/tls" "encoding/json" "fmt" - "github.com/spf13/viper" "net/http" "strings" @@ -25,10 +24,10 @@ type gotifyTypeNotifier struct { logLevels []log.Level } -func newGotifyNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - flags := viper.Sub(".") +func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { + flags := c.PersistentFlags() - gotifyURL := flags.GetString("notification-gotify-url") + gotifyURL, _ := flags.GetString("notification-gotify-url") if len(gotifyURL) < 1 { log.Fatal("Required argument --notification-gotify-url(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_URL(env) is empty.") } else if !(strings.HasPrefix(gotifyURL, "http://") || strings.HasPrefix(gotifyURL, "https://")) { @@ -37,12 +36,12 @@ func newGotifyNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifi log.Warn("Using an HTTP url for Gotify is insecure") } - gotifyToken := flags.GetString("notification-gotify-token") + gotifyToken, _ := flags.GetString("notification-gotify-token") if len(gotifyToken) < 1 { log.Fatal("Required argument --notification-gotify-token(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN(env) is empty.") } - gotifyInsecureSkipVerify := flags.GetBool("notification-gotify-tls-skip-verify") + gotifyInsecureSkipVerify, _ := flags.GetBool("notification-gotify-tls-skip-verify") n := &gotifyTypeNotifier{ gotifyURL: gotifyURL, diff --git a/pkg/notifications/msteams.go b/pkg/notifications/msteams.go index 5b96eaa..ab33966 100644 --- a/pkg/notifications/msteams.go +++ b/pkg/notifications/msteams.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "github.com/spf13/cobra" - "github.com/spf13/viper" "net/http" t "github.com/containrrr/watchtower/pkg/types" @@ -23,14 +22,16 @@ type msTeamsTypeNotifier struct { data bool } -func newMsTeamsNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { - webHookURL := viper.GetString("notification-msteams-hook") + flags := cmd.PersistentFlags() + + webHookURL, _ := flags.GetString("notification-msteams-hook") if len(webHookURL) <= 0 { log.Fatal("Required argument --notification-msteams-hook(cli) or WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL(env) is empty.") } - withData := viper.GetBool("notification-msteams-data") + withData, _ := flags.GetBool("notification-msteams-data") n := &msTeamsTypeNotifier{ levels: acceptedLogLevels, webHookURL: webHookURL, @@ -84,19 +85,19 @@ func (n *msTeamsTypeNotifier) Fire(entry *log.Entry) error { jsonBody, err := json.Marshal(webHookBody) if err != nil { - fmt.Println("Failed to build JSON body for MSTeams notification: ", err) + fmt.Println("Failed to build JSON body for MSTeams notificattion: ", err) return } - resp, err := http.Post(n.webHookURL, "application/json", bytes.NewBuffer(jsonBody)) + resp, err := http.Post(n.webHookURL, "application/json", bytes.NewBuffer([]byte(jsonBody))) if err != nil { - fmt.Println("Failed to send MSTeams notification: ", err) + fmt.Println("Failed to send MSTeams notificattion: ", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode > 299 { - fmt.Println("Failed to send MSTeams notification. HTTP RESPONSE STATUS: ", resp.StatusCode) + fmt.Println("Failed to send MSTeams notificattion. HTTP RESPONSE STATUS: ", resp.StatusCode) if resp.Body != nil { bodyBytes, err := ioutil.ReadAll(resp.Body) if err == nil { diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index 3e1b539..dedb21a 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -5,7 +5,6 @@ import ( "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/spf13/viper" ) // Notifier can send log output as notification to admins, with optional batching. @@ -17,7 +16,9 @@ type Notifier struct { func NewNotifier(c *cobra.Command) *Notifier { n := &Notifier{} - level := viper.GetString("notifications-level") + f := c.PersistentFlags() + + level, _ := f.GetString("notifications-level") logLevel, err := log.ParseLevel(level) if err != nil { log.Fatalf("Notifications invalid log level: %s", err.Error()) @@ -26,7 +27,7 @@ func NewNotifier(c *cobra.Command) *Notifier { acceptedLogLevels := slackrus.LevelThreshold(logLevel) // Parse types and create notifiers. - types := viper.GetStringSlice("notifications") + types, err := f.GetStringSlice("notifications") if err != nil { log.WithField("could not read notifications argument", log.Fields{"Error": err}).Fatal() } diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index ae24ba4..d16808d 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "github.com/containrrr/shoutrrr/pkg/types" - "github.com/spf13/viper" "strings" "text/template" @@ -35,8 +34,9 @@ type shoutrrrTypeNotifier struct { } func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { + flags := c.PersistentFlags() - urls := viper.GetStringSlice("notification-url") + urls, _ := flags.GetStringArray("notification-url") r, err := shoutrrr.CreateSender(urls...) if err != nil { log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error()) @@ -126,11 +126,12 @@ func (e *shoutrrrTypeNotifier) Fire(entry *log.Entry) error { return nil } -func getShoutrrrTemplate(_ *cobra.Command) *template.Template { +func getShoutrrrTemplate(c *cobra.Command) *template.Template { var tpl *template.Template - var err error - tplString := viper.GetString("notification-template") + flags := c.PersistentFlags() + + tplString, err := flags.GetString("notification-template") funcs := template.FuncMap{ "ToUpper": strings.ToUpper, @@ -140,7 +141,7 @@ func getShoutrrrTemplate(_ *cobra.Command) *template.Template { // If we succeed in getting a non-empty template configuration // try to parse the template string. - if tplString != "" { + if tplString != "" && err == nil { tpl, err = template.New("").Funcs(funcs).Parse(tplString) } diff --git a/pkg/notifications/shoutrrr_test.go b/pkg/notifications/shoutrrr_test.go index 15c1252..47334af 100644 --- a/pkg/notifications/shoutrrr_test.go +++ b/pkg/notifications/shoutrrr_test.go @@ -32,7 +32,6 @@ func TestShoutrrrDefaultTemplate(t *testing.T) { func TestShoutrrrTemplate(t *testing.T) { cmd := new(cobra.Command) flags.RegisterNotificationFlags(cmd) - flags.BindViperFlags(cmd) err := cmd.ParseFlags([]string{"--notification-template={{range .}}{{.Level}}: {{.Message}}{{println}}{{end}}"}) require.NoError(t, err) @@ -56,7 +55,6 @@ func TestShoutrrrTemplate(t *testing.T) { func TestShoutrrrStringFunctions(t *testing.T) { cmd := new(cobra.Command) flags.RegisterNotificationFlags(cmd) - flags.BindViperFlags(cmd) err := cmd.ParseFlags([]string{"--notification-template={{range .}}{{.Level | printf \"%v\" | ToUpper }}: {{.Message | ToLower }} {{.Message | Title }}{{println}}{{end}}"}) require.NoError(t, err) @@ -79,8 +77,8 @@ func TestShoutrrrStringFunctions(t *testing.T) { func TestShoutrrrInvalidTemplateUsesTemplate(t *testing.T) { cmd := new(cobra.Command) + flags.RegisterNotificationFlags(cmd) - flags.BindViperFlags(cmd) err := cmd.ParseFlags([]string{"--notification-template={{"}) require.NoError(t, err) @@ -110,7 +108,7 @@ type blockingRouter struct { sent chan bool } -func (b blockingRouter) Send(_ string, _ *types.Params) []error { +func (b blockingRouter) Send(message string, params *types.Params) []error { _ = <-b.unlock b.sent <- true return nil diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go index 9129f57..5f96390 100644 --- a/pkg/notifications/slack.go +++ b/pkg/notifications/slack.go @@ -5,7 +5,6 @@ import ( "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/spf13/viper" ) const ( @@ -16,13 +15,14 @@ type slackTypeNotifier struct { slackrus.SlackrusHook } -func newSlackNotifier(_ *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { + flags := c.PersistentFlags() - hookURL := viper.GetString("notification-slack-hook-url") - userName := viper.GetString("notification-slack-identifier") - channel := viper.GetString("notification-slack-channel") - emoji := viper.GetString("notification-slack-icon-emoji") - iconURL := viper.GetString("notification-slack-icon-url") + hookURL, _ := flags.GetString("notification-slack-hook-url") + userName, _ := flags.GetString("notification-slack-identifier") + channel, _ := flags.GetString("notification-slack-channel") + emoji, _ := flags.GetString("notification-slack-icon-emoji") + iconURL, _ := flags.GetString("notification-slack-icon-url") n := &slackTypeNotifier{ SlackrusHook: slackrus.SlackrusHook{ From 5983d58d7c6ff6127fb74a67944ccc4814789f3a Mon Sep 17 00:00:00 2001 From: Turtle Kalus Date: Tue, 22 Dec 2020 15:48:38 -0800 Subject: [PATCH 19/98] Log based on registry known-support - reduce noise on notifications (#716) Log based on registry known-poor support of HEAD in checking container manifest. Some private registries do not support HEAD (E.G. GitLab Container Registry). With the current config, this log message is causing a notification to be sent for each container hosted in a registry lacking HEAD support. log.Debug or log.Warning for failed HTTP HEAD-check based on registry hostname where HEAD-check is known to fail. For Docker Hub, a failed HEAD leading to a "regular pull" may count against a user's call-quota whereas other registry implementations do not support HEAD, or whose container manifest may be in a different location. --- pkg/container/client.go | 6 +++++- pkg/registry/registry.go | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/pkg/container/client.go b/pkg/container/client.go index 2063332..635aa3e 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -295,7 +295,11 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e log.WithFields(fields).Debugf("Checking if pull is needed") if match, err := digest.CompareDigest(container, opts.RegistryAuth); err != nil { - log.Info("Could not do a head request, falling back to regular pull.") + if registry.WarnOnAPIConsumption(container) { + log.WithFields(fields).Warning("Could not do a head request, falling back to regular pull.") + } else { + log.Debug("Could not do a head request, falling back to regular pull.") + } log.Debugf("Reason: %s", err.Error()) } else if match { log.Debug("No pull needed. Skipping image.") diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 98eab0e..9edd66f 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -1,6 +1,9 @@ package registry import ( + "github.com/containrrr/watchtower/pkg/registry/helpers" + watchtowerTypes "github.com/containrrr/watchtower/pkg/types" + ref "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" log "github.com/sirupsen/logrus" ) @@ -31,3 +34,26 @@ func DefaultAuthHandler() (string, error) { log.Debug("Authentication request was rejected. Trying again without authentication") return "", nil } + +// WarnOnAPIConsumption will return true if the registry is known-expected +// to respond well to HTTP HEAD in checking the container digest -- or if there +// are problems parsing the container hostname. +// Will return false if behavior for container is unknown. +func WarnOnAPIConsumption(container watchtowerTypes.Container) bool { + + normalizedName, err := ref.ParseNormalizedNamed(container.ImageName()) + if err != nil { + return true + } + + containerHost, err := helpers.NormalizeRegistry(normalizedName.String()) + if err != nil { + return true + } + + if containerHost == "index.docker.io" || containerHost == "ghcr.io" { + return true + } + + return false +} From 2fb1f5f7eeeb0dfb97162af36f7ea3b0d9d3da81 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Wed, 23 Dec 2020 00:48:50 +0100 Subject: [PATCH 20/98] docs: add tkalus as a contributor (#721) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 3365865..d084e9f 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -702,6 +702,15 @@ "contributions": [ "code" ] + }, + { + "login": "tkalus", + "name": "Turtle Kalus", + "avatar_url": "https://avatars2.githubusercontent.com/u/287181?v=4", + "profile": "https://github.com/tkalus", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 25e6474..cba30b1 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
yrien30

💻
ksurl

📖
rg9400

💻 +
Turtle Kalus

💻 From 3bbe1bd109070ec546107cdaf250c1b182ccad2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 2 Jan 2021 14:32:05 +0100 Subject: [PATCH 21/98] fix manifest tag index in manifest.go (#731) Co-authored-by: Simon Aronsson --- pkg/registry/manifest/manifest.go | 9 +++++---- pkg/registry/manifest/manifest_test.go | 9 +++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pkg/registry/manifest/manifest.go b/pkg/registry/manifest/manifest.go index aac7762..facbb6c 100644 --- a/pkg/registry/manifest/manifest.go +++ b/pkg/registry/manifest/manifest.go @@ -20,7 +20,7 @@ func BuildManifestURL(container types.Container) (string, error) { } host, err := helpers.NormalizeRegistry(normalizedName.String()) - img, tag := extractImageAndTag(strings.TrimPrefix(container.ImageName(), host+"/")) + img, tag := ExtractImageAndTag(strings.TrimPrefix(container.ImageName(), host+"/")) logrus.WithFields(logrus.Fields{ "image": img, @@ -45,15 +45,16 @@ func BuildManifestURL(container types.Container) (string, error) { return url.String(), nil } -func extractImageAndTag(imageName string) (string, string) { +// 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 = fmt.Sprintf("%s%s", parts[0], parts[1]) - tag = parts[3] + img = parts[0] + tag = strings.Join(parts[1:], ":") } else { img = parts[0] tag = parts[1] diff --git a/pkg/registry/manifest/manifest_test.go b/pkg/registry/manifest/manifest_test.go index 3b86f90..95f196b 100644 --- a/pkg/registry/manifest/manifest_test.go +++ b/pkg/registry/manifest/manifest_test.go @@ -61,6 +61,15 @@ var _ = Describe("the manifest module", func() { Expect(err).NotTo(HaveOccurred()) Expect(res).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" + + imageOut, tagOut := manifest.ExtractImageAndTag(in) + + Expect(imageOut).To(Equal(image)) + Expect(tagOut).To(Equal(tag)) + }) }) }) From 35490c853d95f9ead6799bd2a16c991a0e1a0ddf Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Wed, 6 Jan 2021 20:06:56 +0100 Subject: [PATCH 22/98] cherrypick notification changes from #450 (#745) --- cmd/root.go | 23 ++-- pkg/notifications/email.go | 132 ++++++++--------------- pkg/notifications/gotify.go | 132 ++++++++++------------- pkg/notifications/msteams.go | 126 +++++----------------- pkg/notifications/notifier.go | 40 +++++-- pkg/notifications/notifier_test.go | 163 +++++++++++++++++++++++++++++ pkg/notifications/shoutrrr.go | 15 ++- pkg/notifications/slack.go | 32 +++++- pkg/types/convertable_notifier.go | 7 ++ 9 files changed, 375 insertions(+), 295 deletions(-) create mode 100644 pkg/notifications/notifier_test.go create mode 100644 pkg/types/convertable_notifier.go diff --git a/cmd/root.go b/cmd/root.go index 1e61308..4308dd1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -34,15 +34,20 @@ var ( scope string ) -var rootCmd = &cobra.Command{ - Use: "watchtower", - Short: "Automatically updates running Docker containers", - Long: ` -Watchtower automatically updates running Docker containers whenever a new image is released. -More information available at https://github.com/containrrr/watchtower/. -`, - Run: Run, - PreRun: PreRun, +var rootCmd = NewRootCommand() + +// NewRootCommand creates the root command for watchtower +func NewRootCommand() *cobra.Command { + return &cobra.Command{ + Use: "watchtower", + Short: "Automatically updates running Docker containers", + Long: ` + Watchtower automatically updates running Docker containers whenever a new image is released. + More information available at https://github.com/containrrr/watchtower/. + `, + Run: Run, + PreRun: PreRun, + } } func init() { diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index 6079de7..2356978 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -1,29 +1,22 @@ package notifications import ( - "encoding/base64" - "fmt" - "github.com/spf13/cobra" - "net/smtp" "os" - "strings" "time" + "github.com/spf13/cobra" + + shoutrrrSmtp "github.com/containrrr/shoutrrr/pkg/services/smtp" t "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" - "strconv" ) const ( emailType = "email" ) -// Implements Notifier, logrus.Hook -// The default logrus email integration would have several issues: -// - It would send one email per log output -// - It would only send errors -// We work around that by holding on to log entries until the update cycle is done. type emailTypeNotifier struct { + url string From, To string Server, User, Password, SubjectTag string Port int @@ -33,7 +26,12 @@ type emailTypeNotifier struct { delay time.Duration } -func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +// NewEmailNotifier is a factory method creating a new email notifier instance +func NewEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { + return newEmailNotifier(c, acceptedLogLevels) +} + +func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { flags := c.PersistentFlags() from, _ := flags.GetString("notification-email-from") @@ -47,6 +45,7 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie subjecttag, _ := flags.GetString("notification-email-subjecttag") n := &emailTypeNotifier{ + entries: []*log.Entry{}, From: from, To: to, Server: server, @@ -59,12 +58,33 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie SubjectTag: subjecttag, } - log.AddHook(n) - return n } -func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte { +func (e *emailTypeNotifier) GetURL() string { + conf := &shoutrrrSmtp.Config{ + FromAddress: e.From, + FromName: "Watchtower", + ToAddresses: []string{e.To}, + Port: uint16(e.Port), + Host: e.Server, + Subject: e.getSubject(), + Username: e.User, + Password: e.Password, + UseStartTLS: true, + UseHTML: false, + } + + if len(e.User) > 0 { + conf.Set("auth", "Plain") + } else { + conf.Set("auth", "None") + } + + return conf.GetURL().String() +} + +func (e *emailTypeNotifier) getSubject() string { var emailSubject string if e.SubjectTag == "" { @@ -75,83 +95,13 @@ func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte { if hostname, err := os.Hostname(); err == nil { emailSubject += " on " + hostname } - body := "" - for _, entry := range entries { - body += entry.Time.Format("2006-01-02 15:04:05") + " (" + entry.Level.String() + "): " + entry.Message + "\r\n" - // We don't use fields in watchtower, so don't bother sending them. - } - - t := time.Now() - - header := make(map[string]string) - header["From"] = e.From - header["To"] = e.To - header["Subject"] = emailSubject - header["Date"] = t.Format(time.RFC1123Z) - header["MIME-Version"] = "1.0" - header["Content-Type"] = "text/plain; charset=\"utf-8\"" - header["Content-Transfer-Encoding"] = "base64" - - message := "" - for k, v := range header { - message += fmt.Sprintf("%s: %s\r\n", k, v) - } - - encodedBody := base64.StdEncoding.EncodeToString([]byte(body)) - //RFC 2045 base64 encoding demands line no longer than 76 characters. - for _, line := range SplitSubN(encodedBody, 76) { - message += "\r\n" + line - } - - return []byte(message) + return emailSubject } -func (e *emailTypeNotifier) sendEntries(entries []*log.Entry) { - // Do the sending in a separate goroutine so we don't block the main process. - msg := e.buildMessage(entries) - go func() { - if e.delay > 0 { - time.Sleep(e.delay) - } - - var auth smtp.Auth - if e.User != "" { - auth = smtp.PlainAuth("", e.User, e.Password, e.Server) - } - err := SendMail(e.Server+":"+strconv.Itoa(e.Port), e.tlsSkipVerify, auth, e.From, strings.Split(e.To, ","), msg) - if err != nil { - // Use fmt so it doesn't trigger another email. - fmt.Println("Failed to send notification email: ", err) - } - }() -} - -func (e *emailTypeNotifier) StartNotification() { - if e.entries == nil { - e.entries = make([]*log.Entry, 0, 10) - } -} - -func (e *emailTypeNotifier) SendNotification() { - if e.entries == nil || len(e.entries) <= 0 { - return - } - - e.sendEntries(e.entries) - e.entries = nil -} - -func (e *emailTypeNotifier) Levels() []log.Level { - return e.logLevels -} - -func (e *emailTypeNotifier) Fire(entry *log.Entry) error { - if e.entries != nil { - e.entries = append(e.entries, entry) - } else { - e.sendEntries([]*log.Entry{entry}) - } - return nil -} +// TODO: Delete these once all notifiers have been converted to shoutrrr +func (e *emailTypeNotifier) StartNotification() {} +func (e *emailTypeNotifier) SendNotification() {} +func (e *emailTypeNotifier) Levels() []log.Level { return nil } +func (e *emailTypeNotifier) Fire(entry *log.Entry) error { return nil } func (e *emailTypeNotifier) Close() {} diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go index 789f778..47bab40 100644 --- a/pkg/notifications/gotify.go +++ b/pkg/notifications/gotify.go @@ -1,16 +1,13 @@ package notifications import ( - "bytes" - "crypto/tls" - "encoding/json" - "fmt" - "net/http" "strings" + shoutrrrGotify "github.com/containrrr/shoutrrr/pkg/services/gotify" t "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) const ( @@ -24,10 +21,40 @@ type gotifyTypeNotifier struct { logLevels []log.Level } -func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +// NewGotifyNotifier is a factory method creating a new gotify notifier instance +func NewGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertableNotifier { + return newGotifyNotifier(c, levels) +} + +func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertableNotifier { flags := c.PersistentFlags() + url := getGotifyURL(flags) + token := getGotifyToken(flags) + + skipVerify, _ := flags.GetBool("notification-gotify-tls-skip-verify") + + n := &gotifyTypeNotifier{ + gotifyURL: url, + gotifyAppToken: token, + gotifyInsecureSkipVerify: skipVerify, + logLevels: levels, + } + + return n +} + +func getGotifyToken(flags *pflag.FlagSet) string { + gotifyToken, _ := flags.GetString("notification-gotify-token") + if len(gotifyToken) < 1 { + log.Fatal("Required argument --notification-gotify-token(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN(env) is empty.") + } + return gotifyToken +} + +func getGotifyURL(flags *pflag.FlagSet) string { gotifyURL, _ := flags.GetString("notification-gotify-url") + if len(gotifyURL) < 1 { log.Fatal("Required argument --notification-gotify-url(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_URL(env) is empty.") } else if !(strings.HasPrefix(gotifyURL, "http://") || strings.HasPrefix(gotifyURL, "https://")) { @@ -36,82 +63,29 @@ func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifi log.Warn("Using an HTTP url for Gotify is insecure") } - gotifyToken, _ := flags.GetString("notification-gotify-token") - if len(gotifyToken) < 1 { - log.Fatal("Required argument --notification-gotify-token(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN(env) is empty.") - } - - gotifyInsecureSkipVerify, _ := flags.GetBool("notification-gotify-tls-skip-verify") - - n := &gotifyTypeNotifier{ - gotifyURL: gotifyURL, - gotifyAppToken: gotifyToken, - gotifyInsecureSkipVerify: gotifyInsecureSkipVerify, - logLevels: acceptedLogLevels, - } - - log.AddHook(n) - - return n + return gotifyURL } -func (n *gotifyTypeNotifier) StartNotification() {} - -func (n *gotifyTypeNotifier) SendNotification() {} - -func (n *gotifyTypeNotifier) Close() {} - -func (n *gotifyTypeNotifier) Levels() []log.Level { - return n.logLevels -} - -func (n *gotifyTypeNotifier) getURL() string { +func (n *gotifyTypeNotifier) GetURL() string { url := n.gotifyURL - if !strings.HasSuffix(url, "/") { - url += "/" + + if strings.HasPrefix(url, "https://") { + url = strings.TrimPrefix(url, "https://") + } else { + url = strings.TrimPrefix(url, "http://") } - return url + "message?token=" + n.gotifyAppToken + + url = strings.TrimSuffix(url, "/") + + config := &shoutrrrGotify.Config{ + Host: url, + Token: n.gotifyAppToken, + } + + return config.GetURL().String() } -func (n *gotifyTypeNotifier) Fire(entry *log.Entry) error { - - go func() { - jsonBody, err := json.Marshal(gotifyMessage{ - Message: "(" + entry.Level.String() + "): " + entry.Message, - Title: "Watchtower", - Priority: 0, - }) - if err != nil { - fmt.Println("Failed to create JSON body for Gotify notification: ", err) - return - } - - // Explicitly define the client so we can set InsecureSkipVerify to the desired value. - client := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: n.gotifyInsecureSkipVerify, - }, - }, - } - jsonBodyBuffer := bytes.NewBuffer([]byte(jsonBody)) - resp, err := client.Post(n.getURL(), "application/json", jsonBodyBuffer) - if err != nil { - fmt.Println("Failed to send Gotify notification: ", err) - return - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - fmt.Printf("Gotify notification returned %d HTTP status code", resp.StatusCode) - } - - }() - return nil -} - -type gotifyMessage struct { - Message string `json:"message"` - Title string `json:"title"` - Priority int `json:"priority"` -} +func (n *gotifyTypeNotifier) StartNotification() {} +func (n *gotifyTypeNotifier) SendNotification() {} +func (n *gotifyTypeNotifier) Close() {} +func (n *gotifyTypeNotifier) Levels() []log.Level { return nil } diff --git a/pkg/notifications/msteams.go b/pkg/notifications/msteams.go index ab33966..0c99072 100644 --- a/pkg/notifications/msteams.go +++ b/pkg/notifications/msteams.go @@ -1,15 +1,12 @@ package notifications import ( - "bytes" - "encoding/json" - "fmt" - "github.com/spf13/cobra" - "net/http" + "strings" + shoutrrrTeams "github.com/containrrr/shoutrrr/pkg/services/teams" t "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" - "io/ioutil" + "github.com/spf13/cobra" ) const ( @@ -22,7 +19,12 @@ type msTeamsTypeNotifier struct { data bool } -func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +// NewMsTeamsNotifier is a factory method creating a new teams notifier instance +func NewMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { + return newMsTeamsNotifier(cmd, acceptedLogLevels) +} + +func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { flags := cmd.PersistentFlags() @@ -38,103 +40,29 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Not data: withData, } - log.AddHook(n) - return n } -func (n *msTeamsTypeNotifier) StartNotification() {} +func (n *msTeamsTypeNotifier) GetURL() string { -func (n *msTeamsTypeNotifier) SendNotification() {} + baseURL := "https://outlook.office.com/webhook/" -func (n *msTeamsTypeNotifier) Close() {} + path := strings.Replace(n.webHookURL, baseURL, "", 1) + rawToken := strings.Replace(path, "/IncomingWebhook", "", 1) + token := strings.Split(rawToken, "/") + config := &shoutrrrTeams.Config{ + Token: shoutrrrTeams.Token{ + A: token[0], + B: token[1], + C: token[2], + }, + } -func (n *msTeamsTypeNotifier) Levels() []log.Level { - return n.levels + return config.GetURL().String() } -func (n *msTeamsTypeNotifier) Fire(entry *log.Entry) error { - - message := "(" + entry.Level.String() + "): " + entry.Message - - go func() { - webHookBody := messageCard{ - CardType: "MessageCard", - Context: "http://schema.org/extensions", - Markdown: true, - Text: message, - } - - if n.data && entry.Data != nil && len(entry.Data) > 0 { - section := messageCardSection{ - Facts: make([]messageCardSectionFact, len(entry.Data)), - Text: "", - } - - index := 0 - for k, v := range entry.Data { - section.Facts[index] = messageCardSectionFact{ - Name: k, - Value: fmt.Sprint(v), - } - index++ - } - - webHookBody.Sections = []messageCardSection{section} - } - - jsonBody, err := json.Marshal(webHookBody) - if err != nil { - fmt.Println("Failed to build JSON body for MSTeams notificattion: ", err) - return - } - - resp, err := http.Post(n.webHookURL, "application/json", bytes.NewBuffer([]byte(jsonBody))) - if err != nil { - fmt.Println("Failed to send MSTeams notificattion: ", err) - } - - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode > 299 { - fmt.Println("Failed to send MSTeams notificattion. HTTP RESPONSE STATUS: ", resp.StatusCode) - if resp.Body != nil { - bodyBytes, err := ioutil.ReadAll(resp.Body) - if err == nil { - bodyString := string(bodyBytes) - fmt.Println(bodyString) - } - } - } - }() - - return nil -} - -type messageCard struct { - CardType string `json:"@type"` - Context string `json:"@context"` - CorrelationID string `json:"correlationId,omitempty"` - ThemeColor string `json:"themeColor,omitempty"` - Summary string `json:"summary,omitempty"` - Title string `json:"title,omitempty"` - Text string `json:"text,omitempty"` - Markdown bool `json:"markdown,bool"` - Sections []messageCardSection `json:"sections,omitempty"` -} - -type messageCardSection struct { - Title string `json:"title,omitempty"` - Text string `json:"text,omitempty"` - ActivityTitle string `json:"activityTitle,omitempty"` - ActivitySubtitle string `json:"activitySubtitle,omitempty"` - ActivityImage string `json:"activityImage,omitempty"` - ActivityText string `json:"activityText,omitempty"` - HeroImage string `json:"heroImage,omitempty"` - Facts []messageCardSectionFact `json:"facts,omitempty"` -} - -type messageCardSectionFact struct { - Name string `json:"name,omitempty"` - Value string `json:"value,omitempty"` -} +func (n *msTeamsTypeNotifier) StartNotification() {} +func (n *msTeamsTypeNotifier) SendNotification() {} +func (n *msTeamsTypeNotifier) Close() {} +func (n *msTeamsTypeNotifier) Levels() []log.Level { return nil } +func (n *msTeamsTypeNotifier) Fire(entry *log.Entry) error { return nil } diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index dedb21a..dea0fc8 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -31,26 +31,48 @@ func NewNotifier(c *cobra.Command) *Notifier { if err != nil { log.WithField("could not read notifications argument", log.Fields{"Error": err}).Fatal() } + + n.types = n.GetNotificationTypes(c, acceptedLogLevels, types) + + return n +} + +// GetNotificationTypes produces an array of notifiers from a list of types +func (n *Notifier) GetNotificationTypes(cmd *cobra.Command, levels []log.Level, types []string) []ty.Notifier { + output := make([]ty.Notifier, 0) + for _, t := range types { - var tn ty.Notifier + + if t == shoutrrrType { + output = append(output, newShoutrrrNotifier(cmd, levels)) + continue + } + + var legacyNotifier ty.ConvertableNotifier + switch t { case emailType: - tn = newEmailNotifier(c, acceptedLogLevels) + legacyNotifier = newEmailNotifier(cmd, []log.Level{}) case slackType: - tn = newSlackNotifier(c, acceptedLogLevels) + legacyNotifier = newSlackNotifier(cmd, []log.Level{}) case msTeamsType: - tn = newMsTeamsNotifier(c, acceptedLogLevels) + legacyNotifier = newMsTeamsNotifier(cmd, levels) case gotifyType: - tn = newGotifyNotifier(c, acceptedLogLevels) - case shoutrrrType: - tn = newShoutrrrNotifier(c, acceptedLogLevels) + legacyNotifier = newGotifyNotifier(cmd, []log.Level{}) default: log.Fatalf("Unknown notification type %q", t) } - n.types = append(n.types, tn) + + notifier := newShoutrrrNotifierFromURL( + cmd, + legacyNotifier.GetURL(), + levels, + ) + + output = append(output, notifier) } - return n + return output } // StartNotification starts a log batch. Notifications will be accumulated after this point and only sent when SendNotification() is called. diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go new file mode 100644 index 0000000..6440bbc --- /dev/null +++ b/pkg/notifications/notifier_test.go @@ -0,0 +1,163 @@ +package notifications_test + +import ( + "fmt" + "os" + "testing" + + "github.com/containrrr/watchtower/cmd" + "github.com/containrrr/watchtower/internal/flags" + "github.com/containrrr/watchtower/pkg/notifications" + "github.com/containrrr/watchtower/pkg/types" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func TestActions(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Notifier Suite") +} + +var _ = Describe("notifications", func() { + // TODO: Either, we delete this test or we need to pass it valid URLs in the cobra command. + // --- + // When("getting notifiers from a types array", func() { + // It("should return the same amount of notifiers a string entries", func() { + + // notifier := ¬ifications.Notifier{} + // notifiers := notifier.GetNotificationTypes(&cobra.Command{}, []log.Level{}, []string{"slack", "email"}) + // Expect(len(notifiers)).To(Equal(2)) + // }) + // }) + Describe("the slack notifier", func() { + When("converting a slack service config into a shoutrrr url", func() { + builderFn := notifications.NewSlackNotifier + + It("should return the expected URL", func() { + + username := "containrrrbot" + tokenA := "aaa" + tokenB := "bbb" + tokenC := "ccc" + + password := fmt.Sprintf("%s-%s-%s", tokenA, tokenB, tokenC) + hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC) + expectedOutput := fmt.Sprintf("slack://%s:%s@%s/%s/%s", username, password, tokenA, tokenB, tokenC) + + args := []string{ + "--notification-slack-hook-url", + hookURL, + "--notification-slack-identifier", + username, + } + + testURL(builderFn, args, expectedOutput) + }) + }) + }) + + Describe("the gotify notifier", func() { + When("converting a gotify service config into a shoutrrr url", func() { + builderFn := notifications.NewGotifyNotifier + + It("should return the expected URL", func() { + token := "aaa" + host := "shoutrrr.local" + + expectedOutput := fmt.Sprintf("gotify://%s/%s", host, token) + + args := []string{ + "--notification-gotify-url", + fmt.Sprintf("https://%s", host), + "--notification-gotify-token", + token, + } + + testURL(builderFn, args, expectedOutput) + }) + }) + }) + + Describe("the teams notifier", func() { + When("converting a teams service config into a shoutrrr url", func() { + builderFn := notifications.NewMsTeamsNotifier + + It("should return the expected URL", func() { + + tokenA := "aaa" + tokenB := "bbb" + tokenC := "ccc" + + hookURL := fmt.Sprintf("https://outlook.office.com/webhook/%s/IncomingWebhook/%s/%s", tokenA, tokenB, tokenC) + expectedOutput := fmt.Sprintf("teams://%s:%s@%s", tokenA, tokenB, tokenC) + + args := []string{ + "--notification-msteams-hook", + hookURL, + } + + testURL(builderFn, args, expectedOutput) + }) + }) + }) + + Describe("the email notifier", func() { + + builderFn := notifications.NewEmailNotifier + + When("converting an email service config into a shoutrrr url", func() { + It("should set the from address in the URL", func() { + fromAddress := "lala@example.com" + expectedOutput := buildExpectedURL("", "", "", 25, fromAddress, "", "None") + args := []string{ + "--notification-email-from", + fromAddress, + } + testURL(builderFn, args, expectedOutput) + }) + + It("should return the expected URL", func() { + + fromAddress := "sender@example.com" + toAddress := "receiver@example.com" + expectedOutput := buildExpectedURL("", "", "", 25, fromAddress, toAddress, "None") + + args := []string{ + "--notification-email-from", + fromAddress, + "--notification-email-to", + toAddress, + } + + testURL(builderFn, args, expectedOutput) + }) + }) + }) +}) + +func buildExpectedURL(username string, password string, host string, port int, from string, to string, auth string) string { + hostname, err := os.Hostname() + Expect(err).NotTo(HaveOccurred()) + + subject := fmt.Sprintf("Watchtower updates on %s", hostname) + + var template = "smtp://%s:%s@%s:%d/?fromAddress=%s&fromName=Watchtower&toAddresses=%s&auth=%s&subject=%s&startTls=Yes&useHTML=No" + return fmt.Sprintf(template, username, password, host, port, from, to, auth, subject) +} + +type builderFn = func(c *cobra.Command, acceptedLogLevels []log.Level) types.ConvertableNotifier + +func testURL(builder builderFn, args []string, expectedURL string) { + + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + command.ParseFlags(args) + + notifier := builder(command, []log.Level{}) + actualURL := notifier.GetURL() + + Expect(actualURL).To(Equal(expectedURL)) +} diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index d16808d..2715711 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -35,8 +35,17 @@ type shoutrrrTypeNotifier struct { func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { flags := c.PersistentFlags() - urls, _ := flags.GetStringArray("notification-url") + template := getShoutrrrTemplate(c) + return createSender(urls, acceptedLogLevels, template) +} + +func newShoutrrrNotifierFromURL(c *cobra.Command, url string, levels []log.Level) t.Notifier { + template := getShoutrrrTemplate(c) + return createSender([]string{url}, levels, template) +} + +func createSender(urls []string, levels []log.Level, template *template.Template) t.Notifier { r, err := shoutrrr.CreateSender(urls...) if err != nil { log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error()) @@ -45,10 +54,10 @@ func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Noti n := &shoutrrrTypeNotifier{ Urls: urls, Router: r, - logLevels: acceptedLogLevels, - template: getShoutrrrTemplate(c), messages: make(chan string, 1), done: make(chan bool), + logLevels: levels, + template: template, } log.AddHook(n) diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go index 5f96390..f8cbd45 100644 --- a/pkg/notifications/slack.go +++ b/pkg/notifications/slack.go @@ -1,6 +1,9 @@ package notifications import ( + "strings" + + shoutrrrSlack "github.com/containrrr/shoutrrr/pkg/services/slack" t "github.com/containrrr/watchtower/pkg/types" "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" @@ -15,7 +18,12 @@ type slackTypeNotifier struct { slackrus.SlackrusHook } -func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +// NewSlackNotifier is a factory function used to generate new instance of the slack notifier type +func NewSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { + return newSlackNotifier(c, acceptedLogLevels) +} + +func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { flags := c.PersistentFlags() hookURL, _ := flags.GetString("notification-slack-hook-url") @@ -23,7 +31,6 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie channel, _ := flags.GetString("notification-slack-channel") emoji, _ := flags.GetString("notification-slack-icon-emoji") iconURL, _ := flags.GetString("notification-slack-icon-url") - n := &slackTypeNotifier{ SlackrusHook: slackrus.SlackrusHook{ HookURL: hookURL, @@ -34,12 +41,27 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie AcceptedLevels: acceptedLogLevels, }, } - - log.AddHook(n) return n } -func (s *slackTypeNotifier) StartNotification() {} +func (s *slackTypeNotifier) GetURL() string { + rawTokens := strings.Replace(s.HookURL, "https://hooks.slack.com/services/", "", 1) + tokens := strings.Split(rawTokens, "/") + + conf := &shoutrrrSlack.Config{ + BotName: s.Username, + Token: shoutrrrSlack.Token{ + A: tokens[0], + B: tokens[1], + C: tokens[2], + }, + } + + return conf.GetURL().String() +} + +func (s *slackTypeNotifier) StartNotification() { +} func (s *slackTypeNotifier) SendNotification() {} diff --git a/pkg/types/convertable_notifier.go b/pkg/types/convertable_notifier.go new file mode 100644 index 0000000..3d7ac82 --- /dev/null +++ b/pkg/types/convertable_notifier.go @@ -0,0 +1,7 @@ +package types + +// ConvertableNotifier is a notifier capable of creating a shoutrrr URL +type ConvertableNotifier interface { + Notifier + GetURL() string +} From d7d5b2588277f0e07895a63c12a2117262bdf9fc Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Wed, 6 Jan 2021 22:28:32 +0100 Subject: [PATCH 23/98] Prometheus support (#450) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: nils måsén Co-authored-by: MihailITPlace Co-authored-by: Sebastiaan Tammer --- cmd/root.go | 39 +++- docker-compose.yml | 43 ++++ docs/arguments.md | 16 +- docs/assets/grafana-dashboard.png | Bin 0 -> 32994 bytes docs/metrics.md | 26 +++ docs/notifications.md | 21 +- go.mod | 1 + grafana/dashboards/dashboard.json | 293 +++++++++++++++++++++++++ grafana/dashboards/dashboard.yml | 11 + grafana/datasources/datasource.yml | 8 + internal/actions/actions_suite_test.go | 3 +- internal/actions/update.go | 81 +++++-- internal/actions/update_test.go | 10 +- internal/flags/flags.go | 9 +- mkdocs.yml | 1 + pkg/api/api.go | 105 +++++---- pkg/api/metrics/metrics.go | 27 +++ pkg/api/metrics/metrics_test.go | 77 +++++++ pkg/api/update/update.go | 50 +++++ pkg/metrics/metrics.go | 91 ++++++++ pkg/notifications/gotify.go | 2 +- pkg/notifications/msteams.go | 2 +- prometheus/prometheus.yml | 9 + 23 files changed, 819 insertions(+), 106 deletions(-) create mode 100644 docker-compose.yml create mode 100644 docs/assets/grafana-dashboard.png create mode 100644 docs/metrics.md create mode 100644 grafana/dashboards/dashboard.json create mode 100644 grafana/dashboards/dashboard.yml create mode 100644 grafana/datasources/datasource.yml create mode 100644 pkg/api/metrics/metrics.go create mode 100644 pkg/api/metrics/metrics_test.go create mode 100644 pkg/api/update/update.go create mode 100644 pkg/metrics/metrics.go create mode 100644 prometheus/prometheus.yml diff --git a/cmd/root.go b/cmd/root.go index 4308dd1..0aeeac6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,12 +1,16 @@ package cmd import ( + metrics2 "github.com/containrrr/watchtower/pkg/metrics" "os" "os/signal" "strconv" "syscall" "time" + "github.com/containrrr/watchtower/pkg/api/metrics" + "github.com/containrrr/watchtower/pkg/api/update" + "github.com/containrrr/watchtower/internal/actions" "github.com/containrrr/watchtower/internal/flags" "github.com/containrrr/watchtower/pkg/api" @@ -144,7 +148,10 @@ func PreRun(cmd *cobra.Command, args []string) { func Run(c *cobra.Command, names []string) { filter := filters.BuildFilter(names, enableLabel, scope) runOnce, _ := c.PersistentFlags().GetBool("run-once") - httpAPI, _ := c.PersistentFlags().GetBool("http-api") + enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update") + enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics") + + apiToken, _ := c.PersistentFlags().GetString("http-api-token") if runOnce { if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage { @@ -160,17 +167,20 @@ func Run(c *cobra.Command, names []string) { log.Fatal(err) } - if httpAPI { - apiToken, _ := c.PersistentFlags().GetString("http-api-token") + httpAPI := api.New(apiToken) - if err := api.SetupHTTPUpdates(apiToken, func() { runUpdatesWithNotifications(filter) }); err != nil { - log.Fatal(err) - os.Exit(1) - } - - api.WaitForHTTPUpdates() + if enableUpdateAPI { + updateHandler := update.New(func() { runUpdatesWithNotifications(filter) }) + httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle) } + if enableMetricsAPI { + metricsHandler := metrics.New() + httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle) + } + + httpAPI.Start(enableUpdateAPI) + if err := runUpgradesOnSchedule(c, filter); err != nil { log.Error(err) } @@ -189,8 +199,11 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { select { case v := <-tryLockSem: defer func() { tryLockSem <- v }() - runUpdatesWithNotifications(filter) + metric := runUpdatesWithNotifications(filter) + metrics2.RegisterScan(metric) default: + // Update was skipped + metrics2.RegisterScan(nil) log.Debug("Skipped another update already running.") } @@ -222,7 +235,8 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { return nil } -func runUpdatesWithNotifications(filter t.Filter) { +func runUpdatesWithNotifications(filter t.Filter) *metrics2.Metric { + notifier.StartNotification() updateParams := t.UpdateParams{ Filter: filter, @@ -233,9 +247,10 @@ func runUpdatesWithNotifications(filter t.Filter) { LifecycleHooks: lifecycleHooks, RollingRestart: rollingRestart, } - err := actions.Update(client, updateParams) + metrics, err := actions.Update(client, updateParams) if err != nil { log.Println(err) } notifier.SendNotification() + return metrics } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b0a3373 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3.7' + +services: + watchtower: + container_name: watchtower + build: + context: ./ + dockerfile: dockerfiles/Dockerfile.dev-self-contained + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + ports: + - 8080:8080 + command: --interval 10 --http-api-metrics --http-api-token demotoken --debug prometheus grafana parent child + prometheus: + container_name: prometheus + image: prom/prometheus + volumes: + - ./prometheus/:/etc/prometheus/ + - prometheus:/prometheus/ + ports: + - 9090:9090 + grafana: + container_name: grafana + image: grafana/grafana + ports: + - 3000:3000 + environment: + GF_INSTALL_PLUGINS: grafana-clock-panel,grafana-simple-json-datasource + volumes: + - grafana:/var/lib/grafana + - ./grafana:/etc/grafana/provisioning + parent: + image: nginx + container_name: parent + child: + image: nginx:alpine + labels: + com.centurylinklabs.watchtower.depends-on: parent + container_name: child + +volumes: + prometheus: {} + grafana: {} diff --git a/docs/arguments.md b/docs/arguments.md index a36c438..4fb56c6 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -164,7 +164,7 @@ Environment Variable: WATCHTOWER_LABEL_ENABLE ## 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 containers. -> ### ⚠️ Please note +> **⚠️ Please note** > > Due to Docker API limitations the latest image will still be pulled from the registry. @@ -238,9 +238,7 @@ Sets an authentication token to HTTP API requests. Environment Variable: WATCHTOWER_HTTP_API_TOKEN Type: String Default: - -``` - -## Filter by scope +```## Filter by scope 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.github.io/watchtower/running-multiple-instances). ``` @@ -250,6 +248,16 @@ Environment Variable: WATCHTOWER_SCOPE Default: - ``` +## HTTP API Metrics +Enables a metrics endpoint, exposing prometheus metrics via HTTP. See [Metrics](metrics.md) for details. + +``` + Argument: --http-api-metrics +Environment Variable: WATCHTOWER_HTTP_API_METRICS + Type: Boolean + Default: false +``` + ## Scheduling [Cron expression](https://pkg.go.dev/github.com/robfig/cron@v1.2.0?tab=doc#hdr-CRON_Expression_Format) in 6 fields (rather than the traditional 5) which defines when and how often to check for new images. Either `--interval` or the schedule expression can be defined, but not both. An example: `--schedule "0 0 4 * * *"` diff --git a/docs/assets/grafana-dashboard.png b/docs/assets/grafana-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..faab4229cafa6bbf67e82f994fac5a6b012ddd6c GIT binary patch literal 32994 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 **⚠️ Experimental feature** +> +> This feature was added in v1.0.4 and is still considered experimental. +> If you notice any strange behavior, please raise a ticket in the repository issues. + +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), +as well as creating a port mapping for your container for port `8080`. + +## Available Metrics + +| Name | Type | Description | +| ------------------------------- | ------- | --------------------------------------------------------------------------- | +| `watchtower_containers_scanned` | Gauge | Number of containers scanned for changes by watchtower during the last scan | +| `watchtower_containers_updated` | Gauge | Number of containers updated by watchtower during the last scan | +| `watchtower_containers_failed` | Gauge | Number of containers where update failed during the last scan | +| `watchtower_scans_total` | Counter | Number of scans since the watchtower started | +| `watchtower_scans_skipped` | Counter | Number of skipped scans since watchtower started | + +## Demo + +The repository contains a demo with prometheus and grafana, available through `docker-compose.yml`. This demo +is preconfigured with a dashboard, which will look something like this: + +![grafana metrics](assets/grafana-dashboard.png) \ No newline at end of file diff --git a/docs/notifications.md b/docs/notifications.md index afa23bd..57603cb 100644 --- a/docs/notifications.md +++ b/docs/notifications.md @@ -1,4 +1,3 @@ - # Notifications Watchtower can send notifications when containers are updated. Notifications are sent via hooks in the logging system, [logrus](http://github.com/sirupsen/logrus). @@ -12,14 +11,14 @@ The types of notifications to send are set by passing a comma-separated list of > There is currently a [bug](https://github.com/spf13/viper/issues/380) in Viper, which prevents comma-separated slices to be used when using the environment variable. A workaround is available where we instead put quotes around the environment variable value and replace the commas with spaces, as `WATCHTOWER_NOTIFICATIONS="slack msteams"` -> If you're a `docker-compose` user, make sure to specify environment variables' values in your `.yml` file without double quotes (`"`). +> If you're a `docker-compose` user, make sure to specify environment variables' values in your `.yml` file without double quotes (`"`). > > This prevents unexpected errors when watchtower starts. ## Settings - `--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`. -- 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. +- 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. ## Available services @@ -47,7 +46,7 @@ docker run -d \ -e WATCHTOWER_NOTIFICATION_EMAIL_FROM=fromaddress@gmail.com \ -e WATCHTOWER_NOTIFICATION_EMAIL_TO=toaddress@gmail.com \ -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.gmail.com \ - -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587 \ + -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=587 \ -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=fromaddress@gmail.com \ -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=app_password \ -e WATCHTOWER_NOTIFICATION_EMAIL_DELAY=2 \ @@ -56,19 +55,19 @@ docker run -d \ The previous example assumes, that you already have an SMTP server up and running you can connect to. If you don't or you want to bring up watchtower with your own simple SMTP relay the following `docker-compose.yml` might be a good start for you. -The following example assumes, that your domain is called `your-domain.com` and that you are going to use a certificate valid for `smtp.your-domain.com`. This hostname has to be used as `WATCHTOWER_NOTIFICATION_EMAIL_SERVER` otherwise the TLS connection is going to fail with `Failed to send notification email` or `connect: connection refused`. We also have to add a network for this setup in order to add an alias to it. If you also want to enable DKIM or other features on the SMTP server, you will find more information at [freinet/postfix-relay](https://hub.docker.com/r/freinet/postfix-relay). +The following example assumes, that your domain is called `your-domain.com` and that you are going to use a certificate valid for `smtp.your-domain.com`. This hostname has to be used as `WATCHTOWER_NOTIFICATION_EMAIL_SERVER` otherwise the TLS connection is going to fail with `Failed to send notification email` or `connect: connection refused`. We also have to add a network for this setup in order to add an alias to it. If you also want to enable DKIM or other features on the SMTP server, you will find more information at [freinet/postfix-relay](https://hub.docker.com/r/freinet/postfix-relay). Example including an SMTP relay: ```yaml --- -version: "3.8" +version: '3.8' services: watchtower: image: containrrr/watchtower:latest container_name: watchtower environment: - WATCHTOWER_MONITOR_ONLY: "true" + WATCHTOWER_MONITOR_ONLY: 'true' WATCHTOWER_NOTIFICATIONS: email WATCHTOWER_NOTIFICATION_EMAIL_FROM: from-address@your-domain.com WATCHTOWER_NOTIFICATION_EMAIL_TO: to-address@your-domain.com @@ -90,9 +89,9 @@ services: - 25 environment: MAILNAME: somename.your-domain.com - TLS_KEY: "/etc/ssl/domains/your-domain.com/your-domain.com.key" - TLS_CRT: "/etc/ssl/domains/your-domain.com/your-domain.com.crt" - TLS_CA: "/etc/ssl/domains/your-domain.com/intermediate.crt" + TLS_KEY: '/etc/ssl/domains/your-domain.com/your-domain.com.key' + TLS_CRT: '/etc/ssl/domains/your-domain.com/your-domain.com.crt' + TLS_CA: '/etc/ssl/domains/your-domain.com/intermediate.crt' volumes: - /etc/ssl/domains/your-domain.com/:/etc/ssl/domains/your-domain.com/:ro networks: @@ -172,7 +171,7 @@ docker run -d \ `-e WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN` or `--notification-gotify-token` can also reference a file, in which case the contents of the file are used. -If you want to disable TLS verification for the Gotify instance, you can use either `-e WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY=true` or `--notification-gotify-tls-skip-verify`. +If you want to disable TLS verification for the Gotify instance, you can use either `-e WATCHTOWER_NOTIFICATION_GOTIFY_TLS_SKIP_VERIFY=true` or `--notification-gotify-tls-skip-verify`. ### [containrrr/shoutrrr](https://github.com/containrrr/shoutrrr) diff --git a/go.mod b/go.mod index 0e37602..02d1253 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/runc v0.1.1 // indirect github.com/pkg/errors v0.8.1 // indirect + github.com/prometheus/client_golang v0.9.3 github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 github.com/sirupsen/logrus v1.4.1 github.com/spf13/cobra v0.0.3 diff --git a/grafana/dashboards/dashboard.json b/grafana/dashboards/dashboard.json new file mode 100644 index 0000000..998485b --- /dev/null +++ b/grafana/dashboards/dashboard.json @@ -0,0 +1,293 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": 1, + "links": [], + "panels": [ + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 1, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "7.3.6", + "targets": [ + { + "expr": "watchtower_scans_total", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Total Scans", + "type": "stat" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": null, + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "watchtower_containers_scanned{instance=\"watchtower:8080\", job=\"watchtower\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "Scanned" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "watchtower_containers_failed{instance=\"watchtower:8080\", job=\"watchtower\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "Faled" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "watchtower_containers_updated{instance=\"watchtower:8080\", job=\"watchtower\"}" + }, + "properties": [ + { + "id": "displayName", + "value": "Updated" + } + ] + } + ] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 6, + "x": 1, + "y": 0 + }, + "hiddenSeries": false, + "id": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null as zero", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.6", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "watchtower_containers_scanned", + "interval": "", + "legendFormat": "", + "refId": "A" + }, + { + "expr": "watchtower_containers_failed", + "interval": "", + "legendFormat": "", + "refId": "B" + }, + { + "expr": "watchtower_containers_updated", + "interval": "", + "legendFormat": "", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Container Updates", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "datasource": "Prometheus", + "fieldConfig": { + "defaults": { + "custom": {}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 1, + "x": 0, + "y": 4 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "7.3.6", + "targets": [ + { + "expr": "watchtower_scans_skipped", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Skipped Scans", + "type": "stat" + } + ], + "refresh": false, + "schemaVersion": 26, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Watchtower", + "uid": "d7bdoT-Gz", + "version": 1 +} \ No newline at end of file diff --git a/grafana/dashboards/dashboard.yml b/grafana/dashboards/dashboard.yml new file mode 100644 index 0000000..9f7232c --- /dev/null +++ b/grafana/dashboards/dashboard.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: 'Prometheus' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /etc/grafana/provisioning/dashboards \ No newline at end of file diff --git a/grafana/datasources/datasource.yml b/grafana/datasources/datasource.yml new file mode 100644 index 0000000..8049912 --- /dev/null +++ b/grafana/datasources/datasource.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true \ No newline at end of file diff --git a/internal/actions/actions_suite_test.go b/internal/actions/actions_suite_test.go index 7cbd71b..ffa6e2a 100644 --- a/internal/actions/actions_suite_test.go +++ b/internal/actions/actions_suite_test.go @@ -1,10 +1,11 @@ package actions_test import ( - "github.com/containrrr/watchtower/internal/actions" "testing" "time" + "github.com/containrrr/watchtower/internal/actions" + "github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/container/mocks" diff --git a/internal/actions/update.go b/internal/actions/update.go index e37e671..9320d6a 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -5,6 +5,7 @@ import ( "github.com/containrrr/watchtower/internal/util" "github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/lifecycle" + metrics2 "github.com/containrrr/watchtower/pkg/metrics" "github.com/containrrr/watchtower/pkg/sorter" "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" @@ -14,8 +15,10 @@ import ( // used to start those containers have been updated. If a change is detected in // any of the images, the associated containers are stopped and restarted with // the new image. -func Update(client container.Client, params types.UpdateParams) error { +func Update(client container.Client, params types.UpdateParams) (*metrics2.Metric, error) { log.Debug("Checking containers for updated images") + metric := &metrics2.Metric{} + staleCount := 0 if params.LifecycleHooks { lifecycle.ExecutePreChecks(client, params) @@ -23,9 +26,11 @@ func Update(client container.Client, params types.UpdateParams) error { containers, err := client.ListContainers(params.Filter) if err != nil { - return err + return nil, err } + staleCheckFailed := 0 + for i, targetContainer := range containers { stale, err := client.IsContainerStale(targetContainer) if stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly() && !targetContainer.HasImageInfo() { @@ -34,13 +39,20 @@ func Update(client container.Client, params types.UpdateParams) error { if err != nil { log.Infof("Unable to update container %q: %v. Proceeding to next.", containers[i].Name(), err) stale = false + staleCheckFailed++ + metric.Failed++ } containers[i].Stale = stale + + if stale { + staleCount++ + } } containers, err = sorter.SortByDependencies(containers) + metric.Scanned = len(containers) if err != nil { - return err + return nil, err } checkDependencies(containers) @@ -55,24 +67,32 @@ func Update(client container.Client, params types.UpdateParams) error { } if params.RollingRestart { - performRollingRestart(containersToUpdate, client, params) + metric.Failed += performRollingRestart(containersToUpdate, client, params) } else { - stopContainersInReversedOrder(containersToUpdate, client, params) - restartContainersInSortedOrder(containersToUpdate, client, params) + metric.Failed += stopContainersInReversedOrder(containersToUpdate, client, params) + metric.Failed += restartContainersInSortedOrder(containersToUpdate, client, params) } + + metric.Updated = staleCount - (metric.Failed - staleCheckFailed) + if params.LifecycleHooks { lifecycle.ExecutePostChecks(client, params) } - return nil + return metric, nil } -func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) { +func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) int { cleanupImageIDs := make(map[string]bool) + failed := 0 for i := len(containers) - 1; i >= 0; i-- { if containers[i].Stale { - stopStaleContainer(containers[i], client, params) - restartStaleContainer(containers[i], client, params) + if err := stopStaleContainer(containers[i], client, params); err != nil { + failed++ + } + if err := restartStaleContainer(containers[i], client, params); err != nil { + failed++ + } cleanupImageIDs[containers[i].ImageID()] = true } } @@ -80,50 +100,63 @@ func performRollingRestart(containers []container.Container, client container.Cl if params.Cleanup { cleanupImages(client, cleanupImageIDs) } + return failed } -func stopContainersInReversedOrder(containers []container.Container, client container.Client, params types.UpdateParams) { +func stopContainersInReversedOrder(containers []container.Container, client container.Client, params types.UpdateParams) int { + failed := 0 for i := len(containers) - 1; i >= 0; i-- { - stopStaleContainer(containers[i], client, params) + if err := stopStaleContainer(containers[i], client, params); err != nil { + failed++ + } } + return failed } -func stopStaleContainer(container container.Container, client container.Client, params types.UpdateParams) { +func stopStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error { if container.IsWatchtower() { log.Debugf("This is the watchtower container %s", container.Name()) - return + return nil } if !container.Stale { - return + return nil } if params.LifecycleHooks { if err := lifecycle.ExecutePreUpdateCommand(client, container); err != nil { log.Error(err) log.Info("Skipping container as the pre-update command failed") - return + return err } } if err := client.StopContainer(container, params.Timeout); err != nil { log.Error(err) + return err } + return nil } -func restartContainersInSortedOrder(containers []container.Container, client container.Client, params types.UpdateParams) { +func restartContainersInSortedOrder(containers []container.Container, client container.Client, params types.UpdateParams) int { imageIDs := make(map[string]bool) - for _, staleContainer := range containers { - if !staleContainer.Stale { + failed := 0 + + for _, c := range containers { + if !c.Stale { continue } - restartStaleContainer(staleContainer, client, params) - imageIDs[staleContainer.ImageID()] = true + if err := restartStaleContainer(c, client, params); err != nil { + failed++ + } + imageIDs[c.ImageID()] = true } if params.Cleanup { cleanupImages(client, imageIDs) } + + return failed } func cleanupImages(client container.Client, imageIDs map[string]bool) { @@ -134,7 +167,7 @@ func cleanupImages(client container.Client, imageIDs map[string]bool) { } } -func restartStaleContainer(container container.Container, client container.Client, params types.UpdateParams) { +func restartStaleContainer(container container.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 @@ -142,17 +175,19 @@ func restartStaleContainer(container container.Container, client container.Clien if container.IsWatchtower() { if err := client.RenameContainer(container, util.RandName()); err != nil { log.Error(err) - return + return nil } } if !params.NoRestart { if newContainerID, err := client.StartContainer(container); err != nil { log.Error(err) + return err } else if container.Stale && params.LifecycleHooks { lifecycle.ExecutePostUpdateCommand(client, newContainerID) } } + return nil } func checkDependencies(containers []container.Container) { diff --git a/internal/actions/update_test.go b/internal/actions/update_test.go index 1a53aad..f1b8e85 100644 --- a/internal/actions/update_test.go +++ b/internal/actions/update_test.go @@ -59,7 +59,7 @@ var _ = Describe("the update action", func() { When("there are multiple containers using the same image", func() { It("should only try to remove the image once", func() { - err := actions.Update(client, types.UpdateParams{Cleanup: true}) + _, err := actions.Update(client, types.UpdateParams{Cleanup: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) }) @@ -75,7 +75,7 @@ var _ = Describe("the update action", func() { time.Now(), ), ) - err := actions.Update(client, types.UpdateParams{Cleanup: true}) + _, err := actions.Update(client, types.UpdateParams{Cleanup: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(2)) }) @@ -83,7 +83,7 @@ var _ = Describe("the update action", func() { When("performing a rolling restart update", func() { It("should try to remove the image once", func() { - err := actions.Update(client, types.UpdateParams{Cleanup: true, RollingRestart: true}) + _, err := actions.Update(client, types.UpdateParams{Cleanup: true, RollingRestart: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) }) @@ -121,7 +121,7 @@ var _ = Describe("the update action", func() { }) It("should not update those containers", func() { - err := actions.Update(client, types.UpdateParams{Cleanup: true}) + _, err := actions.Update(client, types.UpdateParams{Cleanup: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) }) @@ -151,7 +151,7 @@ var _ = Describe("the update action", func() { }) It("should not update any containers", func() { - err := actions.Update(client, types.UpdateParams{MonitorOnly: true}) + _, err := actions.Update(client, types.UpdateParams{MonitorOnly: true}) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0)) }) diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 2f7a89f..d45f384 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -130,10 +130,15 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { "Restart containers one at a time") flags.BoolP( - "http-api", + "http-api-update", "", - viper.GetBool("WATCHTOWER_HTTP_API"), + viper.GetBool("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"), + "Runs Watchtower with the Prometheus metrics API enabled") flags.StringP( "http-api-token", diff --git a/mkdocs.yml b/mkdocs.yml index 1d1506b..f628fbc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,5 +28,6 @@ nav: - 'Stop signals': 'stop-signals.md' - 'Lifecycle hooks': 'lifecycle-hooks.md' - 'Running multiple instances': 'running-multiple-instances.md' + - 'Metrics': 'metrics.md' plugins: - search diff --git a/pkg/api/api.go b/pkg/api/api.go index 12d12c3..987e4bd 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -1,63 +1,76 @@ package api import ( - "errors" - "io" - "net/http" - "os" - + "fmt" log "github.com/sirupsen/logrus" + "net/http" ) -var ( - lock chan bool -) +const tokenMissingMsg = "api token is empty or has not been set. exiting" -func init() { - lock = make(chan bool, 1) - lock <- true +// API is the http server responsible for serving the HTTP API endpoints +type API struct { + Token string + hasHandlers bool } -// SetupHTTPUpdates configures the endpoint needed for triggering updates via http -func SetupHTTPUpdates(apiToken string, updateFunction func()) error { - if apiToken == "" { - return errors.New("api token is empty or has not been set. not starting api") +// New is a factory function creating a new API instance +func New(token string) *API { + return &API{ + Token: token, + hasHandlers: false, + } +} + +// RequireToken is wrapper around http.HandleFunc that checks token validity +func (api *API) RequireToken(fn http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", api.Token) { + log.Errorf("Invalid token \"%s\"", r.Header.Get("Authorization")) + log.Debugf("Expected token to be \"%s\"", api.Token) + return + } + log.Println("Valid token found.") + fn(w, r) + } +} + +// RegisterFunc is a wrapper around http.HandleFunc that also sets the flag used to determine whether to launch the API +func (api *API) RegisterFunc(path string, fn http.HandlerFunc) { + api.hasHandlers = true + http.HandleFunc(path, api.RequireToken(fn)) +} + +// RegisterHandler is a wrapper around http.Handler that also sets the flag used to determine whether to launch the API +func (api *API) RegisterHandler(path string, handler http.Handler) { + api.hasHandlers = true + http.Handle(path, api.RequireToken(handler.ServeHTTP)) +} + +// Start the API and serve over HTTP. Requires an API Token to be set. +func (api *API) Start(block bool) error { + + if !api.hasHandlers { + log.Debug("Watchtower HTTP API skipped.") + return nil } - log.Println("Watchtower HTTP API started.") - - http.HandleFunc("/v1/update", func(w http.ResponseWriter, r *http.Request) { - log.Info("Updates triggered by HTTP API request.") - - _, err := io.Copy(os.Stdout, r.Body) - if err != nil { - log.Println(err) - return - } - - if r.Header.Get("Token") != apiToken { - log.Println("Invalid token. Not updating.") - return - } - - log.Println("Valid token found. Attempting to update.") - - select { - case chanValue := <-lock: - defer func() { lock <- chanValue }() - updateFunction() - default: - log.Debug("Skipped. Another update already running.") - } - - }) + if api.Token == "" { + log.Fatal(tokenMissingMsg) + } + log.Info("Watchtower HTTP API started.") + if block { + runHTTPServer() + } else { + go func() { + runHTTPServer() + }() + } return nil } -// WaitForHTTPUpdates starts the http server and listens for requests. -func WaitForHTTPUpdates() error { +func runHTTPServer() { + log.Info("Serving HTTP") log.Fatal(http.ListenAndServe(":8080", nil)) - os.Exit(0) - return nil } diff --git a/pkg/api/metrics/metrics.go b/pkg/api/metrics/metrics.go new file mode 100644 index 0000000..4faad4a --- /dev/null +++ b/pkg/api/metrics/metrics.go @@ -0,0 +1,27 @@ +package metrics + +import ( + "github.com/containrrr/watchtower/pkg/metrics" + "net/http" + + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +// Handler is an HTTP handle for serving metric data +type Handler struct { + Path string + Handle http.HandlerFunc + Metrics *metrics.Metrics +} + +// New is a factory function creating a new Metrics instance +func New() *Handler { + m := metrics.Default() + handler := promhttp.Handler() + + return &Handler{ + Path: "/v1/metrics", + Handle: handler.ServeHTTP, + Metrics: m, + } +} diff --git a/pkg/api/metrics/metrics_test.go b/pkg/api/metrics/metrics_test.go new file mode 100644 index 0000000..c1a4df0 --- /dev/null +++ b/pkg/api/metrics/metrics_test.go @@ -0,0 +1,77 @@ +package metrics_test + +import ( + "fmt" + "github.com/containrrr/watchtower/pkg/metrics" + "io/ioutil" + "net/http" + "testing" + + "github.com/containrrr/watchtower/pkg/api" + metricsAPI "github.com/containrrr/watchtower/pkg/api/metrics" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +const Token = "123123123" + +func TestContainer(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Metrics Suite") +} + +func runTestServer(m *metricsAPI.Handler) { + http.Handle(m.Path, m.Handle) + go func() { + http.ListenAndServe(":8080", nil) + }() +} + +func getWithToken(c http.Client, url string) (*http.Response, error) { + req, _ := http.NewRequest("GET", url, nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", Token)) + return c.Do(req) +} + +var _ = Describe("the metrics", func() { + httpAPI := api.New(Token) + m := metricsAPI.New() + httpAPI.RegisterHandler(m.Path, m.Handle) + httpAPI.Start(false) + + // We should likely split this into multiple tests, but as prometheus requires a restart of the binary + // to reset the metrics and gauges, we'll just do it all at once. + + It("should serve metrics", func() { + metric := &metrics.Metric{ + Scanned: 4, + Updated: 3, + Failed: 1, + } + metrics.RegisterScan(metric) + c := http.Client{} + res, err := getWithToken(c, "http://localhost:8080/v1/metrics") + + Expect(err).NotTo(HaveOccurred()) + contents, err := ioutil.ReadAll(res.Body) + fmt.Printf("%s\n", string(contents)) + Expect(string(contents)).To(ContainSubstring("watchtower_containers_updated 3")) + Expect(string(contents)).To(ContainSubstring("watchtower_containers_failed 1")) + Expect(string(contents)).To(ContainSubstring("watchtower_containers_scanned 4")) + Expect(string(contents)).To(ContainSubstring("watchtower_scans_total 1")) + Expect(string(contents)).To(ContainSubstring("watchtower_scans_skipped 0")) + + for i := 0; i < 3; i++ { + metrics.RegisterScan(nil) + } + + res, err = getWithToken(c, "http://localhost:8080/v1/metrics") + Expect(err).NotTo(HaveOccurred()) + contents, err = ioutil.ReadAll(res.Body) + fmt.Printf("%s\n", string(contents)) + + Expect(string(contents)).To(ContainSubstring("watchtower_scans_total 4")) + Expect(string(contents)).To(ContainSubstring("watchtower_scans_skipped 3")) + }) +}) \ No newline at end of file diff --git a/pkg/api/update/update.go b/pkg/api/update/update.go new file mode 100644 index 0000000..463b082 --- /dev/null +++ b/pkg/api/update/update.go @@ -0,0 +1,50 @@ +package update + +import ( + "io" + "net/http" + "os" + + log "github.com/sirupsen/logrus" +) + +var ( + lock chan bool +) + +// New is a factory function creating a new Handler instance +func New(updateFn func()) *Handler { + lock = make(chan bool, 1) + lock <- true + + return &Handler{ + fn: updateFn, + Path: "/v1/update", + } +} + +// Handler is an API handler used for triggering container update scans +type Handler struct { + fn func() + Path string +} + +// Handle is the actual http.Handle function doing all the heavy lifting +func (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) { + log.Info("Updates triggered by HTTP API request.") + + _, err := io.Copy(os.Stdout, r.Body) + if err != nil { + log.Println(err) + return + } + + select { + case chanValue := <-lock: + defer func() { lock <- chanValue }() + handle.fn() + default: + log.Debug("Skipped. Another update already running.") + } + +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go new file mode 100644 index 0000000..3a235af --- /dev/null +++ b/pkg/metrics/metrics.go @@ -0,0 +1,91 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var metrics *Metrics + +// Metric is the data points of a single scan +type Metric struct { + Scanned int + Updated int + Failed int +} + +// Metrics is the handler processing all individual scan metrics +type Metrics struct { + channel chan *Metric + scanned prometheus.Gauge + updated prometheus.Gauge + failed prometheus.Gauge + total prometheus.Counter + skipped prometheus.Counter +} + +// Register registers metrics for an executed scan +func (metrics *Metrics) Register(metric *Metric) { + metrics.channel <- metric +} + +// Default creates a new metrics handler if none exists, otherwise returns the existing one +func Default() *Metrics { + if metrics != nil { + return metrics + } + + metrics = &Metrics{ + scanned: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "watchtower_containers_scanned", + Help: "Number of containers scanned for changes by watchtower during the last scan", + }), + updated: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "watchtower_containers_updated", + Help: "Number of containers updated by watchtower during the last scan", + }), + failed: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "watchtower_containers_failed", + Help: "Number of containers where update failed during the last scan", + }), + total: promauto.NewCounter(prometheus.CounterOpts{ + Name: "watchtower_scans_total", + Help: "Number of scans since the watchtower started", + }), + skipped: promauto.NewCounter(prometheus.CounterOpts{ + Name: "watchtower_scans_skipped", + Help: "Number of skipped scans since watchtower started", + }), + channel: make(chan *Metric, 10), + } + + go metrics.HandleUpdate(metrics.channel) + + return metrics +} + +// RegisterScan fetches a metric handler and enqueues a metric +func RegisterScan(metric *Metric) { + metrics := Default() + metrics.Register(metric) +} + +// HandleUpdate dequeue the metric channel and processes it +func (metrics *Metrics) HandleUpdate(channel <-chan *Metric) { + for change := range channel { + if change == nil { + // Update was skipped and rescheduled + metrics.total.Inc() + metrics.skipped.Inc() + metrics.scanned.Set(0) + metrics.updated.Set(0) + metrics.failed.Set(0) + continue + } + // Update metrics with the new values + metrics.total.Inc() + metrics.scanned.Set(float64(change.Scanned)) + metrics.updated.Set(float64(change.Updated)) + metrics.failed.Set(float64(change.Failed)) + } +} diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go index 47bab40..bb475bf 100644 --- a/pkg/notifications/gotify.go +++ b/pkg/notifications/gotify.go @@ -87,5 +87,5 @@ func (n *gotifyTypeNotifier) GetURL() string { func (n *gotifyTypeNotifier) StartNotification() {} func (n *gotifyTypeNotifier) SendNotification() {} -func (n *gotifyTypeNotifier) Close() {} +func (n *gotifyTypeNotifier) Close() {} func (n *gotifyTypeNotifier) Levels() []log.Level { return nil } diff --git a/pkg/notifications/msteams.go b/pkg/notifications/msteams.go index 0c99072..63c6aaa 100644 --- a/pkg/notifications/msteams.go +++ b/pkg/notifications/msteams.go @@ -63,6 +63,6 @@ func (n *msTeamsTypeNotifier) GetURL() string { func (n *msTeamsTypeNotifier) StartNotification() {} func (n *msTeamsTypeNotifier) SendNotification() {} -func (n *msTeamsTypeNotifier) Close() {} +func (n *msTeamsTypeNotifier) Close() {} func (n *msTeamsTypeNotifier) Levels() []log.Level { return nil } func (n *msTeamsTypeNotifier) Fire(entry *log.Entry) error { return nil } diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml new file mode 100644 index 0000000..1a30df0 --- /dev/null +++ b/prometheus/prometheus.yml @@ -0,0 +1,9 @@ +scrape_configs: + - job_name: watchtower + scrape_interval: 5s + metrics_path: /v1/metrics + bearer_token: demotoken + static_configs: + - targets: + - 'watchtower:8080' + From 847261c2cacc6e2c59540543939be731f7b40b4e Mon Sep 17 00:00:00 2001 From: Jeroen Roos Date: Wed, 6 Jan 2021 22:30:28 +0100 Subject: [PATCH 24/98] Added a link to HTTP API documentation (#734) Co-authored-by: Jeroen Roos --- docs/arguments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/arguments.md b/docs/arguments.md index 4fb56c6..032affe 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -221,7 +221,7 @@ Environment Variable: WATCHTOWER_RUN_ONCE ``` ## HTTP API Mode -Runs Watchtower in HTTP API mode, only allowing image updates to be triggered by an HTTP request. +Runs Watchtower in HTTP API mode, only allowing image updates to be triggered by an HTTP request. For details see [HTTP API](https://containrrr.github.io/watchtower/http-api-mode). ``` Argument: --http-api From 352574d387e0ce35493a0c442965ecb857d25e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Wed, 6 Jan 2021 22:31:27 +0100 Subject: [PATCH 25/98] add details/summary to issue template (#732) --- .github/ISSUE_TEMPLATE/bug_report.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 76f46f4..1d4b1f6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -28,7 +28,14 @@ If applicable, add screenshots to help explain your problem. - Architecture - Docker version -**Logs from running watchtower with the `--debug` option** +
+ Logs from running watchtower with the --debug option + +``` + +``` + +
**Additional context** Add any other context about the problem here. From e230967196ea88a76804a203e13aaa36c39db636 Mon Sep 17 00:00:00 2001 From: ilike2burnthing <59480337+ilike2burnthing@users.noreply.github.com> Date: Wed, 6 Jan 2021 21:34:58 +0000 Subject: [PATCH 26/98] Add WATCHTOWER_INCLUDE_RESTARTING env for include-restarting flag (#720) --- docs/arguments.md | 10 ++++++++++ internal/flags/flags.go | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/docs/arguments.md b/docs/arguments.md index 032affe..fea17a0 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -118,6 +118,16 @@ Environment Variable: DOCKER_API_VERSION Default: "1.24" ``` +## Include restarting +Will also include created and exited containers. + +``` + Argument: --include-restarting +Environment Variable: WATCHTOWER_INCLUDE_RESTARTING + Type: Boolean + Default: false +``` + ## Include stopped Will also include created and exited containers. diff --git a/internal/flags/flags.go b/internal/flags/flags.go index d45f384..c2dc8ad 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -105,6 +105,12 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { viper.GetBool("WATCHTOWER_RUN_ONCE"), "Run once now and exit") + flags.BoolP( + "include-restarting", + "", + viper.GetBool("WATCHTOWER_INCLUDE_RESTARTING"), + "Will also include restarting containers") + flags.BoolP( "include-stopped", "S", From 99eaf42de0cfe9ee1dcb0158fb2cb7930fc53bfa Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Thu, 7 Jan 2021 07:51:01 +0100 Subject: [PATCH 27/98] fix doc typo --- docs/arguments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/arguments.md b/docs/arguments.md index fea17a0..9c3d58d 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -119,7 +119,7 @@ Environment Variable: DOCKER_API_VERSION ``` ## Include restarting -Will also include created and exited containers. +Will also include restarting containers. ``` Argument: --include-restarting From 06e705d53835ce8df65f319a1350216b52850a59 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sat, 9 Jan 2021 22:03:13 +0100 Subject: [PATCH 28/98] Create post-release.yml --- .github/workflows/post-release.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/post-release.yml diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml new file mode 100644 index 0000000..fcdeaa1 --- /dev/null +++ b/.github/workflows/post-release.yml @@ -0,0 +1,15 @@ +on: + release: + types: + - created + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + - '**/v[0-9]+.[0-9]+.[0-9]+' + +jobs: + build: + name: Renew documentation + runs-on: ubuntu-latest + steps: + - name: Pull new module version + uses: andrewslotin/go-proxy-pull-action@master From 40ab6fd5ba9b7ba03536f556c556dc38e69c61c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 10 Jan 2021 11:12:54 +0100 Subject: [PATCH 29/98] Fix notifications and old instance cleanup (#748) Co-authored-by: Simon Aronsson --- go.mod | 11 +++--- go.sum | 57 ++++++++++++++++++++++++++++++ internal/actions/check.go | 28 +++------------ pkg/api/metrics/metrics_test.go | 5 ++- pkg/notifications/email.go | 12 +++++-- pkg/notifications/notifier_test.go | 42 ++++++++++++++-------- pkg/notifications/slack.go | 21 ++++++++--- 7 files changed, 123 insertions(+), 53 deletions(-) diff --git a/go.mod b/go.mod index 02d1253..87327db 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/containrrr/watchtower go 1.12 +replace golang.org/x/sys => golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a + require ( github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/Microsoft/go-winio v0.4.12 // indirect @@ -15,7 +17,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cloudflare/cfssl v0.0.0-20190911221928-1a911ca1b1d6 // indirect github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect - github.com/containrrr/shoutrrr v0.0.0-20200601144753-78bb9685bc2f + github.com/containrrr/shoutrrr v0.3.0 github.com/docker/cli v0.0.0-20190327152802-57b27434ea29 github.com/docker/distribution v2.7.1+incompatible github.com/docker/docker v0.0.0-20190404075923-dbe4a30928d4 @@ -49,15 +51,14 @@ require ( github.com/prometheus/client_golang v0.9.3 github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 github.com/sirupsen/logrus v1.4.1 - github.com/spf13/cobra v0.0.3 - github.com/spf13/pflag v1.0.3 - github.com/spf13/viper v1.4.0 + github.com/spf13/cobra v0.0.7 + github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.6.3 github.com/stretchr/testify v1.3.0 github.com/theupdateframework/notary v0.6.1 // indirect github.com/zmap/zlint v1.0.2 // indirect golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 - golang.org/x/sys v0.0.0-20190830141801-acfa387b8d69 // indirect golang.org/x/text v0.3.4 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect diff --git a/go.sum b/go.sum index d1d77ae..c2e851a 100644 --- a/go.sum +++ b/go.sum @@ -45,11 +45,15 @@ github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882b github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containrrr/shoutrrr v0.0.0-20200601144753-78bb9685bc2f h1:Ln4yl+CYjrapeTEzMJQpgBwLjruKHcMosWFB/d1M4RQ= github.com/containrrr/shoutrrr v0.0.0-20200601144753-78bb9685bc2f/go.mod h1:eotQeC9bHbsf9eMUnXOU/y5bskegseWNB4PwmxRO7Wc= +github.com/containrrr/shoutrrr v0.3.0 h1:2o1BKQUThSDtcidiMUq99CJijSRDa/nIB8kRhLBYmbk= +github.com/containrrr/shoutrrr v0.3.0/go.mod h1:gqR3sngKPBVaLrmq9Pfw34x/MXxn0ATjY8/dW+rXzrU= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-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.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 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= @@ -82,8 +86,12 @@ github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DP github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -124,9 +132,11 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= @@ -159,7 +169,9 @@ github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22/go.mod h1:u0Jo4 github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07 h1:+kBG/8rjCa6vxJZbUjAiE4MQmBEBYc8nLEb51frnvBY= github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07/go.mod h1:j1kV/8f3jowErEq4XyeypkCdvg5EeHkf0YCKCcq5Ybo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +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/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= @@ -179,18 +191,30 @@ github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/pkcs11 v0.0.0-20190401114359-553cfdd26aaa h1:gOXc1BXmFuxWYmTfoK51YJR7srco3CwbsVHgr+8Y4r0= github.com/miekg/pkcs11 v0.0.0-20190401114359-553cfdd26aaa/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= +github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= @@ -217,6 +241,8 @@ github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59P github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= @@ -245,30 +271,48 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn 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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +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 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU= +github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs= +github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0= github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -316,6 +360,7 @@ golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ym golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 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= @@ -331,12 +376,19 @@ golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190830141801-acfa387b8d69 h1:Wdn4Yb8d5VrsO3jWgaeSZss09x1VLVBMePDh4VW/xSQ= golang.org/x/sys v0.0.0-20190830141801-acfa387b8d69/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-20200409092240-59c9f1ba88fa h1:mQTN3ECqfsViCNBgq+A40vdwhkGykrrQlYe3mPj6BoU= +golang.org/x/sys v0.0.0-20200409092240-59c9f1ba88fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= 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.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -349,6 +401,7 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm 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-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -387,6 +440,9 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gorethink/gorethink.v3 v3.0.5 h1:e2Uc/Xe+hpcVQFsj6MuHlYog3r0JYpnTzwDj/y2O4MU= gopkg.in/gorethink/gorethink.v3 v3.0.5/go.mod h1:+3yIIHJUGMBK+wyPH+iN5TP+88ikFDfZdqTlK3Y9q8I= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= +gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/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= @@ -395,6 +451,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 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.8/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= gosrc.io/xmpp v0.1.1 h1:iMtE9W3fx254+4E6rI34AOPJDqWvpfQR6EYaVMzhJ4s= diff --git a/internal/actions/check.go b/internal/actions/check.go index aeff0cd..87133fc 100644 --- a/internal/actions/check.go +++ b/internal/actions/check.go @@ -1,15 +1,12 @@ package actions import ( - "errors" "fmt" "sort" - "strings" "time" "github.com/containrrr/watchtower/pkg/filters" "github.com/containrrr/watchtower/pkg/sorter" - "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus" @@ -40,7 +37,6 @@ func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool, } func cleanupExcessWatchtowers(containers []container.Container, client container.Client, cleanup bool) error { - var cleanupErrors int var stopErrors int sort.Sort(sorter.ByCreated(containers)) @@ -49,37 +45,23 @@ func cleanupExcessWatchtowers(containers []container.Container, client container for _, c := range allContainersExceptLast { if err := client.StopContainer(c, 10*time.Minute); err != nil { // logging the original here as we're just returning a count - logrus.Error(err) + logrus.WithError(err).Error("Could not stop a previous watchtower instance.") stopErrors++ continue } if cleanup { if err := client.RemoveImageByID(c.ImageID()); err != nil { - // logging the original here as we're just returning a count - logrus.Error(err) - cleanupErrors++ + logrus.WithError(err).Warning("Could not cleanup watchtower images, possibly because of other watchtowers instances in other scopes.") } } } - return createErrorIfAnyHaveOccurred(stopErrors, cleanupErrors) -} - -func createErrorIfAnyHaveOccurred(c int, i int) error { - if c == 0 && i == 0 { - return nil + if stopErrors > 0 { + return fmt.Errorf("%d errors while stopping watchtower containers", stopErrors) } - var output strings.Builder - - if c > 0 { - output.WriteString(fmt.Sprintf("%d errors while stopping containers", c)) - } - if i > 0 { - output.WriteString(fmt.Sprintf("%d errors while cleaning up images", c)) - } - return errors.New(output.String()) + return nil } func awaitDockerClient() { diff --git a/pkg/api/metrics/metrics_test.go b/pkg/api/metrics/metrics_test.go index c1a4df0..156601f 100644 --- a/pkg/api/metrics/metrics_test.go +++ b/pkg/api/metrics/metrics_test.go @@ -55,7 +55,7 @@ var _ = Describe("the metrics", func() { Expect(err).NotTo(HaveOccurred()) contents, err := ioutil.ReadAll(res.Body) - fmt.Printf("%s\n", string(contents)) + Expect(string(contents)).To(ContainSubstring("watchtower_containers_updated 3")) Expect(string(contents)).To(ContainSubstring("watchtower_containers_failed 1")) Expect(string(contents)).To(ContainSubstring("watchtower_containers_scanned 4")) @@ -69,9 +69,8 @@ var _ = Describe("the metrics", func() { res, err = getWithToken(c, "http://localhost:8080/v1/metrics") Expect(err).NotTo(HaveOccurred()) contents, err = ioutil.ReadAll(res.Body) - fmt.Printf("%s\n", string(contents)) Expect(string(contents)).To(ContainSubstring("watchtower_scans_total 4")) Expect(string(contents)).To(ContainSubstring("watchtower_scans_skipped 3")) }) -}) \ No newline at end of file +}) diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index 2356978..184bf84 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -1,9 +1,11 @@ package notifications import ( + "fmt" "os" "time" + "github.com/containrrr/shoutrrr/pkg/format" "github.com/spf13/cobra" shoutrrrSmtp "github.com/containrrr/shoutrrr/pkg/services/smtp" @@ -75,10 +77,16 @@ func (e *emailTypeNotifier) GetURL() string { UseHTML: false, } + pkr := format.NewPropKeyResolver(conf) + var err error if len(e.User) > 0 { - conf.Set("auth", "Plain") + err = pkr.Set("auth", "Plain") } else { - conf.Set("auth", "None") + err = pkr.Set("auth", "None") + } + + if err != nil { + fmt.Printf("Could not set auth type for email notifier: %v", err) } return conf.GetURL().String() diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go index 6440bbc..57132ac 100644 --- a/pkg/notifications/notifier_test.go +++ b/pkg/notifications/notifier_test.go @@ -22,19 +22,32 @@ func TestActions(t *testing.T) { } var _ = Describe("notifications", func() { - // TODO: Either, we delete this test or we need to pass it valid URLs in the cobra command. - // --- - // When("getting notifiers from a types array", func() { - // It("should return the same amount of notifiers a string entries", func() { - - // notifier := ¬ifications.Notifier{} - // notifiers := notifier.GetNotificationTypes(&cobra.Command{}, []log.Level{}, []string{"slack", "email"}) - // Expect(len(notifiers)).To(Equal(2)) - // }) - // }) Describe("the slack notifier", func() { + builderFn := notifications.NewSlackNotifier + + When("passing a discord url to the slack notifier", func() { + channel := "123456789" + token := "abvsihdbau" + expected := fmt.Sprintf("discord://%s@%s", token, channel) + buildArgs := func(url string) []string { + return []string{ + "--notifications", + "slack", + "--notification-slack-hook-url", + url, + } + } + + It("should return a discord url when using a hook url with the domain discord.com", func() { + hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discord.com", channel, token) + testURL(builderFn, buildArgs(hookURL), expected) + }) + It("should return a discord url when using a hook url with the domain discordapp.com", func() { + hookURL := fmt.Sprintf("https://%s/api/webhooks/%s/%s/slack", "discordapp.com", channel, token) + testURL(builderFn, buildArgs(hookURL), expected) + }) + }) When("converting a slack service config into a shoutrrr url", func() { - builderFn := notifications.NewSlackNotifier It("should return the expected URL", func() { @@ -43,9 +56,8 @@ var _ = Describe("notifications", func() { tokenB := "bbb" tokenC := "ccc" - password := fmt.Sprintf("%s-%s-%s", tokenA, tokenB, tokenC) hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC) - expectedOutput := fmt.Sprintf("slack://%s:%s@%s/%s/%s", username, password, tokenA, tokenB, tokenC) + expectedOutput := fmt.Sprintf("slack://%s@%s/%s/%s", username, tokenA, tokenB, tokenC) args := []string{ "--notification-slack-hook-url", @@ -144,8 +156,8 @@ func buildExpectedURL(username string, password string, host string, port int, f subject := fmt.Sprintf("Watchtower updates on %s", hostname) - var template = "smtp://%s:%s@%s:%d/?fromAddress=%s&fromName=Watchtower&toAddresses=%s&auth=%s&subject=%s&startTls=Yes&useHTML=No" - return fmt.Sprintf(template, username, password, host, port, from, to, auth, subject) + var template = "smtp://%s:%s@%s:%d/?auth=%s&encryption=None&fromaddress=%s&fromname=Watchtower&starttls=Yes&subject=%s&toaddresses=%s&usehtml=No" + return fmt.Sprintf(template, username, password, host, port, auth, from, subject, to) } type builderFn = func(c *cobra.Command, acceptedLogLevels []log.Level) types.ConvertableNotifier diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go index f8cbd45..ede7141 100644 --- a/pkg/notifications/slack.go +++ b/pkg/notifications/slack.go @@ -3,6 +3,7 @@ package notifications import ( "strings" + shoutrrrDisco "github.com/containrrr/shoutrrr/pkg/services/discord" shoutrrrSlack "github.com/containrrr/shoutrrr/pkg/services/slack" t "github.com/containrrr/watchtower/pkg/types" "github.com/johntdyer/slackrus" @@ -31,6 +32,7 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert channel, _ := flags.GetString("notification-slack-channel") emoji, _ := flags.GetString("notification-slack-icon-emoji") iconURL, _ := flags.GetString("notification-slack-icon-url") + n := &slackTypeNotifier{ SlackrusHook: slackrus.SlackrusHook{ HookURL: hookURL, @@ -45,16 +47,25 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert } func (s *slackTypeNotifier) GetURL() string { + trimmedURL := strings.TrimRight(s.HookURL, "/") + trimmedURL = strings.TrimLeft(trimmedURL, "https://") + parts := strings.Split(trimmedURL, "/") + + if parts[0] == "discord.com" || parts[0] == "discordapp.com" { + log.Debug("Detected a discord slack wrapper URL, using shoutrrr discord service") + conf := &shoutrrrDisco.Config{ + Channel: parts[len(parts)-3], + Token: parts[len(parts)-2], + } + return conf.GetURL().String() + } + rawTokens := strings.Replace(s.HookURL, "https://hooks.slack.com/services/", "", 1) tokens := strings.Split(rawTokens, "/") conf := &shoutrrrSlack.Config{ BotName: s.Username, - Token: shoutrrrSlack.Token{ - A: tokens[0], - B: tokens[1], - C: tokens[2], - }, + Token: tokens, } return conf.GetURL().String() From e18b57efc89efa094c1e411f66781f84604db05d Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 10 Jan 2021 11:25:46 +0100 Subject: [PATCH 30/98] Cleanup readme --- README.md | 55 ++++++++++++++----------------------------------------- 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index cba30b1..6c93864 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,23 @@

-

-

- Watchtower -

- -

+ + # Watchtower + A process for automating Docker container base image updates.

- - Circle CI - - - GoDoc - - - Microbadger - - - Go Report Card - - - latest version - - - Apache-2.0 License - - - Codacy Badge - - - Codacy Badge - - - All Contributors - - - Pulls from DockerHub - + + [![Circle CI](https://circleci.com/gh/containrrr/watchtower.svg?style=shield)](https://circleci.com/gh/containrrr/watchtower) + [![GoDoc](https://godoc.org/github.com/containrrr/watchtower?status.svg)](https://godoc.org/github.com/containrrr/watchtower) + [![Microbadger](https://images.microbadger.com/badges/image/containrrr/watchtower.svg)](https://microbadger.com/images/containrrr/watchtower) + [![Go Report Card](https://goreportcard.com/badge/github.com/containrrr/watchtower)](https://goreportcard.com/report/github.com/containrrr/watchtower) + [![latest version](https://img.shields.io/github/tag/containrrr/watchtower.svg)](https://github.com/containrrr/watchtower/releases) + [![Apache-2.0 License](https://img.shields.io/github/license/containrrr/watchtower.svg)](https://www.apache.org/licenses/LICENSE-2.0) + [![Codacy Badge](https://app.codacy.com/project/badge/Grade/1c48cfb7646d4009aa8c6f71287670b8)](https://www.codacy.com/gh/containrrr/watchtower/dashboard?utm_source=github.com&utm_medium=referral&utm_content=containrrr/watchtower&utm_campaign=Badge_Grade) + [![All Contributors](https://img.shields.io/badge/all_contributors-30-orange.svg?style=flat-square)](#contributors) + [![Pulls from DockerHub](https://img.shields.io/docker/pulls/containrrr/watchtower.svg?style=flat-square)](https://hub.docker.com/r/containrrr/watchtower) +

-> ### ⚠️ Help needed -> -> We're finding it a bit hard to keep up with all issues and pull requests. Interested in helping out with triage, troubleshooting and issue handling? Let us know in the ["Discussions"](https://github.com/containrrr/watchtower/discussions) tab! - - ## Quick Start With watchtower you can update the running version of your containerized app simply by pushing a new image to the Docker Hub or your own image registry. Watchtower will pull down your new image, gracefully shut down your existing container and restart it with the same options that were used when it was deployed initially. Run the watchtower container with the following command: From 47e53adf6d5de2b7ab55adbf1f80ce8c12131561 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 10 Jan 2021 11:25:59 +0100 Subject: [PATCH 31/98] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c93864..152b91a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

+

# Watchtower @@ -16,7 +16,7 @@ [![All Contributors](https://img.shields.io/badge/all_contributors-30-orange.svg?style=flat-square)](#contributors) [![Pulls from DockerHub](https://img.shields.io/docker/pulls/containrrr/watchtower.svg?style=flat-square)](https://hub.docker.com/r/containrrr/watchtower) -

+
## Quick Start From 5b349e5dc4b7bca18bf6b548f7bf317821e444dd Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 10 Jan 2021 11:27:26 +0100 Subject: [PATCH 32/98] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 152b91a..52c42b9 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@

[![Circle CI](https://circleci.com/gh/containrrr/watchtower.svg?style=shield)](https://circleci.com/gh/containrrr/watchtower) + [![codecov](https://codecov.io/gh/containrrr/watchtower/branch/master/graph/badge.svg?token=8pxWgB380Y)](https://codecov.io/gh/containrrr/watchtower) [![GoDoc](https://godoc.org/github.com/containrrr/watchtower?status.svg)](https://godoc.org/github.com/containrrr/watchtower) [![Microbadger](https://images.microbadger.com/badges/image/containrrr/watchtower.svg)](https://microbadger.com/images/containrrr/watchtower) [![Go Report Card](https://goreportcard.com/badge/github.com/containrrr/watchtower)](https://goreportcard.com/report/github.com/containrrr/watchtower) From 4e0ce6e824a74913c9f16dc2367ff27dcb89065c Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 10 Jan 2021 12:24:13 +0100 Subject: [PATCH 33/98] chore(ci): move to github actions --- .github/workflows/pull-request.yml | 61 ++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/pull-request.yml diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..cb516ba --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,61 @@ +name: Main Workflow + +on: + pull_request: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.15.x + - name: Lint + run: | + go get -u golang.org/lint/golint + golint -set_exit_status ./... + test: + strategy: + matrix: + go-version: + - 1.15.x + platform: + - ubuntu-latest + - windows-latest + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.15.x + - name: Run tests + run: | + go test ./... -coverprofile coverage.out + + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.15.x + - name: Build + uses: goreleaser/goreleaser-action@v2 + with: + version: v0.104.1 + args: --snapshot --skip-publish --debug \ No newline at end of file From aa3ed8833f484b3c016f8c76260442b1487a91c4 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 10 Jan 2021 12:27:26 +0100 Subject: [PATCH 34/98] chore(ci): rename workflow --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index cb516ba..e54d5c4 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,4 +1,4 @@ -name: Main Workflow +name: Pull Request on: pull_request: From 2ba51c0de017d31af9b6e890b808367cc2769b1d Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 10 Jan 2021 12:32:10 +0100 Subject: [PATCH 35/98] chore(ci): add manual dispatch --- .github/workflows/pull-request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e54d5c4..1af047f 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,6 +1,7 @@ name: Pull Request on: + workflow_dispatch: {} pull_request: branches: - main From 668e7a15110ab1183291283513f4deadf9fe22ca Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 10 Jan 2021 12:33:45 +0100 Subject: [PATCH 36/98] fix(ci): set correct get url for linter --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 1af047f..852bbcd 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -20,7 +20,7 @@ jobs: go-version: 1.15.x - name: Lint run: | - go get -u golang.org/lint/golint + go get -u golang.org/x/lint/golint golint -set_exit_status ./... test: strategy: From ea53cdbe7c5b9b3b1f1a6065aa537f23cd37457d Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 10 Jan 2021 12:42:54 +0100 Subject: [PATCH 37/98] add names to steps --- .github/workflows/pull-request.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 852bbcd..d09b4f2 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -8,6 +8,7 @@ on: jobs: lint: + name: Lint runs-on: ubuntu-latest steps: - name: Checkout @@ -18,11 +19,14 @@ jobs: uses: actions/setup-go@v2 with: go-version: 1.15.x - - name: Lint + - name: Install linter run: | go get -u golang.org/x/lint/golint + - name: Lint files + run: | golint -set_exit_status ./... test: + name: Test strategy: matrix: go-version: @@ -45,6 +49,7 @@ jobs: go test ./... -coverprofile coverage.out build: + name: Build runs-on: ubuntu-latest steps: - name: Checkout From 140cc4ea12ebc435b5e23261998acdbd4c8b54fa Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 10 Jan 2021 12:44:14 +0100 Subject: [PATCH 38/98] add macos to the testing matrix --- .github/workflows/pull-request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index d09b4f2..5bdd01e 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -33,6 +33,7 @@ jobs: - 1.15.x platform: - ubuntu-latest + - macos-latest - windows-latest runs-on: ${{ matrix.platform }} steps: From 5532c4e90508174293079f2db5c1d81f795d4bd8 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 10 Jan 2021 16:36:03 +0100 Subject: [PATCH 39/98] chore(ci): set up release workflow --- .github/workflows/post-release.yml | 15 -- ...-dev-dockerimage.yaml => release-dev.yaml} | 2 +- .github/workflows/release.yml | 175 ++++++++++++++++++ 3 files changed, 176 insertions(+), 16 deletions(-) delete mode 100644 .github/workflows/post-release.yml rename .github/workflows/{publish-dev-dockerimage.yaml => release-dev.yaml} (92%) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml deleted file mode 100644 index fcdeaa1..0000000 --- a/.github/workflows/post-release.yml +++ /dev/null @@ -1,15 +0,0 @@ -on: - release: - types: - - created - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' - - '**/v[0-9]+.[0-9]+.[0-9]+' - -jobs: - build: - name: Renew documentation - runs-on: ubuntu-latest - steps: - - name: Pull new module version - uses: andrewslotin/go-proxy-pull-action@master diff --git a/.github/workflows/publish-dev-dockerimage.yaml b/.github/workflows/release-dev.yaml similarity index 92% rename from .github/workflows/publish-dev-dockerimage.yaml rename to .github/workflows/release-dev.yaml index c02186a..dde175c 100644 --- a/.github/workflows/publish-dev-dockerimage.yaml +++ b/.github/workflows/release-dev.yaml @@ -1,4 +1,4 @@ -name: Docker image (latest-dev) +name: Release (Develop) on: push: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5b73616 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,175 @@ +name: Release (Production) + +on: + release: + types: + - created + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + - '**/v[0-9]+.[0-9]+.[0-9]+' + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.15.x + - name: Install linter + run: | + go get -u golang.org/x/lint/golint + - name: Lint files + run: | + golint -set_exit_status ./... + + test: + name: Test + strategy: + matrix: + go-version: + - 1.15.x + platform: + - ubuntu-latest + - macos-latest + - windows-latest + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.15.x + - name: Run tests + run: | + go test ./... -coverprofile coverage.out + + build: + name: Build + runs-on: ubuntu-latest + needs: + - test + - lint + env: + CGO_ENABLED: ${CGO_ENABLED:-0} + TAG: ${GITHUB_REF#refs/tags/} + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.15.x + - name: Run goreleaser + uses: goreleaser/goreleaser-action@v2 + with: + version: v0.104.1 + args: --debug + - name: Enable experimental docker features + run: | + mkdir -p ~/.docker/ && \ + echo '{"experimental": "enabled"}' > ~/.docker/config.json + - name: Create manifest for version + run: | + export DH_TAG=$(echo $TAG | sed 's/^v*//') + docker manifest create \ + containrrr/watchtower:$DH_TAG \ + containrrr/watchtower:amd64-$DH_TAG \ + containrrr/watchtower:i386-$DH_TAG \ + containrrr/watchtower:armhf-$DH_TAG \ + containrrr/watchtower:arm64v8-$DH_TAG + - name: Annotate manifest for version + run: | + docker manifest annotate \ + containrrr/watchtower:$(echo $TAG | sed 's/^v*//') \ + containrrr/watchtower:i386-$(echo $TAG | sed 's/^v*//') \ + --os linux \ + --arch 386 + + docker manifest annotate \ + containrrr/watchtower:$(echo $TAG | sed 's/^v*//') \ + containrrr/watchtower:armhf-$(echo $TAG | sed 's/^v*//') \ + --os linux \ + --arch arm + + docker manifest annotate \ + containrrr/watchtower:$(echo $TAG | sed 's/^v*//') \ + containrrr/watchtower:arm64v8-$(echo $TAG | sed 's/^v*//') \ + --os linux \ + --arch arm64 \ + --variant v8 + - name: Create manifest for latest + run: | + docker manifest create \ + containrrr/watchtower:latest \ + containrrr/watchtower:amd64-latest \ + containrrr/watchtower:i386-latest \ + containrrr/watchtower:armhf-latest \ + containrrr/watchtower:arm64v8-latest + - name: Annotate manifest for latest + run: | + docker manifest annotate \ + containrrr/watchtower:latest \ + containrrr/watchtower:i386-latest \ + --os linux \ + --arch 386 + + docker manifest annotate \ + containrrr/watchtower:latest \ + containrrr/watchtower:armhf-latest \ + --os linux \ + --arch arm + + docker manifest annotate \ + containrrr/watchtower:latest \ + containrrr/watchtower:arm64v8-latest \ + --os linux \ + --arch arm64 \ + --variant v8 + - name: Push manifests to Dockerhub + run: | + echo "$DOCKER_PASS" | docker login -u $DOCKER_USER --password-stdin && \ + docker manifest push containrrr/watchtower:$(echo $TAG | sed 's/^v*//') && \ + docker manifest push containrrr/watchtower:latest + + publish-docs: + name: Publish Docs + needs: build + runs-on: ubuntu-latest + steps: + - name: Install mkdocs + run: | + pip install \ + mkdocs \ + mkdocs-material \ + md-toc + - name: Generate docs + run: mkdocs build + - name: Publish docs + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site + + renew-docs: + name: Refresh pkg.go.dev + needs: build + runs-on: ubuntu-latest + steps: + - name: Pull new module version + uses: andrewslotin/go-proxy-pull-action@master + + + + + \ No newline at end of file From 3369d2427c3a7862444f8f34f09aeed3526f1424 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 10 Jan 2021 16:39:45 +0100 Subject: [PATCH 40/98] chore(ci): add coverage upload --- .github/workflows/pull-request.yml | 6 +++++- .github/workflows/release.yml | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 5bdd01e..1f0ec4f 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -47,7 +47,11 @@ jobs: go-version: 1.15.x - name: Run tests run: | - go test ./... -coverprofile coverage.out + CGO_ENABLED=0 go test -v -coverprofile=coverage.out -covermode=atomic ./... + - name: Publish coverage + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} build: name: Build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b73616..4a83e1f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -147,6 +147,10 @@ jobs: needs: build runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 - name: Install mkdocs run: | pip install \ From fb9469e58e16b4f48418c21b679c72f4a9cafd19 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Tue, 12 Jan 2021 08:29:23 +0100 Subject: [PATCH 41/98] remove cgo_enable flag for the test step this was a last-minute change that actually isn't needed. we'll know whether this is an issue in the build step anyway. --- .github/workflows/pull-request.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 1f0ec4f..2ff161a 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -47,7 +47,7 @@ jobs: go-version: 1.15.x - name: Run tests run: | - CGO_ENABLED=0 go test -v -coverprofile=coverage.out -covermode=atomic ./... + go test -v -coverprofile=coverage.out -covermode=atomic ./... - name: Publish coverage uses: codecov/codecov-action@v1 with: @@ -69,4 +69,4 @@ jobs: uses: goreleaser/goreleaser-action@v2 with: version: v0.104.1 - args: --snapshot --skip-publish --debug \ No newline at end of file + args: --snapshot --skip-publish --debug From 4689853493b827105213dabecffafd30e971dded Mon Sep 17 00:00:00 2001 From: Zois Pagoulatos Date: Tue, 12 Jan 2021 08:34:11 +0100 Subject: [PATCH 42/98] fix: Set log level to debug for message about API token (#757) --- pkg/api/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 987e4bd..9afded5 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -30,7 +30,7 @@ func (api *API) RequireToken(fn http.HandlerFunc) http.HandlerFunc { log.Debugf("Expected token to be \"%s\"", api.Token) return } - log.Println("Valid token found.") + log.Debug("Valid token found.") fn(w, r) } } From b6af45b9ae7ae259a14f0b3487cbbe35eb833c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Tue, 12 Jan 2021 20:04:58 +0100 Subject: [PATCH 43/98] chore(ci): disable fail-fast for pr tests (#766) --- .github/workflows/pull-request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2ff161a..768a7b5 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -28,6 +28,7 @@ jobs: test: name: Test strategy: + fail-fast: false matrix: go-version: - 1.15.x From bfd57c7180a61bbac47a75a0302dfaf5eaf97ce9 Mon Sep 17 00:00:00 2001 From: Srihari Thalla Date: Tue, 12 Jan 2021 20:06:11 +0100 Subject: [PATCH 44/98] chore: Update Badges on Docs (#764) --- docs/index.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index 1b997bf..10d9b16 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,6 +11,9 @@ Circle CI + + Codecov + GoDoc @@ -26,13 +29,10 @@ Apache-2.0 License - - Codacy Badge + + Codacy Badge - - Codacy Badge - - + All Contributors From 8b95e8c1c37a658c7a9f247859d9788bd195e6f1 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 12 Jan 2021 20:08:15 +0100 Subject: [PATCH 45/98] docs: add SrihariThalla as a contributor (#769) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++ README.md | 156 ++++++++++++++++++++++---------------------- 2 files changed, 88 insertions(+), 77 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index d084e9f..0de94a3 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -711,6 +711,15 @@ "contributions": [ "code" ] + }, + { + "login": "SrihariThalla", + "name": "Srihari Thalla", + "avatar_url": "https://avatars1.githubusercontent.com/u/7479937?v=4", + "profile": "https://github.com/SrihariThalla", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 52c42b9..9fc4194 100644 --- a/README.md +++ b/README.md @@ -42,107 +42,109 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - - + + + + + + + - - - - - - + + + + + + +

James

⚠️ 🤔

Florian

👀 📖

Brian DeHamer

💻 🚧

Ross Cadogan

💻

stffabi

💻 🚧

Austin

📖

David Gardner

👀 📖

James

⚠️ 🤔

Florian

👀 📖

Brian DeHamer

💻 🚧

Ross Cadogan

💻

stffabi

💻 🚧

Austin

📖

David Gardner

👀 📖

Tanguy ⧓ Herrmann

💻

Rodrigo Damazio Bovendorp

💻 📖

Ryan Kuba

🚇

cnrmck

📖

Harry Walter

💻

Robotex

📖

Gerald Pape

📖

Tanguy ⧓ Herrmann

💻

Rodrigo Damazio Bovendorp

💻 📖

Ryan Kuba

🚇

cnrmck

📖

Harry Walter

💻

Robotex

📖

Gerald Pape

📖

fomk

💻

Sven Gottwald

🚇

techknowlogick

💻

waja

📖

Scott Albertson

📖

Jason Huddleston

📖

Napster

💻

fomk

💻

Sven Gottwald

🚇

techknowlogick

💻

waja

📖

Scott Albertson

📖

Jason Huddleston

📖

Napster

💻

Maxim

💻 📖

Max Schmitt

📖

cron410

📖

Paulo Henrique

📖

Kaleb Elwert

📖

Bill Butler

📖

Mario Tacke

💻

Maxim

💻 📖

Max Schmitt

📖

cron410

📖

Paulo Henrique

📖

Kaleb Elwert

📖

Bill Butler

📖

Mario Tacke

💻

Mark Woodbridge

💻

Simon Aronsson

💻 🚧 👀 📖

Ansem93

📖

Luka Peschke

💻 📖

Zois Pagoulatos

💻 👀

Alexandre Menif

💻

Andrey

📖

Mark Woodbridge

💻

Simon Aronsson

💻 🚧 👀 📖

Ansem93

📖

Luka Peschke

💻 📖

Zois Pagoulatos

💻 👀

Alexandre Menif

💻

Andrey

📖

Armando Lüscher

📖

Ryan Budke

📖

Kaloyan Raev

💻 ⚠️

sixth

📖

Gina Häußge

💻

Max H.

💻

Jungkook Park

📖

Armando Lüscher

📖

Ryan Budke

📖

Kaloyan Raev

💻 ⚠️

sixth

📖

Gina Häußge

💻

Max H.

💻

Jungkook Park

📖

Jan Kristof Nidzwetzki

📖

lukas

💻

Ameya Shenoy

💻

Raymon de Looff

💻

John Clayton

💻

Germs2004

📖

Lukas Willburger

💻

Jan Kristof Nidzwetzki

📖

lukas

💻

Ameya Shenoy

💻

Raymon de Looff

💻

John Clayton

💻

Germs2004

📖

Lukas Willburger

💻

Oliver Cervera

📖

Victor Moura

⚠️ 💻 📖

Maximilian Brandau

💻 ⚠️

Andrew

📖

sixcorners

📖

nils måsén

📖 💻

Arne Jørgensen

⚠️ 👀

Oliver Cervera

📖

Victor Moura

⚠️ 💻 📖

Maximilian Brandau

💻 ⚠️

Andrew

📖

sixcorners

📖

nils måsén

📖 💻

Arne Jørgensen

⚠️ 👀

PatSki123

📖

Valentine Zavadsky

💻 📖 ⚠️

Alexander Voronin

💻 🐛

Oliver Mueller

📖

Sebastiaan Tammer

💻

miosame

📖

Andrew Metzger

🐛 💡

PatSki123

📖

Valentine Zavadsky

💻 📖 ⚠️

Alexander Voronin

💻 🐛

Oliver Mueller

📖

Sebastiaan Tammer

💻

miosame

📖

Andrew Metzger

🐛 💡

Pierre Grimaud

📖

Matt Doran

📖

MihailITPlace

💻

bugficks

💻 📖

Michael

💻

D. Domig

📖

Ben Osheroff

💻

Pierre Grimaud

📖

Matt Doran

📖

MihailITPlace

💻

bugficks

💻 📖

Michael

💻

D. Domig

📖

Ben Osheroff

💻

David H.

💻

Chander Ganesan

📖

yrien30

💻

ksurl

📖

rg9400

💻

Turtle Kalus

💻

David H.

💻

Chander Ganesan

📖

yrien30

💻

ksurl

📖

rg9400

💻

Turtle Kalus

💻

Srihari Thalla

📖
- + + This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! From eccf87a47d354bc1c39a3ba20605965b48737f77 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Tue, 12 Jan 2021 20:13:19 +0100 Subject: [PATCH 46/98] make test command windows compatible --- .github/workflows/pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 768a7b5..180b105 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -48,7 +48,7 @@ jobs: go-version: 1.15.x - name: Run tests run: | - go test -v -coverprofile=coverage.out -covermode=atomic ./... + go test -v -coverprofile coverage.out -covermode atomic ./... - name: Publish coverage uses: codecov/codecov-action@v1 with: From d04d71508f40d3ed027a9d06c67deea26afc1cc0 Mon Sep 17 00:00:00 2001 From: Zois Pagoulatos Date: Tue, 12 Jan 2021 20:19:14 +0100 Subject: [PATCH 47/98] fix: Disallow log level 'trace' (#765) Co-authored-by: Simon Aronsson --- pkg/notifications/notifier.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index dea0fc8..651ab6a 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -25,6 +25,10 @@ func NewNotifier(c *cobra.Command) *Notifier { } acceptedLogLevels := slackrus.LevelThreshold(logLevel) + // slackrus does not allow log level TRACE, even though it's an accepted log level for logrus + if len(acceptedLogLevels) == 0 { + log.Fatalf("Unsupported notification log level provided: %s", level) + } // Parse types and create notifiers. types, err := f.GetStringSlice("notifications") From 0961c74e6b299da5cbebfb857ee7c767d4353886 Mon Sep 17 00:00:00 2001 From: Zois Pagoulatos Date: Tue, 12 Jan 2021 20:43:41 +0100 Subject: [PATCH 48/98] ci: Add codeQL analysis checks (#770) --- .github/workflows/codeql-analysis.yml | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..2437bb2 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [main] + pull_request: + # The branches below must be a subset of the branches above + branches: [main] + schedule: + - cron: '0 1 * * 4' + workflow_dispatch: + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['go'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From ae6bd207c12ad86739da2589a3704427f476eff7 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 12 Jan 2021 20:43:57 +0100 Subject: [PATCH 49/98] docs: add zoispag as a contributor (#771) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 0de94a3..33e967a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -311,7 +311,8 @@ "profile": "https://github.com/zoispag", "contributions": [ "code", - "review" + "review", + "maintenance" ] }, { diff --git a/README.md b/README.md index 9fc4194..d330c3d 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d

Simon Aronsson

💻 🚧 👀 📖
Ansem93

📖
Luka Peschke

💻 📖 -
Zois Pagoulatos

💻 👀 +
Zois Pagoulatos

💻 👀 🚧
Alexandre Menif

💻
Andrey

📖 From 9cee66c93ddc2747dad5789403c4243720891017 Mon Sep 17 00:00:00 2001 From: Zois Pagoulatos Date: Thu, 14 Jan 2021 17:27:57 +0100 Subject: [PATCH 50/98] ci: Update code of conduct URL in github action (#778) Closes #777 --- .github/workflows/greetings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index 899bb52..20302f0 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -11,7 +11,7 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} issue-message: > Hi there! 👋🏼 - As you're new to this repo, we'd like to suggest that you read our [code of conduct](https://github.com/containrrr/watchtower/blob/master/CODE_OF_CONDUCT.md) + As you're new to this repo, we'd like to suggest that you read our [code of conduct](https://github.com/containrrr/.github/blob/master/CODE_OF_CONDUCT.md) as well as our [contribution guidelines](https://github.com/containrrr/watchtower/blob/master/CONTRIBUTING.md). Thanks a bunch for opening your first issue! 🙏 pr-message: > From d60c0d6b573bd8fb66906ceda5b64e1433fee24f Mon Sep 17 00:00:00 2001 From: Srihari Thalla Date: Tue, 19 Jan 2021 07:08:18 +0100 Subject: [PATCH 51/98] chore: fix badge url for contributors and docker pulls (#774) --- README.md | 4 ++-- docs/index.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d330c3d..c002e77 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ [![latest version](https://img.shields.io/github/tag/containrrr/watchtower.svg)](https://github.com/containrrr/watchtower/releases) [![Apache-2.0 License](https://img.shields.io/github/license/containrrr/watchtower.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/1c48cfb7646d4009aa8c6f71287670b8)](https://www.codacy.com/gh/containrrr/watchtower/dashboard?utm_source=github.com&utm_medium=referral&utm_content=containrrr/watchtower&utm_campaign=Badge_Grade) - [![All Contributors](https://img.shields.io/badge/all_contributors-30-orange.svg?style=flat-square)](#contributors) - [![Pulls from DockerHub](https://img.shields.io/docker/pulls/containrrr/watchtower.svg?style=flat-square)](https://hub.docker.com/r/containrrr/watchtower) + [![All Contributors](https://img.shields.io/github/all-contributors/containrrr/watchtower)](#contributors) + [![Pulls from DockerHub](https://img.shields.io/docker/pulls/containrrr/watchtower.svg)](https://hub.docker.com/r/containrrr/watchtower) diff --git a/docs/index.md b/docs/index.md index 10d9b16..e999c03 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,10 +33,10 @@ Codacy Badge - All Contributors + All Contributors - Pulls from DockerHub + Pulls from DockerHub

From aecac404139d7776f9495f2417404630cfc0f94d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 19 Jan 2021 07:08:37 +0100 Subject: [PATCH 52/98] docs: add SrihariThalla as a contributor (#787) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> From a5b200e0334dcfbf4d5cd97a8d93e4f4de7c4dce Mon Sep 17 00:00:00 2001 From: Thomas Gaudin Date: Tue, 19 Jan 2021 07:11:35 +0100 Subject: [PATCH 53/98] Fix arguments doc formatting (#782) --- docs/arguments.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/arguments.md b/docs/arguments.md index 9c3d58d..6a615ce 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -248,7 +248,9 @@ Sets an authentication token to HTTP API requests. Environment Variable: WATCHTOWER_HTTP_API_TOKEN Type: String Default: - -```## Filter by scope +``` + +## Filter by scope 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.github.io/watchtower/running-multiple-instances). ``` From 1886fde84a364fdeee959626f04171f69b2543e2 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 19 Jan 2021 07:12:07 +0100 Subject: [PATCH 54/98] docs: add nymous as a contributor (#788) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 +++ 2 files changed, 12 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 33e967a..622d086 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -721,6 +721,15 @@ "contributions": [ "doc" ] + }, + { + "login": "nymous", + "name": "Thomas Gaudin", + "avatar_url": "https://avatars1.githubusercontent.com/u/4216559?v=4", + "profile": "https://nymous.io", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index c002e77..55380d5 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Turtle Kalus

💻
Srihari Thalla

📖 + +
Thomas Gaudin

📖 + From a068203e4d023933bf27c0d9554f535e2c530bb8 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Thu, 4 Feb 2021 09:50:35 +0100 Subject: [PATCH 55/98] delete unused circleci config --- .circleci/config.yml | 227 ------------------------------------------- 1 file changed, 227 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 82d16b5..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,227 +0,0 @@ -version: 2.1 - -executors: - py: - docker: - - image: circleci/python:latest - working_directory: ~/repo - go: - docker: - - image: circleci/golang:latest - working_directory: ~/repo - -workflows: - version: 2 - ci: - jobs: - - checkout: - filters: - branches: - only: /.*/ - tags: - only: /.*/ - - linting: - requires: - - checkout - filters: - branches: - only: /.*/ - tags: - only: /.*/ - - testing: - requires: - - checkout - filters: - branches: - only: /.*/ - tags: - only: /.*/ - - build: - requires: - - testing - - linting - filters: - branches: - only: /.*/ - tags: - ignore: /^v[0-9]+(\.[0-9]+)*$/ - - publishing: - requires: - - testing - - linting - filters: - branches: - ignore: /.*/ - tags: - only: /^v[0-9]+(\.[0-9]+)*$/ - - publish-docs: - requires: - - testing - - linting - filters: - branches: - ignore: /.*/ - tags: - only: /^v[0-9]+(\.[0-9]+)*$/ -jobs: - checkout: - executor: go - steps: - - checkout - - persist_to_workspace: - paths: - - . - root: ~/repo - linting: - executor: go - steps: - - attach_workspace: - at: . - - run: go build . - - run: go get -u golang.org/x/lint/golint - - run: golint -set_exit_status ./... - testing: - executor: go - steps: - - attach_workspace: - at: . - - run: go build ./... - - run: go get github.com/schrej/godacov - - run: go test ./... -coverprofile coverage.out - # - run: godacov -t $CODACY_TOKEN -r ./coverage.out -c $CIRCLE_SHA1 - build: - executor: go - steps: - - attach_workspace: - at: . - - setup_remote_docker - - run: - name: Install Goreleaser - command: | - cd .. && \ - wget https://github.com/goreleaser/goreleaser/releases/download/v0.104.1/goreleaser_Linux_x86_64.tar.gz && \ - tar -xvf goreleaser_Linux_x86_64.tar.gz && \ - ./goreleaser -v - - run: - name: Execute goreleaser - command: CGO_ENABLED=${CGO_ENABLED:-0} ../goreleaser --snapshot --skip-publish --debug - publishing: - executor: go - steps: - - attach_workspace: - at: . - - setup_remote_docker - - run: - name: Install Goreleaser - command: | - cd .. && \ - wget https://github.com/goreleaser/goreleaser/releases/download/v0.104.1/goreleaser_Linux_x86_64.tar.gz && \ - tar -xvf goreleaser_Linux_x86_64.tar.gz && \ - ./goreleaser -v - - run: - name: Login to docker hub - command: | - echo "$DOCKER_PASS" | docker login -u $DOCKER_USER --password-stdin - - run: - name: Execute goreleaser - command: CGO_ENABLED=${CGO_ENABLED:-0} ../goreleaser --debug - - run: - name: Enable experimental docker features - command: | - mkdir -p ~/.docker/ && \ - echo '{"experimental": "enabled"}' > ~/.docker/config.json - - run: - name: Create manifest for version - command: | - docker manifest create \ - containrrr/watchtower:$(echo $CIRCLE_TAG | sed 's/^v*//') \ - containrrr/watchtower:amd64-$(echo $CIRCLE_TAG | sed 's/^v*//') \ - containrrr/watchtower:i386-$(echo $CIRCLE_TAG | sed 's/^v*//') \ - containrrr/watchtower:armhf-$(echo $CIRCLE_TAG | sed 's/^v*//') \ - containrrr/watchtower:arm64v8-$(echo $CIRCLE_TAG | sed 's/^v*//') - - run: - name: Annotate i386 version - command: | - docker manifest annotate \ - containrrr/watchtower:$(echo $CIRCLE_TAG | sed 's/^v*//') \ - containrrr/watchtower:i386-$(echo $CIRCLE_TAG | sed 's/^v*//') \ - --os linux \ - --arch 386 - - run: - name: Annotate ARM version - command: | - docker manifest annotate \ - containrrr/watchtower:$(echo $CIRCLE_TAG | sed 's/^v*//') \ - containrrr/watchtower:armhf-$(echo $CIRCLE_TAG | sed 's/^v*//') \ - --os linux \ - --arch arm - - run: - name: Annotate ARM64 version - command: | - docker manifest annotate \ - containrrr/watchtower:$(echo $CIRCLE_TAG | sed 's/^v*//') \ - containrrr/watchtower:arm64v8-$(echo $CIRCLE_TAG | sed 's/^v*//') \ - --os linux \ - --arch arm64 \ - --variant v8 - - run: - name: Create manifest for latest - command: | - docker manifest create \ - containrrr/watchtower:latest \ - containrrr/watchtower:amd64-latest \ - containrrr/watchtower:i386-latest \ - containrrr/watchtower:armhf-latest \ - containrrr/watchtower:arm64v8-latest - - run: - name: Annotate i386 latest - command: | - docker manifest annotate \ - containrrr/watchtower:latest \ - containrrr/watchtower:i386-latest \ - --os linux \ - --arch 386 - - run: - name: Annotate ARM latest - command: | - docker manifest annotate \ - containrrr/watchtower:latest \ - containrrr/watchtower:armhf-latest \ - --os linux \ - --arch arm - - run: - name: Annotate ARM64 latest - command: | - docker manifest annotate \ - containrrr/watchtower:latest \ - containrrr/watchtower:arm64v8-latest \ - --os linux \ - --arch arm64 \ - --variant v8 - - run: - name: Push manifests to Dockerhub - command: | - echo "$DOCKER_PASS" | docker login -u $DOCKER_USER --password-stdin && - docker manifest push containrrr/watchtower:$(echo $CIRCLE_TAG | sed 's/^v*//') && \ - docker manifest push containrrr/watchtower:latest - publish-docs: - executor: py - steps: - - attach_workspace: - at: . - - run: - name: Install prerequisites - command: | - sudo pip install \ - mkdocs \ - mkdocs-material \ - md-toc - - add_ssh_keys: - fingerprints: - - '91:75:47:15:b2:8e:85:e5:67:0e:63:7f:22:d2:b4:6e' - - run: - name: Generate and publish - command: | - mkdir ~/.ssh && touch ~/.ssh/known_hosts; - ssh-keyscan -H github.com >> ~/.ssh/known_hosts && \ - mkdocs gh-deploy From d0943c3b77537175d816e89f9e90cc995c7b2967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Thu, 4 Feb 2021 11:30:56 +0100 Subject: [PATCH 56/98] chore(ci): set image platform on image build (#811) Co-authored-by: Simon Aronsson --- .github/workflows/pull-request.yml | 2 +- .github/workflows/release.yml | 6 ++-- dockerfiles/Dockerfile | 2 +- goreleaser.yml | 45 ++++++++++++++++++------------ 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 180b105..099e259 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -69,5 +69,5 @@ jobs: - name: Build uses: goreleaser/goreleaser-action@v2 with: - version: v0.104.1 + version: v0.155.0 args: --snapshot --skip-publish --debug diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a83e1f..e2bdb86 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,10 +70,10 @@ jobs: uses: actions/setup-go@v2 with: go-version: 1.15.x - - name: Run goreleaser + - name: Build uses: goreleaser/goreleaser-action@v2 with: - version: v0.104.1 + version: v0.155.0 args: --debug - name: Enable experimental docker features run: | @@ -176,4 +176,4 @@ jobs: - \ No newline at end of file + diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile index 7e28eb2..345f5c2 100644 --- a/dockerfiles/Dockerfile +++ b/dockerfiles/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.11 as alpine +FROM --platform=$BUILDPLATFORM alpine:3.11 as alpine RUN apk add --no-cache \ ca-certificates \ diff --git a/goreleaser.yml b/goreleaser.yml index 927cdcd..2bdd067 100644 --- a/goreleaser.yml +++ b/goreleaser.yml @@ -9,23 +9,26 @@ build: - 386 - arm - arm64 -archive: - name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}" - format: tar.gz - replacements: - arm: armhf - arm64: arm64v8 - amd64: amd64 - 386: 386 - darwin: macOS - linux: linux - format_overrides: - - goos: windows - format: zip - files: - - LICENSE.md +archives: + - + name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}" + format: tar.gz + replacements: + arm: armhf + arm64: arm64v8 + amd64: amd64 + 386: 386 + darwin: macOS + linux: linux + format_overrides: + - goos: windows + format: zip + files: + - LICENSE.md dockers: - + use_buildx: true + build_flag_templates: [ "--platform=linux/amd64" ] goos: linux goarch: amd64 goarm: '' @@ -35,7 +38,9 @@ dockers: - containrrr/watchtower:amd64-latest binaries: - watchtower - - + - + use_buildx: true + build_flag_templates: [ "--platform=linux/386" ] goos: linux goarch: 386 goarm: '' @@ -45,7 +50,9 @@ dockers: - containrrr/watchtower:i386-latest binaries: - watchtower - - + - + use_buildx: true + build_flag_templates: [ "--platform=linux/arm/v6" ] goos: linux goarch: arm goarm: 6 @@ -55,7 +62,9 @@ dockers: - containrrr/watchtower:armhf-latest binaries: - watchtower - - + - + use_buildx: true + build_flag_templates: [ "--platform=linux/arm64/v8" ] goos: linux goarch: arm64 goarm: '' From 52e6e11395e807ea284b28c43447d6d786758d04 Mon Sep 17 00:00:00 2001 From: "D. Domig" <18613935+jokay@users.noreply.github.com> Date: Tue, 9 Feb 2021 10:18:57 +0100 Subject: [PATCH 57/98] docs: update changed contributor username (#817) --- .all-contributorsrc | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 622d086..41c90ac 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -642,10 +642,10 @@ ] }, { - "login": "x-jokay", + "login": "jokay", "name": "D. Domig", "avatar_url": "https://avatars0.githubusercontent.com/u/18613935?v=4", - "profile": "https://github.com/x-jokay", + "profile": "https://github.com/jokay", "contributions": [ "doc" ] diff --git a/README.md b/README.md index 55380d5..983da4b 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
MihailITPlace

💻
bugficks

💻 📖
Michael

💻 -
D. Domig

📖 +
D. Domig

📖
Ben Osheroff

💻 From f10b4e3492f234d4aaea139741ec940b7fee97ad Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Sun, 28 Feb 2021 19:29:18 +0100 Subject: [PATCH 58/98] Update HTTP API docs (#827) --- docs/http-api-mode.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/http-api-mode.md b/docs/http-api-mode.md index def90d5..5f5b19a 100644 --- a/docs/http-api-mode.md +++ b/docs/http-api-mode.md @@ -4,7 +4,7 @@ Watchtower provides an HTTP API mode that enables an HTTP endpoint that can be r --- -To enable this mode, use the flag `--http-api`. For example, in a Docker Compose config file: +To enable this mode, use the flag `--http-api-update`. For example, in a Docker Compose config file: ```yaml version: '3' @@ -19,7 +19,7 @@ services: image: containrrr/watchtower volumes: - /var/run/docker.sock:/var/run/docker.sock - command: --debug --http-api + command: --debug --http-api-update environment: - WATCHTOWER_HTTP_API_TOKEN=mytoken labels: @@ -31,5 +31,5 @@ services: Notice that there is an environment variable named WATCHTOWER_HTTP_API_TOKEN. To prevent external services from accidentally triggering image updates, all of the requests have to contain a "Token" field, valued as the token defined in WATCHTOWER_HTTP_API_TOKEN, in their headers. In this case, there is a port bind to the host machine, allowing to request localhost:8080 to reach Watchtower. The following `curl` command would trigger an image update: ```bash -curl -H "Token: mytoken" localhost:8080/v1/update -``` \ No newline at end of file +curl -H "Authorization: Bearer mytoken" localhost:8080/v1/update +``` From ce9e102835e0f685e93df164d5032fc5ecf6a730 Mon Sep 17 00:00:00 2001 From: hydrargyrum Date: Sun, 28 Feb 2021 18:31:55 +0000 Subject: [PATCH 59/98] docs: fix broken markup of "HTTP API Token" (#834) From 88d1dcf0d22610d357e56642f0d63c13f3706dfa Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 28 Feb 2021 19:32:33 +0100 Subject: [PATCH 60/98] docs: add hydrargyrum as a contributor (#839) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 41c90ac..661ab63 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -730,6 +730,15 @@ "contributions": [ "doc" ] + }, + { + "login": "hydrargyrum", + "name": "hydrargyrum", + "avatar_url": "https://avatars.githubusercontent.com/u/2804645?v=4", + "profile": "https://indigo.re/", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 983da4b..d534ac0 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Thomas Gaudin

📖 +
hydrargyrum

📖 From 70c737cebd2b4accc43627ddf201643ede9ecd63 Mon Sep 17 00:00:00 2001 From: Flavio Maria De Stefano Date: Tue, 2 Mar 2021 11:38:51 +0100 Subject: [PATCH 61/98] Typo in --http-api (#841) --- docs/arguments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/arguments.md b/docs/arguments.md index 6a615ce..01de345 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -234,7 +234,7 @@ Environment Variable: WATCHTOWER_RUN_ONCE Runs Watchtower in HTTP API mode, only allowing image updates to be triggered by an HTTP request. For details see [HTTP API](https://containrrr.github.io/watchtower/http-api-mode). ``` - Argument: --http-api + Argument: --http-api-update Environment Variable: WATCHTOWER_HTTP_API Type: Boolean Default: false From 60a6300f0eef17a38bed137235132b4885612d88 Mon Sep 17 00:00:00 2001 From: Zois Pagoulatos Date: Tue, 9 Mar 2021 15:18:03 +0200 Subject: [PATCH 62/98] Set different default branch for mkdocs edit (#846) From mkdocs [documentation](https://www.mkdocs.org/user-guide/configuration/#edit_uri): ``` Note On a few known hosts (specifically GitHub, Bitbucket and GitLab), the edit_uri is derived from the 'repo_url' and does not need to be set manually. Simply defining a repo_url will automatically populate the edit_uri configs setting. For example, for a GitHub- or GitLab-hosted repository, the edit_uri would be automatically set as edit/master/docs/ (Note the edit path and master branch). For a Bitbucket-hosted repository, the equivalent edit_uri would be automatically set as src/default/docs/ (note the src path and default branch). To use a different URI than the default (for example a different branch), simply set the edit_uri to your desired string. If you do not want any "edit URL link" displayed on your pages, then set edit_uri to an empty string to disable the automatic setting. ``` --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index f628fbc..b40f794 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,7 @@ site_name: Watchtower site_url: http://containrrr.github.io/watchtower/ repo_url: https://github.com/containrrr/watchtower/ +edit_uri: edit/main/docs/index.md theme: name: 'material' palette: From 738215a1f7505e6fe8c92ee0f3f98638d76ec7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 13 Mar 2021 08:58:11 +0100 Subject: [PATCH 63/98] Update Shoutrrr to v0.4 (#810) --- go.mod | 17 ++-- go.sum | 150 +++++++++++++++++++++-------- pkg/notifications/email.go | 47 +++------ pkg/notifications/gotify.go | 36 +++---- pkg/notifications/msteams.go | 40 +++----- pkg/notifications/notifier.go | 38 +++++++- pkg/notifications/notifier_test.go | 42 +++++--- pkg/notifications/slack.go | 27 +++--- pkg/types/convertable_notifier.go | 7 -- pkg/types/convertible_notifier.go | 6 ++ 10 files changed, 244 insertions(+), 166 deletions(-) delete mode 100644 pkg/types/convertable_notifier.go create mode 100644 pkg/types/convertible_notifier.go diff --git a/go.mod b/go.mod index 87327db..bf557f7 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cloudflare/cfssl v0.0.0-20190911221928-1a911ca1b1d6 // indirect github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect - github.com/containrrr/shoutrrr v0.3.0 + github.com/containrrr/shoutrrr v0.4.1 github.com/docker/cli v0.0.0-20190327152802-57b27434ea29 github.com/docker/distribution v2.7.1+incompatible github.com/docker/docker v0.0.0-20190404075923-dbe4a30928d4 @@ -28,39 +28,34 @@ require ( github.com/docker/go-units v0.3.3 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect github.com/gofrs/uuid v3.2.0+incompatible // indirect - github.com/golang/protobuf v1.4.2 // indirect github.com/google/certificate-transparency-go v1.0.21 // indirect github.com/gorilla/mux v1.7.0 // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/hashicorp/go-version v1.1.0 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jinzhu/gorm v1.9.11 // indirect github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22 // indirect github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect - github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/lib/pq v1.2.0 // indirect github.com/miekg/pkcs11 v0.0.0-20190401114359-553cfdd26aaa // indirect github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c // indirect - github.com/onsi/ginkgo v1.11.0 - github.com/onsi/gomega v1.10.0 + github.com/onsi/ginkgo v1.14.2 + github.com/onsi/gomega v1.10.1 github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/runc v0.1.1 // indirect - github.com/pkg/errors v0.8.1 // indirect github.com/prometheus/client_golang v0.9.3 github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 - github.com/sirupsen/logrus v1.4.1 + github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v0.0.7 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.6.3 - github.com/stretchr/testify v1.3.0 + github.com/stretchr/testify v1.4.0 github.com/theupdateframework/notary v0.6.1 // indirect github.com/zmap/zlint v1.0.2 // indirect golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect - golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 + golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 golang.org/x/text v0.3.4 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect gopkg.in/fatih/pool.v2 v2.0.0 // indirect gopkg.in/gorethink/gorethink.v3 v3.0.5 // indirect diff --git a/go.sum b/go.sum index c2e851a..19fe808 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,7 @@ github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWX github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= @@ -38,15 +39,19 @@ github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywR github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= +github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= +github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= +github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= +github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= +github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cfssl v0.0.0-20190911221928-1a911ca1b1d6 h1:A7RURps5t4yDU0zktlgrE3Bdmjfv35nVs+xJdoWgIgY= github.com/cloudflare/cfssl v0.0.0-20190911221928-1a911ca1b1d6/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882bXEDKfWIf0wa8HRvpnBoPszJJXL+TVbBw4M= github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containrrr/shoutrrr v0.0.0-20200601144753-78bb9685bc2f h1:Ln4yl+CYjrapeTEzMJQpgBwLjruKHcMosWFB/d1M4RQ= -github.com/containrrr/shoutrrr v0.0.0-20200601144753-78bb9685bc2f/go.mod h1:eotQeC9bHbsf9eMUnXOU/y5bskegseWNB4PwmxRO7Wc= -github.com/containrrr/shoutrrr v0.3.0 h1:2o1BKQUThSDtcidiMUq99CJijSRDa/nIB8kRhLBYmbk= -github.com/containrrr/shoutrrr v0.3.0/go.mod h1:gqR3sngKPBVaLrmq9Pfw34x/MXxn0ATjY8/dW+rXzrU= +github.com/containrrr/shoutrrr v0.4.1 h1:+p5+3Gb5dhzjUf3yriUIK6IeXtElJFFgBUGD9vb9ygE= +github.com/containrrr/shoutrrr v0.4.1/go.mod h1:zqL2BvfC1W4FujrT4b3/ZCLxvD+uoeEpBL7rg9Dqpbg= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -82,23 +87,45 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNE github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= +github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-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-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -113,6 +140,9 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 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.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= @@ -135,13 +165,21 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I= +github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U= github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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= @@ -169,14 +207,20 @@ github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22/go.mod h1:u0Jo4 github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07 h1:+kBG/8rjCa6vxJZbUjAiE4MQmBEBYc8nLEb51frnvBY= github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07/go.mod h1:j1kV/8f3jowErEq4XyeypkCdvg5EeHkf0YCKCcq5Ybo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaHeMeg= +github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -186,6 +230,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -193,14 +239,22 @@ github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDe github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -212,25 +266,30 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/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 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.6 h1:11TGpSHY7Esh/i/qnq02Jo5oVrI1Gue8Slbq0ujPZFQ= +github.com/nxadm/tail v1.4.6/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.10.0 h1:Gwkk+PTu/nfOwNMtUB/mRUv0X7ewW5dO4AERT1ThVKo= -github.com/onsi/gomega v1.10.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -273,11 +332,14 @@ github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967/go.mod h1:JGuDeoQd7Z6y github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -289,14 +351,13 @@ github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU= github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 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= @@ -311,12 +372,19 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0= github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/weppos/publicsuffix-go v0.4.0 h1:YSnfg3V65LcCFKtIGKGoBhkyKolEd0hlipcXaOjdnQw= github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= @@ -327,11 +395,13 @@ github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e h1:mvOa4+/DXStR4ZXOks github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e/go.mod h1:w7kd3qXHh8FNaczNjslXqvFQiv5mMWRXlL9klTUAHc8= github.com/zmap/zlint v1.0.2 h1:07+WuC/prlXVlWa1CJx2lCpuCd8biIeBAVnwTN2CPaA= github.com/zmap/zlint v1.0.2/go.mod h1:29UiAJNsiVdvTBFCJW8e3q6dcDbOoPkhMgttOSCIMMY= +go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 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-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -344,9 +414,11 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk 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-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -356,8 +428,9 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/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-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 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-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -367,23 +440,9 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= 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/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-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190830141801-acfa387b8d69 h1:Wdn4Yb8d5VrsO3jWgaeSZss09x1VLVBMePDh4VW/xSQ= -golang.org/x/sys v0.0.0-20190830141801-acfa387b8d69/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-20200409092240-59c9f1ba88fa h1:mQTN3ECqfsViCNBgq+A40vdwhkGykrrQlYe3mPj6BoU= -golang.org/x/sys v0.0.0-20200409092240-59c9f1ba88fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= @@ -394,6 +453,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -402,7 +463,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/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/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -427,6 +488,7 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -438,6 +500,7 @@ gopkg.in/fatih/pool.v2 v2.0.0 h1:xIFeWtxifuQJGk/IEPKsTduEKcKvPmhoiVDGpC40nKg= gopkg.in/fatih/pool.v2 v2.0.0/go.mod h1:8xVGeu1/2jr2wm5V9SPuMht2H5AEmf5aFMGSQixtjTY= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/gorethink/gorethink.v3 v3.0.5 h1:e2Uc/Xe+hpcVQFsj6MuHlYog3r0JYpnTzwDj/y2O4MU= gopkg.in/gorethink/gorethink.v3 v3.0.5/go.mod h1:+3yIIHJUGMBK+wyPH+iN5TP+88ikFDfZdqTlK3Y9q8I= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -452,12 +515,19 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 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.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= -gosrc.io/xmpp v0.1.1 h1:iMtE9W3fx254+4E6rI34AOPJDqWvpfQR6EYaVMzhJ4s= -gosrc.io/xmpp v0.1.1/go.mod h1:4JgaXzw4MnEv2sGltONtK3GMhj+h9gpQ7cO8nwbFJLU= +gosrc.io/xmpp v0.5.1 h1:Rgrm5s2rt+npGggJH3HakQxQXR8ZZz3+QRzakRQqaq4= +gosrc.io/xmpp v0.5.1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= +gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 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= +mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= +nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY= +nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k= +nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index 184bf84..4984139 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -1,11 +1,8 @@ package notifications import ( - "fmt" - "os" "time" - "github.com/containrrr/shoutrrr/pkg/format" "github.com/spf13/cobra" shoutrrrSmtp "github.com/containrrr/shoutrrr/pkg/services/smtp" @@ -29,11 +26,11 @@ type emailTypeNotifier struct { } // NewEmailNotifier is a factory method creating a new email notifier instance -func NewEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { +func NewEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier { return newEmailNotifier(c, acceptedLogLevels) } -func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { +func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier { flags := c.PersistentFlags() from, _ := flags.GetString("notification-email-from") @@ -63,7 +60,7 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert return n } -func (e *emailTypeNotifier) GetURL() string { +func (e *emailTypeNotifier) GetURL() (string, error) { conf := &shoutrrrSmtp.Config{ FromAddress: e.From, FromName: "Watchtower", @@ -73,43 +70,29 @@ func (e *emailTypeNotifier) GetURL() string { Subject: e.getSubject(), Username: e.User, Password: e.Password, - UseStartTLS: true, + UseStartTLS: !e.tlsSkipVerify, UseHTML: false, + Encryption: shoutrrrSmtp.EncMethods.Auto, + Auth: shoutrrrSmtp.AuthTypes.None, } - pkr := format.NewPropKeyResolver(conf) - var err error if len(e.User) > 0 { - err = pkr.Set("auth", "Plain") - } else { - err = pkr.Set("auth", "None") + conf.Auth = shoutrrrSmtp.AuthTypes.Plain } - if err != nil { - fmt.Printf("Could not set auth type for email notifier: %v", err) + if e.tlsSkipVerify { + conf.Encryption = shoutrrrSmtp.EncMethods.None } - return conf.GetURL().String() + return conf.GetURL().String(), nil } func (e *emailTypeNotifier) getSubject() string { - var emailSubject string + subject := GetTitle() - if e.SubjectTag == "" { - emailSubject = "Watchtower updates" - } else { - emailSubject = e.SubjectTag + " Watchtower updates" + if e.SubjectTag != "" { + subject = e.SubjectTag + " " + subject } - if hostname, err := os.Hostname(); err == nil { - emailSubject += " on " + hostname - } - return emailSubject + + return subject } - -// TODO: Delete these once all notifiers have been converted to shoutrrr -func (e *emailTypeNotifier) StartNotification() {} -func (e *emailTypeNotifier) SendNotification() {} -func (e *emailTypeNotifier) Levels() []log.Level { return nil } -func (e *emailTypeNotifier) Fire(entry *log.Entry) error { return nil } - -func (e *emailTypeNotifier) Close() {} diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go index bb475bf..7a6009b 100644 --- a/pkg/notifications/gotify.go +++ b/pkg/notifications/gotify.go @@ -1,6 +1,7 @@ package notifications import ( + "net/url" "strings" shoutrrrGotify "github.com/containrrr/shoutrrr/pkg/services/gotify" @@ -22,20 +23,20 @@ type gotifyTypeNotifier struct { } // NewGotifyNotifier is a factory method creating a new gotify notifier instance -func NewGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertableNotifier { +func NewGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifier { return newGotifyNotifier(c, levels) } -func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertableNotifier { +func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertibleNotifier { flags := c.PersistentFlags() - url := getGotifyURL(flags) + apiURL := getGotifyURL(flags) token := getGotifyToken(flags) skipVerify, _ := flags.GetBool("notification-gotify-tls-skip-verify") n := &gotifyTypeNotifier{ - gotifyURL: url, + gotifyURL: apiURL, gotifyAppToken: token, gotifyInsecureSkipVerify: skipVerify, logLevels: levels, @@ -66,26 +67,19 @@ func getGotifyURL(flags *pflag.FlagSet) string { return gotifyURL } -func (n *gotifyTypeNotifier) GetURL() string { - url := n.gotifyURL - - if strings.HasPrefix(url, "https://") { - url = strings.TrimPrefix(url, "https://") - } else { - url = strings.TrimPrefix(url, "http://") +func (n *gotifyTypeNotifier) GetURL() (string, error) { + apiURL, err := url.Parse(n.gotifyURL) + if err != nil { + return "", err } - url = strings.TrimSuffix(url, "/") - config := &shoutrrrGotify.Config{ - Host: url, - Token: n.gotifyAppToken, + Host: apiURL.Host, + Path: apiURL.Path, + DisableTLS: apiURL.Scheme == "http", + Title: GetTitle(), + Token: n.gotifyAppToken, } - return config.GetURL().String() + return config.GetURL().String(), nil } - -func (n *gotifyTypeNotifier) StartNotification() {} -func (n *gotifyTypeNotifier) SendNotification() {} -func (n *gotifyTypeNotifier) Close() {} -func (n *gotifyTypeNotifier) Levels() []log.Level { return nil } diff --git a/pkg/notifications/msteams.go b/pkg/notifications/msteams.go index 63c6aaa..6c47229 100644 --- a/pkg/notifications/msteams.go +++ b/pkg/notifications/msteams.go @@ -1,12 +1,11 @@ package notifications import ( - "strings" - shoutrrrTeams "github.com/containrrr/shoutrrr/pkg/services/teams" t "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "net/url" ) const ( @@ -20,11 +19,11 @@ type msTeamsTypeNotifier struct { } // NewMsTeamsNotifier is a factory method creating a new teams notifier instance -func NewMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { +func NewMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier { return newMsTeamsNotifier(cmd, acceptedLogLevels) } -func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { +func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier { flags := cmd.PersistentFlags() @@ -43,26 +42,19 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con return n } -func (n *msTeamsTypeNotifier) GetURL() string { - - baseURL := "https://outlook.office.com/webhook/" - - path := strings.Replace(n.webHookURL, baseURL, "", 1) - rawToken := strings.Replace(path, "/IncomingWebhook", "", 1) - token := strings.Split(rawToken, "/") - config := &shoutrrrTeams.Config{ - Token: shoutrrrTeams.Token{ - A: token[0], - B: token[1], - C: token[2], - }, +func (n *msTeamsTypeNotifier) GetURL() (string, error) { + webhookURL, err := url.Parse(n.webHookURL) + if err != nil { + return "", err } - return config.GetURL().String() -} + config, err := shoutrrrTeams.ConfigFromWebhookURL(*webhookURL) + if err != nil { + return "", err + } -func (n *msTeamsTypeNotifier) StartNotification() {} -func (n *msTeamsTypeNotifier) SendNotification() {} -func (n *msTeamsTypeNotifier) Close() {} -func (n *msTeamsTypeNotifier) Levels() []log.Level { return nil } -func (n *msTeamsTypeNotifier) Fire(entry *log.Entry) error { return nil } + config.Color = ColorHex + config.Title = GetTitle() + + return config.GetURL().String(), nil +} diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index 651ab6a..938bb9e 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -5,6 +5,7 @@ import ( "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "os" ) // Notifier can send log output as notification to admins, with optional batching. @@ -36,13 +37,13 @@ func NewNotifier(c *cobra.Command) *Notifier { log.WithField("could not read notifications argument", log.Fields{"Error": err}).Fatal() } - n.types = n.GetNotificationTypes(c, acceptedLogLevels, types) + n.types = n.getNotificationTypes(c, acceptedLogLevels, types) return n } -// GetNotificationTypes produces an array of notifiers from a list of types -func (n *Notifier) GetNotificationTypes(cmd *cobra.Command, levels []log.Level, types []string) []ty.Notifier { +// getNotificationTypes produces an array of notifiers from a list of types +func (n *Notifier) getNotificationTypes(cmd *cobra.Command, levels []log.Level, types []string) []ty.Notifier { output := make([]ty.Notifier, 0) for _, t := range types { @@ -52,7 +53,8 @@ func (n *Notifier) GetNotificationTypes(cmd *cobra.Command, levels []log.Level, continue } - var legacyNotifier ty.ConvertableNotifier + var legacyNotifier ty.ConvertibleNotifier + var err error switch t { case emailType: @@ -65,11 +67,20 @@ func (n *Notifier) GetNotificationTypes(cmd *cobra.Command, levels []log.Level, legacyNotifier = newGotifyNotifier(cmd, []log.Level{}) default: log.Fatalf("Unknown notification type %q", t) + // Not really needed, used for nil checking static analysis + continue } + shoutrrrURL, err := legacyNotifier.GetURL() + if err != nil { + log.Fatal("failed to create notification config:", err) + } + + println(shoutrrrURL) + notifier := newShoutrrrNotifierFromURL( cmd, - legacyNotifier.GetURL(), + shoutrrrURL, levels, ) @@ -99,3 +110,20 @@ func (n *Notifier) Close() { t.Close() } } + +// GetTitle returns a common notification title with hostname appended +func GetTitle() (title string) { + title = "Watchtower updates" + + if hostname, err := os.Hostname(); err == nil { + title += " on " + hostname + } + + return +} + +// ColorHex is the default notification color used for services that support it (formatted as a CSS hex string) +const ColorHex = "#406170" + +// ColorInt is the default notification color used for services that support it (as an int value) +const ColorInt = 0x406170 diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go index 57132ac..5ef75a0 100644 --- a/pkg/notifications/notifier_test.go +++ b/pkg/notifications/notifier_test.go @@ -2,6 +2,7 @@ package notifications_test import ( "fmt" + "net/url" "os" "testing" @@ -28,7 +29,9 @@ var _ = Describe("notifications", func() { When("passing a discord url to the slack notifier", func() { channel := "123456789" token := "abvsihdbau" - expected := fmt.Sprintf("discord://%s@%s", token, channel) + color := notifications.ColorInt + title := url.QueryEscape(notifications.GetTitle()) + expected := fmt.Sprintf("discord://%s@%s?avatar=&color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&splitlines=Yes&title=%s&username=watchtower", token, channel, color, title) buildArgs := func(url string) []string { return []string{ "--notifications", @@ -55,9 +58,11 @@ var _ = Describe("notifications", func() { tokenA := "aaa" tokenB := "bbb" tokenC := "ccc" + color := url.QueryEscape(notifications.ColorHex) + title := url.QueryEscape(notifications.GetTitle()) hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC) - expectedOutput := fmt.Sprintf("slack://%s@%s/%s/%s", username, tokenA, tokenB, tokenC) + expectedOutput := fmt.Sprintf("slack://%s@%s/%s/%s?color=%s&title=%s", username, tokenA, tokenB, tokenC, color, title) args := []string{ "--notification-slack-hook-url", @@ -78,8 +83,9 @@ var _ = Describe("notifications", func() { It("should return the expected URL", func() { token := "aaa" host := "shoutrrr.local" + title := url.QueryEscape(notifications.GetTitle()) - expectedOutput := fmt.Sprintf("gotify://%s/%s", host, token) + expectedOutput := fmt.Sprintf("gotify://%s/%s?disabletls=No&priority=0&title=%s", host, token, title) args := []string{ "--notification-gotify-url", @@ -99,12 +105,14 @@ var _ = Describe("notifications", func() { It("should return the expected URL", func() { - tokenA := "aaa" - tokenB := "bbb" - tokenC := "ccc" + tokenA := "11111111-4444-4444-8444-cccccccccccc@22222222-4444-4444-8444-cccccccccccc" + tokenB := "33333333012222222222333333333344" + tokenC := "44444444-4444-4444-8444-cccccccccccc" + color := url.QueryEscape(notifications.ColorHex) + title := url.QueryEscape(notifications.GetTitle()) hookURL := fmt.Sprintf("https://outlook.office.com/webhook/%s/IncomingWebhook/%s/%s", tokenA, tokenB, tokenC) - expectedOutput := fmt.Sprintf("teams://%s:%s@%s", tokenA, tokenB, tokenC) + expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s&host=outlook.office.com&title=%s", tokenA, tokenB, tokenC, color, title) args := []string{ "--notification-msteams-hook", @@ -156,20 +164,30 @@ func buildExpectedURL(username string, password string, host string, port int, f subject := fmt.Sprintf("Watchtower updates on %s", hostname) - var template = "smtp://%s:%s@%s:%d/?auth=%s&encryption=None&fromaddress=%s&fromname=Watchtower&starttls=Yes&subject=%s&toaddresses=%s&usehtml=No" - return fmt.Sprintf(template, username, password, host, port, auth, from, subject, to) + var template = "smtp://%s:%s@%s:%d/?auth=%s&encryption=Auto&fromaddress=%s&fromname=Watchtower&starttls=Yes&subject=%s&toaddresses=%s&usehtml=No" + return fmt.Sprintf(template, + url.QueryEscape(username), + url.QueryEscape(password), + host, port, auth, + url.QueryEscape(from), + url.QueryEscape(subject), + url.QueryEscape(to)) } -type builderFn = func(c *cobra.Command, acceptedLogLevels []log.Level) types.ConvertableNotifier +type builderFn = func(c *cobra.Command, acceptedLogLevels []log.Level) types.ConvertibleNotifier func testURL(builder builderFn, args []string, expectedURL string) { command := cmd.NewRootCommand() flags.RegisterNotificationFlags(command) - command.ParseFlags(args) + + err := command.ParseFlags(args) + Expect(err).NotTo(HaveOccurred()) notifier := builder(command, []log.Level{}) - actualURL := notifier.GetURL() + actualURL, err := notifier.GetURL() + + Expect(err).NotTo(HaveOccurred()) Expect(actualURL).To(Equal(expectedURL)) } diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go index ede7141..b3df119 100644 --- a/pkg/notifications/slack.go +++ b/pkg/notifications/slack.go @@ -20,11 +20,11 @@ type slackTypeNotifier struct { } // NewSlackNotifier is a factory function used to generate new instance of the slack notifier type -func NewSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { +func NewSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier { return newSlackNotifier(c, acceptedLogLevels) } -func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { +func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertibleNotifier { flags := c.PersistentFlags() hookURL, _ := flags.GetString("notification-slack-hook-url") @@ -46,7 +46,7 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert return n } -func (s *slackTypeNotifier) GetURL() string { +func (s *slackTypeNotifier) GetURL() (string, error) { trimmedURL := strings.TrimRight(s.HookURL, "/") trimmedURL = strings.TrimLeft(trimmedURL, "https://") parts := strings.Split(trimmedURL, "/") @@ -54,10 +54,14 @@ func (s *slackTypeNotifier) GetURL() string { if parts[0] == "discord.com" || parts[0] == "discordapp.com" { log.Debug("Detected a discord slack wrapper URL, using shoutrrr discord service") conf := &shoutrrrDisco.Config{ - Channel: parts[len(parts)-3], - Token: parts[len(parts)-2], + Channel: parts[len(parts)-3], + Token: parts[len(parts)-2], + Color: ColorInt, + Title: GetTitle(), + SplitLines: true, + Username: s.Username, } - return conf.GetURL().String() + return conf.GetURL().String(), nil } rawTokens := strings.Replace(s.HookURL, "https://hooks.slack.com/services/", "", 1) @@ -66,14 +70,9 @@ func (s *slackTypeNotifier) GetURL() string { conf := &shoutrrrSlack.Config{ BotName: s.Username, Token: tokens, + Color: ColorHex, + Title: GetTitle(), } - return conf.GetURL().String() + return conf.GetURL().String(), nil } - -func (s *slackTypeNotifier) StartNotification() { -} - -func (s *slackTypeNotifier) SendNotification() {} - -func (s *slackTypeNotifier) Close() {} diff --git a/pkg/types/convertable_notifier.go b/pkg/types/convertable_notifier.go deleted file mode 100644 index 3d7ac82..0000000 --- a/pkg/types/convertable_notifier.go +++ /dev/null @@ -1,7 +0,0 @@ -package types - -// ConvertableNotifier is a notifier capable of creating a shoutrrr URL -type ConvertableNotifier interface { - Notifier - GetURL() string -} diff --git a/pkg/types/convertible_notifier.go b/pkg/types/convertible_notifier.go new file mode 100644 index 0000000..2614d12 --- /dev/null +++ b/pkg/types/convertible_notifier.go @@ -0,0 +1,6 @@ +package types + +// ConvertibleNotifier is a notifier capable of creating a shoutrrr URL +type ConvertibleNotifier interface { + GetURL() (string, error) +} From 45168e8515cfe87740d219b822131618e58dea70 Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Tue, 23 Mar 2021 16:56:12 +0100 Subject: [PATCH 64/98] Doc fix: default interval is 24h instead of 5m (#856) Probably due to the docker hub rate limiting, the default interval was changed from 5 minutes to 24 hours. It is mentioned in the documentation for `--interval`, but not yet here :-) --- docs/usage-overview.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/usage-overview.md b/docs/usage-overview.md index b5737c3..04178a4 100644 --- a/docs/usage-overview.md +++ b/docs/usage-overview.md @@ -39,7 +39,7 @@ docker run -d \ > NOTE: if you mount `config.json` in the manner above, changes from the host system will (generally) not be propagated to the running container. Mounting files into the Docker daemon uses bind mounts, which are based on inodes. Most applications (including `docker login` and `vim`) will not directly edit the file, but instead make a copy and replace the original file, which results in a new inode which in turn *breaks* the bind mount. **As a workaround**, you can create a symlink to your `config.json` file and then mount the symlink in the container. The symlinked file will always have the same inode, which keeps the bind mount intact and will ensure changes to the original file are propagated to the running container (regardless of the inode of the source file!). -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 5 minutes. +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. ```yaml version: "3" @@ -55,4 +55,4 @@ services: - /var/run/docker.sock:/var/run/docker.sock - /root/.docker/config.json:/config.json command: --interval 30 -``` \ No newline at end of file +``` From 5e17ef6014e4fbd4b988ba2995753d2a3b54519a Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 23 Mar 2021 16:56:22 +0100 Subject: [PATCH 65/98] docs: add reinout as a contributor (#857) Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 1 + 2 files changed, 10 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 661ab63..1973f66 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -739,6 +739,15 @@ "contributions": [ "doc" ] + }, + { + "login": "reinout", + "name": "Reinout van Rees", + "avatar_url": "https://avatars.githubusercontent.com/u/121433?v=4", + "profile": "https://reinout.vanrees.org", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index d534ac0..4c2b805 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Thomas Gaudin

📖
hydrargyrum

📖 +
Reinout van Rees

📖 From 9fa2fd82a697bd66fdbfb64bc7cc1223761bf9d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 28 Mar 2021 21:04:11 +0200 Subject: [PATCH 66/98] feat: include additional info in startup (#809) --- build.sh | 5 ++ cmd/root.go | 109 ++++++++++++++++++++++++++-------- pkg/filters/filters.go | 35 ++++++++++- pkg/filters/filters_test.go | 6 +- pkg/notifications/notifier.go | 21 +++++++ pkg/notifications/shoutrrr.go | 63 ++++++++++++-------- pkg/types/notifier.go | 1 + 7 files changed, 185 insertions(+), 55 deletions(-) create mode 100644 build.sh diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..363dc74 --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +VERSION=$(git describe) +echo "Building $VERSION..." +go build -o watchtower -ldflags "-X github.com/containrrr/watchtower/cmd.version=$VERSION" \ No newline at end of file diff --git a/cmd/root.go b/cmd/root.go index 0aeeac6..3a597d0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,14 +1,15 @@ package cmd import ( - metrics2 "github.com/containrrr/watchtower/pkg/metrics" + "math" "os" "os/signal" "strconv" + "strings" "syscall" "time" - "github.com/containrrr/watchtower/pkg/api/metrics" + apiMetrics "github.com/containrrr/watchtower/pkg/api/metrics" "github.com/containrrr/watchtower/pkg/api/update" "github.com/containrrr/watchtower/internal/actions" @@ -16,6 +17,7 @@ import ( "github.com/containrrr/watchtower/pkg/api" "github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/filters" + "github.com/containrrr/watchtower/pkg/metrics" "github.com/containrrr/watchtower/pkg/notifications" t "github.com/containrrr/watchtower/pkg/types" "github.com/robfig/cron" @@ -36,6 +38,8 @@ var ( lifecycleHooks bool rollingRestart bool scope string + // Set on build using ldflags + version = "v0.0.0-unknown" ) var rootCmd = NewRootCommand() @@ -69,7 +73,7 @@ func Execute() { } // PreRun is a lifecycle hook that runs before the command is executed. -func PreRun(cmd *cobra.Command, args []string) { +func PreRun(cmd *cobra.Command, _ []string) { f := cmd.PersistentFlags() if enabled, _ := f.GetBool("no-color"); enabled { @@ -146,7 +150,7 @@ func PreRun(cmd *cobra.Command, args []string) { // Run is the main execution flow of the command func Run(c *cobra.Command, names []string) { - filter := filters.BuildFilter(names, enableLabel, scope) + filter, filterDesc := filters.BuildFilter(names, enableLabel, scope) runOnce, _ := c.PersistentFlags().GetBool("run-once") enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update") enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics") @@ -154,9 +158,7 @@ func Run(c *cobra.Command, names []string) { apiToken, _ := c.PersistentFlags().GetString("http-api-token") if runOnce { - if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage { - log.Info("Running a one time update.") - } + writeStartupMessage(c, time.Time{}, filterDesc) runUpdatesWithNotifications(filter) notifier.Close() os.Exit(0) @@ -175,39 +177,99 @@ func Run(c *cobra.Command, names []string) { } if enableMetricsAPI { - metricsHandler := metrics.New() + metricsHandler := apiMetrics.New() httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle) } - httpAPI.Start(enableUpdateAPI) + if err := httpAPI.Start(enableUpdateAPI); err != nil { + log.Error("failed to start API", err) + } - if err := runUpgradesOnSchedule(c, filter); err != nil { + if err := runUpgradesOnSchedule(c, filter, filterDesc); err != nil { log.Error(err) } os.Exit(1) } -func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { +func formatDuration(d time.Duration) string { + sb := strings.Builder{} + + hours := int64(d.Hours()) + minutes := int64(math.Mod(d.Minutes(), 60)) + seconds := int64(math.Mod(d.Seconds(), 60)) + + if hours == 1 { + sb.WriteString("1 hour") + } else if hours != 0 { + sb.WriteString(strconv.FormatInt(hours, 10)) + sb.WriteString(" hours") + } + + if hours != 0 && (seconds != 0 || minutes != 0) { + sb.WriteString(", ") + } + + if minutes == 1 { + sb.WriteString("1 minute") + } else if minutes != 0 { + sb.WriteString(strconv.FormatInt(minutes, 10)) + sb.WriteString(" minutes") + } + + if minutes != 0 && (seconds != 0) { + sb.WriteString(", ") + } + + if seconds == 1 { + sb.WriteString("1 second") + } else if seconds != 0 || (hours == 0 && minutes == 0) { + sb.WriteString(strconv.FormatInt(seconds, 10)) + sb.WriteString(" seconds") + } + + return sb.String() +} + +func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) { + if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage { + schedMessage := "Running a one time update." + if !sched.IsZero() { + until := formatDuration(time.Until(sched)) + schedMessage = "Scheduling first run: " + sched.Format("2006-01-02 15:04:05 -0700 MST") + + "\nNote that the first check will be performed in " + until + } + + notifs := "Using no notifications" + notifList := notifier.String() + if len(notifList) > 0 { + notifs = "Using notifications: " + notifList + } + + log.Info("Watchtower ", version, "\n", notifs, "\n", filtering, "\n", schedMessage) + } +} + +func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string) error { tryLockSem := make(chan bool, 1) tryLockSem <- true - cron := cron.New() - err := cron.AddFunc( + scheduler := cron.New() + err := scheduler.AddFunc( scheduleSpec, func() { select { case v := <-tryLockSem: defer func() { tryLockSem <- v }() metric := runUpdatesWithNotifications(filter) - metrics2.RegisterScan(metric) + metrics.RegisterScan(metric) default: // Update was skipped - metrics2.RegisterScan(nil) + metrics.RegisterScan(nil) log.Debug("Skipped another update already running.") } - nextRuns := cron.Entries() + nextRuns := scheduler.Entries() if len(nextRuns) > 0 { log.Debug("Scheduled next run: " + nextRuns[0].Next.String()) } @@ -217,11 +279,9 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { return err } - if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage { - log.Info("Starting Watchtower and scheduling first run: " + cron.Entries()[0].Schedule.Next(time.Now()).String()) - } + writeStartupMessage(c, scheduler.Entries()[0].Schedule.Next(time.Now()), filtering) - cron.Start() + scheduler.Start() // Graceful shut-down on SIGINT/SIGTERM interrupt := make(chan os.Signal, 1) @@ -229,14 +289,13 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter) error { signal.Notify(interrupt, syscall.SIGTERM) <-interrupt - cron.Stop() + scheduler.Stop() log.Info("Waiting for running update to be finished...") <-tryLockSem return nil } -func runUpdatesWithNotifications(filter t.Filter) *metrics2.Metric { - +func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric { notifier.StartNotification() updateParams := t.UpdateParams{ Filter: filter, @@ -247,10 +306,10 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics2.Metric { LifecycleHooks: lifecycleHooks, RollingRestart: rollingRestart, } - metrics, err := actions.Update(client, updateParams) + metricResults, err := actions.Update(client, updateParams) if err != nil { log.Println(err) } notifier.SendNotification() - return metrics + return metricResults } diff --git a/pkg/filters/filters.go b/pkg/filters/filters.go index 0e37885..18f39c2 100644 --- a/pkg/filters/filters.go +++ b/pkg/filters/filters.go @@ -1,6 +1,9 @@ package filters -import t "github.com/containrrr/watchtower/pkg/types" +import ( + t "github.com/containrrr/watchtower/pkg/types" + "strings" +) // WatchtowerContainersFilter filters only watchtower containers func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatchtower() } @@ -68,19 +71,45 @@ func FilterByScope(scope string, baseFilter t.Filter) t.Filter { } // BuildFilter creates the needed filter of containers -func BuildFilter(names []string, enableLabel bool, scope string) t.Filter { +func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, string) { + sb := strings.Builder{} filter := NoFilter filter = FilterByNames(names, filter) + + if len(names) > 0 { + sb.WriteString("with name \"") + for i, n := range names { + sb.WriteString(n) + if i < len(names)-1 { + sb.WriteString(`" or "`) + } + } + sb.WriteString(`", `) + } + if enableLabel { // If label filtering is enabled, containers should only be considered // if the label is specifically set. filter = FilterByEnableLabel(filter) + sb.WriteString("using enable label, ") } if scope != "" { // If a scope has been defined, containers should only be considered // if the scope is specifically set. filter = FilterByScope(scope, filter) + sb.WriteString(`in scope "`) + sb.WriteString(scope) + sb.WriteString(`", `) } filter = FilterByDisabledLabel(filter) - return filter + + filterDesc := "Checking all containers (except explicitly disabled with label)" + if sb.Len() > 0 { + filterDesc = "Only checking containers " + sb.String() + + // Remove the last ", " + filterDesc = filterDesc[:len(filterDesc)-2] + } + + return filter, filterDesc } diff --git a/pkg/filters/filters_test.go b/pkg/filters/filters_test.go index 5766b64..3b52b5e 100644 --- a/pkg/filters/filters_test.go +++ b/pkg/filters/filters_test.go @@ -114,7 +114,8 @@ func TestBuildFilter(t *testing.T) { var names []string names = append(names, "test") - filter := BuildFilter(names, false, "") + filter, desc := BuildFilter(names, false, "") + assert.Contains(t, desc, "test") container := new(mocks.FilterableContainer) container.On("Name").Return("Invalid") @@ -150,7 +151,8 @@ func TestBuildFilterEnableLabel(t *testing.T) { var names []string names = append(names, "test") - filter := BuildFilter(names, true, "") + filter, desc := BuildFilter(names, true, "") + assert.Contains(t, desc, "using enable label") container := new(mocks.FilterableContainer) container.On("Enabled").Return(false, false) diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index 938bb9e..b9e322e 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -6,6 +6,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "os" + "strings" ) // Notifier can send log output as notification to admins, with optional batching. @@ -42,6 +43,26 @@ func NewNotifier(c *cobra.Command) *Notifier { return n } +func (n *Notifier) String() string { + if len(n.types) < 1 { + return "" + } + + sb := strings.Builder{} + for _, notif := range n.types { + for _, name := range notif.GetNames() { + sb.WriteString(name) + sb.WriteString(", ") + } + } + names := sb.String() + + // remove the last separator + names = names[:len(names)-2] + + return names +} + // getNotificationTypes produces an array of notifiers from a list of types func (n *Notifier) getNotificationTypes(cmd *cobra.Command, levels []log.Level, types []string) []ty.Notifier { output := make([]ty.Notifier, 0) diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index 2715711..8376c91 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -33,16 +33,29 @@ type shoutrrrTypeNotifier struct { done chan bool } +func (n *shoutrrrTypeNotifier) GetNames() []string { + names := make([]string, len(n.Urls)) + for i, u := range n.Urls { + schemeEnd := strings.Index(u, ":") + if schemeEnd <= 0 { + names[i] = "invalid" + continue + } + names[i] = u[:schemeEnd] + } + return names +} + func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { flags := c.PersistentFlags() urls, _ := flags.GetStringArray("notification-url") - template := getShoutrrrTemplate(c) - return createSender(urls, acceptedLogLevels, template) + tpl := getShoutrrrTemplate(c) + return createSender(urls, acceptedLogLevels, tpl) } func newShoutrrrNotifierFromURL(c *cobra.Command, url string, levels []log.Level) t.Notifier { - template := getShoutrrrTemplate(c) - return createSender([]string{url}, levels, template) + tpl := getShoutrrrTemplate(c) + return createSender([]string{url}, levels, tpl) } func createSender(urls []string, levels []log.Level, template *template.Template) t.Notifier { @@ -83,54 +96,54 @@ func sendNotifications(n *shoutrrrTypeNotifier) { n.done <- true } -func (e *shoutrrrTypeNotifier) buildMessage(entries []*log.Entry) string { +func (n *shoutrrrTypeNotifier) buildMessage(entries []*log.Entry) string { var body bytes.Buffer - if err := e.template.Execute(&body, entries); err != nil { + if err := n.template.Execute(&body, entries); err != nil { fmt.Printf("Failed to execute Shoutrrrr template: %s\n", err.Error()) } return body.String() } -func (e *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry) { - msg := e.buildMessage(entries) - e.messages <- msg +func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry) { + msg := n.buildMessage(entries) + n.messages <- msg } -func (e *shoutrrrTypeNotifier) StartNotification() { - if e.entries == nil { - e.entries = make([]*log.Entry, 0, 10) +func (n *shoutrrrTypeNotifier) StartNotification() { + if n.entries == nil { + n.entries = make([]*log.Entry, 0, 10) } } -func (e *shoutrrrTypeNotifier) SendNotification() { - if e.entries == nil || len(e.entries) <= 0 { +func (n *shoutrrrTypeNotifier) SendNotification() { + if n.entries == nil || len(n.entries) <= 0 { return } - e.sendEntries(e.entries) - e.entries = nil + n.sendEntries(n.entries) + n.entries = nil } -func (e *shoutrrrTypeNotifier) Close() { - close(e.messages) +func (n *shoutrrrTypeNotifier) Close() { + close(n.messages) // Use fmt so it doesn't trigger another notification. fmt.Println("Waiting for the notification goroutine to finish") - _ = <-e.done + _ = <-n.done } -func (e *shoutrrrTypeNotifier) Levels() []log.Level { - return e.logLevels +func (n *shoutrrrTypeNotifier) Levels() []log.Level { + return n.logLevels } -func (e *shoutrrrTypeNotifier) Fire(entry *log.Entry) error { - if e.entries != nil { - e.entries = append(e.entries, entry) +func (n *shoutrrrTypeNotifier) Fire(entry *log.Entry) error { + if n.entries != nil { + n.entries = append(n.entries, entry) } else { // Log output generated outside a cycle is sent immediately. - e.sendEntries([]*log.Entry{entry}) + n.sendEntries([]*log.Entry{entry}) } return nil } diff --git a/pkg/types/notifier.go b/pkg/types/notifier.go index 27dc483..f72f980 100644 --- a/pkg/types/notifier.go +++ b/pkg/types/notifier.go @@ -4,5 +4,6 @@ package types type Notifier interface { StartNotification() SendNotification() + GetNames() []string Close() } From 228dd75d7b630c201f8b6b9775a5b1e41042d7f1 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 28 Mar 2021 21:08:28 +0200 Subject: [PATCH 67/98] Update release-dev.yaml --- .github/workflows/release-dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml index dde175c..5005b2e 100644 --- a/.github/workflows/release-dev.yaml +++ b/.github/workflows/release-dev.yaml @@ -3,7 +3,7 @@ name: Release (Develop) on: push: branches: - - master + - main jobs: From 027584aca2728540ee507b58094b367bbb906e38 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 28 Mar 2021 21:27:16 +0200 Subject: [PATCH 68/98] add gh token to goreleaser --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e2bdb86..6bd28dc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,6 +75,8 @@ jobs: with: version: v0.155.0 args: --debug + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Enable experimental docker features run: | mkdir -p ~/.docker/ && \ From c4c0533bffa654ea2da897cf4ab29edf73c7f0c9 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 28 Mar 2021 21:43:54 +0200 Subject: [PATCH 69/98] add dockerhub login step --- .github/workflows/release.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6bd28dc..641a6c3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,6 +70,11 @@ jobs: uses: actions/setup-go@v2 with: go-version: 1.15.x + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build uses: goreleaser/goreleaser-action@v2 with: @@ -140,7 +145,6 @@ jobs: --variant v8 - name: Push manifests to Dockerhub run: | - echo "$DOCKER_PASS" | docker login -u $DOCKER_USER --password-stdin && \ docker manifest push containrrr/watchtower:$(echo $TAG | sed 's/^v*//') && \ docker manifest push containrrr/watchtower:latest From 8448851987661be7ee771add8e19299bdc8239a2 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 28 Mar 2021 22:05:40 +0200 Subject: [PATCH 70/98] fix tag name parsing, hopefully --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 641a6c3..e212774 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: - lint env: CGO_ENABLED: ${CGO_ENABLED:-0} - TAG: ${GITHUB_REF#refs/tags/} + TAG: ${{ github.event.release.tag_name }} steps: - name: Checkout uses: actions/checkout@v2 From b7f3e68d49f87864fdde17d74d54d489af00b11a Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 28 Mar 2021 22:15:29 +0200 Subject: [PATCH 71/98] Update release.yml --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e212774..5ad5f64 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -145,6 +145,7 @@ jobs: --variant v8 - name: Push manifests to Dockerhub run: | + echo "$DOCKER_TOKEN" | docker login -u $DOCKER_USER --password-stdin && \ docker manifest push containrrr/watchtower:$(echo $TAG | sed 's/^v*//') && \ docker manifest push containrrr/watchtower:latest From bde9aec1c0cee6ae3e86a73beced2b2cb940a15d Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 28 Mar 2021 22:28:19 +0200 Subject: [PATCH 72/98] fix docker secrets --- .github/workflows/release.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5ad5f64..7971d96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -144,8 +144,11 @@ jobs: --arch arm64 \ --variant v8 - name: Push manifests to Dockerhub + env: + DOCKER_USER: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} run: | - echo "$DOCKER_TOKEN" | docker login -u $DOCKER_USER --password-stdin && \ + docker login -u $DOCKER_USER -p $DOCKER_TOKEN && \ docker manifest push containrrr/watchtower:$(echo $TAG | sed 's/^v*//') && \ docker manifest push containrrr/watchtower:latest From 69b7480b4d7928e58768bc4717836fb25fc9f19a Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 28 Mar 2021 23:48:47 +0200 Subject: [PATCH 73/98] permanently disable cgo for production releases --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7971d96..3083abb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,7 @@ jobs: - test - lint env: - CGO_ENABLED: ${CGO_ENABLED:-0} + CGO_ENABLED: 0 TAG: ${{ github.event.release.tag_name }} steps: - name: Checkout From fac88f9cd27fb8d41540d8c55f232b98008490df Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Mon, 29 Mar 2021 18:01:05 +0200 Subject: [PATCH 74/98] add version info to goreleasers ldflags --- goreleaser.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/goreleaser.yml b/goreleaser.yml index 2bdd067..6a8ffdf 100644 --- a/goreleaser.yml +++ b/goreleaser.yml @@ -9,6 +9,9 @@ build: - 386 - arm - arm64 + ldflags: + - -s -w -X main.build={{.Version}} -X github.com/containrrr/watchtower/cmd.version={{.Version}} + - ./usemsan=-msan archives: - name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}" From da56c215db6e78a5b420125abc3732ef65dbe7dd Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Mon, 29 Mar 2021 18:19:08 +0200 Subject: [PATCH 75/98] rem vals we dont need or use from the gr config --- goreleaser.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/goreleaser.yml b/goreleaser.yml index 6a8ffdf..9b738e6 100644 --- a/goreleaser.yml +++ b/goreleaser.yml @@ -10,8 +10,7 @@ build: - arm - arm64 ldflags: - - -s -w -X main.build={{.Version}} -X github.com/containrrr/watchtower/cmd.version={{.Version}} - - ./usemsan=-msan + - -s -w -X github.com/containrrr/watchtower/cmd.version={{.Version}} archives: - name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}" From a5ffb653dfe45f8723c4c5d457c11e98818c938e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Thu, 1 Apr 2021 19:18:36 +0200 Subject: [PATCH 76/98] chore(ci): fix default branch in Dockerfiles (#875) --- build.sh | 4 ++-- dockerfiles/Dockerfile.dev-self-contained | 6 +++--- dockerfiles/Dockerfile.self-contained | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.sh b/build.sh index 363dc74..47d2b5c 100644 --- a/build.sh +++ b/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -VERSION=$(git describe) +VERSION=$(git describe --tags) echo "Building $VERSION..." -go build -o watchtower -ldflags "-X github.com/containrrr/watchtower/cmd.version=$VERSION" \ No newline at end of file +go build -o watchtower -ldflags "-X github.com/containrrr/watchtower/cmd.version=$VERSION" diff --git a/dockerfiles/Dockerfile.dev-self-contained b/dockerfiles/Dockerfile.dev-self-contained index 307ffbe..b22ef13 100644 --- a/dockerfiles/Dockerfile.dev-self-contained +++ b/dockerfiles/Dockerfile.dev-self-contained @@ -4,8 +4,8 @@ FROM golang:alpine as builder -# use version (for example "v0.3.3") or "master" -ARG WATCHTOWER_VERSION=master +# use version (for example "v0.3.3") or "main" +ARG WATCHTOWER_VERSION=main RUN apk add --no-cache \ alpine-sdk \ @@ -18,7 +18,7 @@ COPY . /watchtower RUN \ cd /watchtower && \ \ - GO111MODULE=on CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' . && \ + GO111MODULE=on CGO_ENABLED=0 GOOS=linux go build -a -ldflags "-extldflags '-static' -X github.com/containrrr/watchtower/cmd.version=$(git describe --tags)" . && \ GO111MODULE=on go test ./... -v diff --git a/dockerfiles/Dockerfile.self-contained b/dockerfiles/Dockerfile.self-contained index 64d5dc0..f24701f 100644 --- a/dockerfiles/Dockerfile.self-contained +++ b/dockerfiles/Dockerfile.self-contained @@ -4,8 +4,8 @@ FROM golang:alpine as builder -# use version (for example "v0.3.3") or "master" -ARG WATCHTOWER_VERSION=master +# use version (for example "v0.3.3") or "main" +ARG WATCHTOWER_VERSION=main RUN apk add --no-cache \ alpine-sdk \ @@ -18,7 +18,7 @@ RUN git clone --branch "${WATCHTOWER_VERSION}" https://github.com/containrrr/wat RUN \ cd watchtower && \ \ - GO111MODULE=on CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' . && \ + GO111MODULE=on CGO_ENABLED=0 GOOS=linux go build -a -ldflags "-extldflags '-static' -X github.com/containrrr/watchtower/cmd.version=$(git describe --tags)" . && \ GO111MODULE=on go test ./... -v From 3b60afe55378bdb36554e9e9dff2eed962c184a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 3 Apr 2021 00:29:09 +0200 Subject: [PATCH 77/98] docs: remove the explicit file name from edit url (#879) follow up from #846 --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index b40f794..67529aa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,7 @@ site_name: Watchtower site_url: http://containrrr.github.io/watchtower/ repo_url: https://github.com/containrrr/watchtower/ -edit_uri: edit/main/docs/index.md +edit_uri: edit/main/docs/ theme: name: 'material' palette: From b644ec6829c9137e62f26413a14ec214413e0f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 18 Apr 2021 18:11:46 +0200 Subject: [PATCH 78/98] fix(notifier): don't panic on unconfigured notifier (#869) --- pkg/notifications/notifier.go | 6 ++++++ pkg/notifications/notifier_test.go | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index b9e322e..ec313a5 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -55,6 +55,12 @@ func (n *Notifier) String() string { sb.WriteString(", ") } } + + if sb.Len() < 2 { + // No notification services are configured, return early as the separator strip is not applicable + return "none" + } + names := sb.String() // remove the last separator diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go index 5ef75a0..ecd228f 100644 --- a/pkg/notifications/notifier_test.go +++ b/pkg/notifications/notifier_test.go @@ -23,6 +23,22 @@ func TestActions(t *testing.T) { } var _ = Describe("notifications", func() { + Describe("the notifier", func() { + When("only empty notifier types are provided", func() { + + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + + err := command.ParseFlags([]string{ + "--notifications", + "shoutrrr", + }) + Expect(err).NotTo(HaveOccurred()) + notif := notifications.NewNotifier(command) + + Expect(notif.String()).To(Equal("none")) + }) + }) Describe("the slack notifier", func() { builderFn := notifications.NewSlackNotifier From bf8dec1b88b336c4e767c0983808aa2bd3e39124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 18 Apr 2021 18:30:58 +0200 Subject: [PATCH 79/98] chore(ci): run code coverage on main push (#870) * chore(ci): run code coverage on main push * merge workflows for pushes to main * add workflow dispatch for production releases Co-authored-by: Simon Aronsson --- .github/workflows/release-dev.yaml | 50 ++++++++++++++++++++++-------- .github/workflows/release.yml | 1 + 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml index 5005b2e..7928e45 100644 --- a/.github/workflows/release-dev.yaml +++ b/.github/workflows/release-dev.yaml @@ -1,23 +1,47 @@ -name: Release (Develop) +name: Push to main on: + workflow_dispatch: {} push: branches: - main jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - uses: jerray/publish-docker-action@master - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - file: dockerfiles/Dockerfile.self-contained - repository: containrrr/watchtower - tags: latest-dev + - uses: actions/checkout@v2 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.15 + - name: Build + run: go build -v ./... + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.15 + - name: Test + run: go test -v -coverprofile coverage.out -covermode atomic ./... + - name: Publish coverage + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + publish: + needs: + - build + - test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: jerray/publish-docker-action@master + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + file: dockerfiles/Dockerfile.self-contained + repository: containrrr/watchtower + tags: latest-dev diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3083abb..de72102 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,7 @@ name: Release (Production) on: + workflow_dispatch: {} release: types: - created From 62a6d31880b6cc509708d40f5c34d86bd7f1d1c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 18 Apr 2021 18:32:44 +0200 Subject: [PATCH 80/98] docs: suggest mounting localtime, not of timezone (#877) --- docs/arguments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/arguments.md b/docs/arguments.md index 01de345..70efc21 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -39,7 +39,7 @@ Environment Variable: N/A ## Time Zone Sets the time zone to be used by WatchTower's logs and the optional Cron scheduling argument (--schedule). If this environment variable is not set, Watchtower will use the default time zone: UTC. -To find out the right value, see [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), find your location and use the value in _TZ Database Name_, e.g _Europe/Rome_. The timezone can alternatively be set by volume mounting your hosts /etc/timezone file. `-v /etc/timezone:/etc/timezone:ro` +To find out the right value, see [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), find your location and use the value in _TZ Database Name_, e.g _Europe/Rome_. The timezone can alternatively be set by volume mounting your hosts /etc/localtime file. `-v /etc/localtime:/etc/localtime:ro` ``` Argument: N/A From 6a9d985ce76844cf16ca0d586d552e7d91afbb74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 18 Apr 2021 18:34:38 +0200 Subject: [PATCH 81/98] feat(log): use short image/container IDs in logs (#888) --- pkg/container/client.go | 25 ++++++++++++--------- pkg/container/util.go | 23 +++++++++++++++++++ pkg/container/util_test.go | 46 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 pkg/container/util.go create mode 100644 pkg/container/util_test.go diff --git a/pkg/container/client.go b/pkg/container/client.go index 635aa3e..b125ed6 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -147,8 +147,10 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err signal = defaultStopSignal } + shortID := ShortID(c.ID()) + if c.IsRunning() { - log.Infof("Stopping %s (%s) with %s", c.Name(), c.ID(), signal) + log.Infof("Stopping %s (%s) with %s", c.Name(), shortID, signal) if err := client.api.ContainerKill(bg, c.ID(), signal); err != nil { return err } @@ -158,9 +160,9 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err _ = client.waitForStopOrTimeout(c, timeout) if c.containerInfo.HostConfig.AutoRemove { - log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", c.ID()) + log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", shortID) } else { - log.Debugf("Removing container %s", c.ID()) + log.Debugf("Removing container %s", shortID) if err := client.api.ContainerRemove(bg, c.ID(), types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.removeVolumes}); err != nil { return err @@ -169,7 +171,7 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err // Wait for container to be removed. In this case an error is a good thing if err := client.waitForStopOrTimeout(c, timeout); err == nil { - return fmt.Errorf("container %s (%s) could not be removed", c.Name(), c.ID()) + return fmt.Errorf("container %s (%s) could not be removed", c.Name(), shortID) } return nil @@ -229,7 +231,7 @@ func (client dockerClient) StartContainer(c Container) (string, error) { func (client dockerClient) doStartContainer(bg context.Context, c Container, creation container.ContainerCreateCreatedBody) error { name := c.Name() - log.Debugf("Starting container %s (%s)", name, creation.ID) + log.Debugf("Starting container %s (%s)", name, ShortID(creation.ID)) err := client.api.ContainerStart(bg, creation.ID, types.ContainerStartOptions{}) if err != nil { return err @@ -239,7 +241,7 @@ func (client dockerClient) doStartContainer(bg context.Context, c Container, cre func (client dockerClient) RenameContainer(c Container, newName string) error { bg := context.Background() - log.Debugf("Renaming container %s (%s) to %s", c.Name(), c.ID(), newName) + log.Debugf("Renaming container %s (%s) to %s", c.Name(), ShortID(c.ID()), newName) return client.api.ContainerRename(bg, c.ID(), newName) } @@ -269,7 +271,7 @@ func (client dockerClient) HasNewImage(ctx context.Context, container Container) return false, nil } - log.Infof("Found new %s image (%s)", imageName, newImageInfo.ID) + log.Infof("Found new %s image (%s)", imageName, ShortID(newImageInfo.ID)) return true, nil } @@ -284,13 +286,13 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e log.WithFields(fields).Debugf("Trying to load authentication credentials.") opts, err := registry.GetPullOptions(imageName) - if opts.RegistryAuth != "" { - log.Debug("Credentials loaded") - } if err != nil { log.Debugf("Error loading authentication credentials %s", err) return err } + if opts.RegistryAuth != "" { + log.Debug("Credentials loaded") + } log.WithFields(fields).Debugf("Checking if pull is needed") @@ -326,7 +328,7 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e } func (client dockerClient) RemoveImageByID(id string) error { - log.Infof("Removing image %s", id) + log.Infof("Removing image %s", ShortID(id)) _, err := client.api.ImageRemove( context.Background(), @@ -404,6 +406,7 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e for { execInspect, err := client.api.ContainerExecInspect(ctx, ID) + //goland:noinspection GoNilness log.WithFields(log.Fields{ "exit-code": execInspect.ExitCode, "exec-id": execInspect.ExecID, diff --git a/pkg/container/util.go b/pkg/container/util.go new file mode 100644 index 0000000..261316f --- /dev/null +++ b/pkg/container/util.go @@ -0,0 +1,23 @@ +package container + +import "strings" + +// ShortID returns the 12-character (hex) short version of an image ID hash, removing any "sha256:" prefix if present +func ShortID(imageID string) (short string) { + prefixSep := strings.IndexRune(imageID, ':') + offset := 0 + length := 12 + if prefixSep >= 0 { + if imageID[0:prefixSep] == "sha256" { + offset = prefixSep + 1 + } else { + length += prefixSep + 1 + } + } + + if len(imageID) >= offset+length { + return imageID[offset : offset+length] + } + + return imageID +} diff --git a/pkg/container/util_test.go b/pkg/container/util_test.go new file mode 100644 index 0000000..8cb0328 --- /dev/null +++ b/pkg/container/util_test.go @@ -0,0 +1,46 @@ +package container_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + . "github.com/containrrr/watchtower/pkg/container" +) + +var _ = Describe("container utils", func() { + Describe("ShortID", func() { + When("given a normal image ID", func() { + When("it contains a sha256 prefix", func() { + It("should return that ID in short version", func() { + actual := ShortID("sha256:0123456789abcd00000000001111111111222222222233333333334444444444") + Expect(actual).To(Equal("0123456789ab")) + }) + }) + When("it doesn't contain a prefix", func() { + It("should return that ID in short version", func() { + actual := ShortID("0123456789abcd00000000001111111111222222222233333333334444444444") + Expect(actual).To(Equal("0123456789ab")) + }) + }) + }) + When("given a short image ID", func() { + When("it contains no prefix", func() { + It("should return the same string", func() { + Expect(ShortID("0123456789ab")).To(Equal("0123456789ab")) + }) + }) + When("it contains a the sha256 prefix", func() { + It("should return the ID without the prefix", func() { + Expect(ShortID("sha256:0123456789ab")).To(Equal("0123456789ab")) + }) + }) + }) + When("given an ID with an unknown prefix", func() { + It("should return a short version of that ID including the prefix", func() { + Expect(ShortID("md5:0123456789ab")).To(Equal("md5:0123456789ab")) + Expect(ShortID("md5:0123456789abcdefg")).To(Equal("md5:0123456789ab")) + Expect(ShortID("md5:01")).To(Equal("md5:01")) + }) + }) + }) +}) From 4142f7966a6703ed3221f1ebffb6e03fa4e18747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 18 Apr 2021 18:35:15 +0200 Subject: [PATCH 82/98] fix: move notify URL to trace log (#907) --- pkg/notifications/notifier.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index ec313a5..c4e962f 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -103,7 +103,7 @@ func (n *Notifier) getNotificationTypes(cmd *cobra.Command, levels []log.Level, log.Fatal("failed to create notification config:", err) } - println(shoutrrrURL) + log.WithField("URL", shoutrrrURL).Trace("created Shoutrrr URL from legacy notifier") notifier := newShoutrrrNotifierFromURL( cmd, From 3de202a965faac19c0388ba2ec82d2980411d1d8 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sun, 18 Apr 2021 18:37:35 +0200 Subject: [PATCH 83/98] fix depends on behavior and simplify some of its logic (#908) * fix depends on behavior and simplify some of its logic * fix comments --- cmd/root.go | 24 ++++++++++++++++++++++-- internal/actions/check.go | 34 ++++++++++++++++++++++++---------- internal/actions/update.go | 32 ++++++++++++++++++-------------- pkg/container/container.go | 6 +++--- scripts/dependency-test.sh | 16 ++++++++++++++++ 5 files changed, 83 insertions(+), 29 deletions(-) create mode 100755 scripts/dependency-test.sh diff --git a/cmd/root.go b/cmd/root.go index 3a597d0..df14ab6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -154,9 +154,18 @@ func Run(c *cobra.Command, names []string) { runOnce, _ := c.PersistentFlags().GetBool("run-once") enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update") enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics") - apiToken, _ := c.PersistentFlags().GetString("http-api-token") + if rollingRestart && monitorOnly { + log.Fatal("Rolling restarts is not compatible with the global monitor only flag") + } + + awaitDockerClient() + + if err := actions.CheckForSanity(client, filter, rollingRestart); err != nil { + logNotifyExit(err) + } + if runOnce { writeStartupMessage(c, time.Time{}, filterDesc) runUpdatesWithNotifications(filter) @@ -166,7 +175,7 @@ func Run(c *cobra.Command, names []string) { } if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup, scope); err != nil { - log.Fatal(err) + logNotifyExit(err) } httpAPI := api.New(apiToken) @@ -192,6 +201,17 @@ func Run(c *cobra.Command, names []string) { os.Exit(1) } +func logNotifyExit(err error) { + log.Error(err) + notifier.Close() + os.Exit(1) +} + +func awaitDockerClient() { + log.Debug("Sleeping for a second to ensure the docker api client has been properly initialized.") + time.Sleep(1 * time.Second) +} + func formatDuration(d time.Duration) string { sb := strings.Builder{} diff --git a/internal/actions/check.go b/internal/actions/check.go index 87133fc..436931f 100644 --- a/internal/actions/check.go +++ b/internal/actions/check.go @@ -2,28 +2,47 @@ package actions import ( "fmt" + "github.com/containrrr/watchtower/pkg/types" "sort" "time" "github.com/containrrr/watchtower/pkg/filters" "github.com/containrrr/watchtower/pkg/sorter" - "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus" "github.com/containrrr/watchtower/pkg/container" ) +// CheckForSanity makes sure everything is sane before starting +func CheckForSanity(client container.Client, filter types.Filter, rollingRestarts bool) error { + log.Debug("Making sure everything is sane before starting") + + if rollingRestarts { + containers, err := client.ListContainers(filter) + if err != nil { + return err + } + for _, c := range containers { + if len(c.Links()) > 0 { + return fmt.Errorf( + "%q is depending on at least one other container. This is not compatible with rolling restarts", + c.Name(), + ) + } + } + } + return nil +} + // CheckForMultipleWatchtowerInstances will ensure that there are not multiple instances of the // watchtower running simultaneously. If multiple watchtower containers are detected, this function // 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 { - awaitDockerClient() containers, err := client.ListContainers(filters.FilterByScope(scope, filters.WatchtowerContainersFilter)) if err != nil { - log.Fatal(err) return err } @@ -45,14 +64,14 @@ func cleanupExcessWatchtowers(containers []container.Container, client container for _, c := range allContainersExceptLast { if err := client.StopContainer(c, 10*time.Minute); err != nil { // logging the original here as we're just returning a count - logrus.WithError(err).Error("Could not stop a previous watchtower instance.") + log.WithError(err).Error("Could not stop a previous watchtower instance.") stopErrors++ continue } if cleanup { if err := client.RemoveImageByID(c.ImageID()); err != nil { - logrus.WithError(err).Warning("Could not cleanup watchtower images, possibly because of other watchtowers instances in other scopes.") + log.WithError(err).Warning("Could not cleanup watchtower images, possibly because of other watchtowers instances in other scopes.") } } } @@ -63,8 +82,3 @@ func cleanupExcessWatchtowers(containers []container.Container, client container return nil } - -func awaitDockerClient() { - log.Debug("Sleeping for a second to ensure the docker api client has been properly initialized.") - time.Sleep(1 * time.Second) -} diff --git a/internal/actions/update.go b/internal/actions/update.go index 9320d6a..06bb345 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -50,6 +50,7 @@ func Update(client container.Client, params types.UpdateParams) (*metrics2.Metri } containers, err = sorter.SortByDependencies(containers) + metric.Scanned = len(containers) if err != nil { return nil, err @@ -57,11 +58,11 @@ func Update(client container.Client, params types.UpdateParams) (*metrics2.Metri checkDependencies(containers) - containersToUpdate := []container.Container{} + var containersToUpdate []container.Container if !params.MonitorOnly { - for i := len(containers) - 1; i >= 0; i-- { - if !containers[i].IsMonitorOnly() { - containersToUpdate = append(containersToUpdate, containers[i]) + for _, c := range containers { + if !c.IsMonitorOnly() { + containersToUpdate = append(containersToUpdate, c) } } } @@ -86,7 +87,7 @@ func performRollingRestart(containers []container.Container, client container.Cl failed := 0 for i := len(containers) - 1; i >= 0; i-- { - if containers[i].Stale { + if containers[i].ToRestart() { if err := stopStaleContainer(containers[i], client, params); err != nil { failed++ } @@ -119,7 +120,7 @@ func stopStaleContainer(container container.Container, client container.Client, return nil } - if !container.Stale { + if !container.ToRestart() { return nil } if params.LifecycleHooks { @@ -143,7 +144,7 @@ func restartContainersInSortedOrder(containers []container.Container, client con failed := 0 for _, c := range containers { - if !c.Stale { + if !c.ToRestart() { continue } if err := restartStaleContainer(c, client, params); err != nil { @@ -183,7 +184,7 @@ func restartStaleContainer(container container.Container, client container.Clien if newContainerID, err := client.StartContainer(container); err != nil { log.Error(err) return err - } else if container.Stale && params.LifecycleHooks { + } else if container.ToRestart() && params.LifecycleHooks { lifecycle.ExecutePostUpdateCommand(client, newContainerID) } } @@ -192,16 +193,19 @@ func restartStaleContainer(container container.Container, client container.Clien func checkDependencies(containers []container.Container) { - for i, parent := range containers { - if parent.ToRestart() { + for _, c := range containers { + if c.ToRestart() { continue } LinkLoop: - for _, linkName := range parent.Links() { - for _, child := range containers { - if child.Name() == linkName && child.ToRestart() { - containers[i].Linked = true + for _, linkName := range c.Links() { + for _, candidate := range containers { + if candidate.Name() != linkName { + continue + } + if candidate.ToRestart() { + c.LinkedToRestarting = true break LinkLoop } } diff --git a/pkg/container/container.go b/pkg/container/container.go index 8a9d39e..7631b5e 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -22,8 +22,8 @@ func NewContainer(containerInfo *types.ContainerJSON, imageInfo *types.ImageInsp // Container represents a running Docker container. type Container struct { - Linked bool - Stale bool + LinkedToRestarting bool + Stale bool containerInfo *types.ContainerJSON imageInfo *types.ImageInspect @@ -142,7 +142,7 @@ func (c Container) Links() []string { // ToRestart return whether the container should be restarted, either because // is stale or linked to another stale container. func (c Container) ToRestart() bool { - return c.Stale || c.Linked + return c.Stale || c.LinkedToRestarting } // IsWatchtower returns a boolean flag indicating whether or not the current diff --git a/scripts/dependency-test.sh b/scripts/dependency-test.sh new file mode 100755 index 0000000..0da0110 --- /dev/null +++ b/scripts/dependency-test.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Simulates a container that will always be updated, checking whether it shuts down it's dependencies correctly. + +docker rm -f parent || true +docker rm -f depending || true + +CHANGE=redis:latest +KEEP=tutum/hello-world + +docker tag tutum/hello-world:latest redis:latest + +docker run -d --name parent $CHANGE +docker run -d --name depending --link parent $KEEP + +go run . --run-once --debug $@ From 29f5c4b254a686d9198277a78f5066666240f0e0 Mon Sep 17 00:00:00 2001 From: ksurl Date: Tue, 20 Apr 2021 05:06:48 -0700 Subject: [PATCH 84/98] add ghcr (#850) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add ghcr * Update .github/workflows/release-dev.yaml Co-authored-by: nils måsén * Update .github/workflows/release.yml Co-authored-by: nils måsén * Apply suggestions from code review I might be dyslectic * Update .github/workflows/release.yml Co-authored-by: nils måsén * Update .github/workflows/release.yml Co-authored-by: nils måsén Co-authored-by: nils måsén Co-authored-by: Simon Aronsson --- .github/workflows/release-dev.yaml | 12 ++++++- .github/workflows/release.yml | 53 +++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml index 7928e45..5c71258 100644 --- a/.github/workflows/release-dev.yaml +++ b/.github/workflows/release-dev.yaml @@ -38,10 +38,20 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: jerray/publish-docker-action@master + - name: Publish to Docker Hub + uses: jerray/publish-docker-action@master with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} file: dockerfiles/Dockerfile.self-contained repository: containrrr/watchtower tags: latest-dev + - name: Publish to GHCR + uses: jerray/publish-docker-action@master + with: + username: ${{ secrets.BOT_USERNAME }} + password: ${{ secrets.BOT_GHCR_PAT }} + file: dockerfiles/Dockerfile.self-contained + registry: ghcr.io + repository: containrrr/watchtower + tags: latest-dev diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de72102..39e43fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -95,27 +95,36 @@ jobs: containrrr/watchtower:amd64-$DH_TAG \ containrrr/watchtower:i386-$DH_TAG \ containrrr/watchtower:armhf-$DH_TAG \ - containrrr/watchtower:arm64v8-$DH_TAG + containrrr/watchtower:arm64v8-$DH_TAG \ + ghcr.io/containrrr/watchtower:$DH_TAG \ + ghcr.io/containrrr/watchtower:amd64-$DH_TAG \ + ghcr.io/containrrr/watchtower:i386-$DH_TAG \ + ghcr.io/containrrr/watchtower:armhf-$DH_TAG \ + ghcr.io/containrrr/watchtower:arm64v8-$DH_TAG - name: Annotate manifest for version run: | + for REPO in '' ghrc.io/ ; do + docker manifest annotate \ - containrrr/watchtower:$(echo $TAG | sed 's/^v*//') \ - containrrr/watchtower:i386-$(echo $TAG | sed 's/^v*//') \ + ${REPO}containrrr/watchtower:$(echo $TAG | sed 's/^v*//') \ + ${REPO}containrrr/watchtower:i386-$(echo $TAG | sed 's/^v*//') \ --os linux \ --arch 386 docker manifest annotate \ - containrrr/watchtower:$(echo $TAG | sed 's/^v*//') \ - containrrr/watchtower:armhf-$(echo $TAG | sed 's/^v*//') \ + ${REPO}containrrr/watchtower:$(echo $TAG | sed 's/^v*//') \ + ${REPO}containrrr/watchtower:armhf-$(echo $TAG | sed 's/^v*//') \ --os linux \ --arch arm docker manifest annotate \ - containrrr/watchtower:$(echo $TAG | sed 's/^v*//') \ - containrrr/watchtower:arm64v8-$(echo $TAG | sed 's/^v*//') \ + ${REPO}containrrr/watchtower:$(echo $TAG | sed 's/^v*//') \ + ${REPO}containrrr/watchtower:arm64v8-$(echo $TAG | sed 's/^v*//') \ --os linux \ --arch arm64 \ --variant v8 + + done - name: Create manifest for latest run: | docker manifest create \ @@ -123,27 +132,36 @@ jobs: containrrr/watchtower:amd64-latest \ containrrr/watchtower:i386-latest \ containrrr/watchtower:armhf-latest \ - containrrr/watchtower:arm64v8-latest + containrrr/watchtower:arm64v8-latest \ + ghcr.io/containrrr/watchtower:latest \ + ghcr.io/containrrr/watchtower:amd64-latest \ + ghcr.io/containrrr/watchtower:i386-latest \ + ghcr.io/containrrr/watchtower:armhf-latest \ + ghcr.io/containrrr/watchtower:arm64v8-latest - name: Annotate manifest for latest run: | + for REPO in '' ghrc.io/ ; do + docker manifest annotate \ - containrrr/watchtower:latest \ - containrrr/watchtower:i386-latest \ + ${REPO}containrrr/watchtower:latest \ + ${REPO}containrrr/watchtower:i386-latest \ --os linux \ --arch 386 docker manifest annotate \ - containrrr/watchtower:latest \ - containrrr/watchtower:armhf-latest \ + ${REPO}containrrr/watchtower:latest \ + ${REPO}containrrr/watchtower:armhf-latest \ --os linux \ --arch arm - + docker manifest annotate \ - containrrr/watchtower:latest \ - containrrr/watchtower:arm64v8-latest \ + ${REPO}containrrr/watchtower:latest \ + ${REPO}containrrr/watchtower:arm64v8-latest \ --os linux \ --arch arm64 \ --variant v8 + + done - name: Push manifests to Dockerhub env: DOCKER_USER: ${{ secrets.DOCKERHUB_USERNAME }} @@ -152,6 +170,11 @@ jobs: docker login -u $DOCKER_USER -p $DOCKER_TOKEN && \ docker manifest push containrrr/watchtower:$(echo $TAG | sed 's/^v*//') && \ docker manifest push containrrr/watchtower:latest + - name: Push manifests to GitHub Container Registry + run: | + echo "$BOT_GHCR_PAT" | docker login -u $BOT_USERNAME --password-stdin && \ + docker manifest push ghcr.io/containrrr/watchtower:$(echo $TAG | sed 's/^v*//') && \ + docker manifest push ghcr.io/containrrr/watchtower:latest publish-docs: name: Publish Docs From 6a7e5a959b6e32587c02dea68c1efb807b02b9b2 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Wed, 21 Apr 2021 10:01:33 +0200 Subject: [PATCH 85/98] Create SECURITY.md --- SECURITY.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..550f904 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Supported Versions + +Security updates will always only be applied to the latest version of Watchtower. +As the software by default is set to auto-update if you use the `latest` tag, you will get these security updates automatically as soon as they are released. + +## Reporting a Vulnerability + +Critical vulnerabilities that might open up for external attacks are best reported directly either to simme@arcticbit.se or nils@piksel.se. +We'll always try to get back to you as swiftly as possible, but keep in mind that since this is a community project, we can't really leave any guarantees about the speed. + +Non-critical vulnerabilities may be reported as regular GitHub issues. From 028f19ac47c1ebccd3165ea2f2d0730413adf251 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Wed, 21 Apr 2021 10:03:05 +0200 Subject: [PATCH 86/98] Move token logs to trace --- pkg/api/api.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 9afded5..b2279e1 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -26,8 +26,8 @@ func New(token string) *API { func (api *API) RequireToken(fn http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", api.Token) { - log.Errorf("Invalid token \"%s\"", r.Header.Get("Authorization")) - log.Debugf("Expected token to be \"%s\"", api.Token) + log.Tracef("Invalid token \"%s\"", r.Header.Get("Authorization")) + log.Tracef("Expected token to be \"%s\"", api.Token) return } log.Debug("Valid token found.") From 058e3c6d4804f271c727dfd27e7aef6a47ba9d66 Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Wed, 21 Apr 2021 10:17:41 +0200 Subject: [PATCH 87/98] Update bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1d4b1f6..53e1a53 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,27 +6,37 @@ labels: 'Priority: Medium, Status: Available, Type: Bug' assignees: '' --- + **Describe the bug** -A clear and concise description of what the bug is. + **To Reproduce** + **Expected behavior** -A clear and concise description of what you expected to happen. + **Screenshots** + **Environment** +
Logs from running watchtower with the --debug option @@ -38,4 +48,6 @@ If applicable, add screenshots to help explain your problem.
**Additional context** -Add any other context about the problem here. + From 23572add7425e30a3d3c711143e87a45fcd99d0d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 22 Apr 2021 15:13:42 +0200 Subject: [PATCH 88/98] docs: add ksurl as a contributor (#917) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 3 ++- README.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 1973f66..a8aa234 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -692,7 +692,8 @@ "avatar_url": "https://avatars1.githubusercontent.com/u/1371562?v=4", "profile": "https://github.com/ksurl", "contributions": [ - "doc" + "doc", + "code" ] }, { diff --git a/README.md b/README.md index 4c2b805..0d9d512 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
David H.

💻
Chander Ganesan

📖
yrien30

💻 -
ksurl

📖 +
ksurl

📖 💻
rg9400

💻
Turtle Kalus

💻
Srihari Thalla

📖 From b4cf17d33fc361988201a503d3e32a902c5d29da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Fri, 23 Apr 2021 16:34:21 +0200 Subject: [PATCH 89/98] feat: make head pull failure warning toggleable (#912) * feat: make head pull failure warning toggleable * expect prometheus tests to go through EVENTUALLY * wait for queue to be empty before checking test conditions * clean up new head failure toggle * fixup! clean up new head failure toggle * test: add registry tests * test: add warn on head failure tests * fix client interface and make tests hit more lines * make all tests use NewClient instead of creating a struct pointer * fix lint issues Co-authored-by: Simon Aronsson --- cmd/root.go | 2 + coverage.out | 620 ---------------------------- internal/actions/mocks/client.go | 5 + internal/flags/flags.go | 6 + pkg/api/metrics/metrics_test.go | 17 +- pkg/container/client.go | 28 +- pkg/container/container_test.go | 36 +- pkg/metrics/metrics.go | 5 + pkg/registry/digest/digest.go | 6 +- pkg/registry/registry_suite_test.go | 13 + pkg/registry/registry_test.go | 45 ++ pkg/registry/trust_test.go | 6 - scripts/codecov.sh | 6 + 13 files changed, 148 insertions(+), 647 deletions(-) delete mode 100644 coverage.out create mode 100644 pkg/registry/registry_suite_test.go create mode 100644 pkg/registry/registry_test.go create mode 100755 scripts/codecov.sh diff --git a/cmd/root.go b/cmd/root.go index df14ab6..6f84727 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -132,6 +132,7 @@ func PreRun(cmd *cobra.Command, _ []string) { includeRestarting, _ := f.GetBool("include-restarting") reviveStopped, _ := f.GetBool("revive-stopped") removeVolumes, _ := f.GetBool("remove-volumes") + warnOnHeadPullFailed, _ := f.GetString("warn-on-head-failure") if monitorOnly && noPull { log.Warn("Using `WATCHTOWER_NO_PULL` and `WATCHTOWER_MONITOR_ONLY` simultaneously might lead to no action being taken at all. If this is intentional, you may safely ignore this message.") @@ -143,6 +144,7 @@ func PreRun(cmd *cobra.Command, _ []string) { reviveStopped, removeVolumes, includeRestarting, + warnOnHeadPullFailed, ) notifier = notifications.NewNotifier(cmd) diff --git a/coverage.out b/coverage.out deleted file mode 100644 index 40470d1..0000000 --- a/coverage.out +++ /dev/null @@ -1,620 +0,0 @@ -mode: set -github.com/containrrr/watchtower/internal/util/rand_name.go:8.24,10.19 2 0 -github.com/containrrr/watchtower/internal/util/rand_name.go:14.2,14.18 1 0 -github.com/containrrr/watchtower/internal/util/rand_name.go:10.19,12.3 1 0 -github.com/containrrr/watchtower/internal/util/util.go:4.39,5.24 1 1 -github.com/containrrr/watchtower/internal/util/util.go:9.2,9.20 1 1 -github.com/containrrr/watchtower/internal/util/util.go:15.2,15.13 1 1 -github.com/containrrr/watchtower/internal/util/util.go:5.24,7.3 1 1 -github.com/containrrr/watchtower/internal/util/util.go:9.20,10.21 1 1 -github.com/containrrr/watchtower/internal/util/util.go:10.21,12.4 1 1 -github.com/containrrr/watchtower/internal/util/util.go:19.46,22.24 2 1 -github.com/containrrr/watchtower/internal/util/util.go:37.2,37.10 1 1 -github.com/containrrr/watchtower/internal/util/util.go:22.24,25.25 2 1 -github.com/containrrr/watchtower/internal/util/util.go:32.3,32.13 1 1 -github.com/containrrr/watchtower/internal/util/util.go:25.25,26.16 1 1 -github.com/containrrr/watchtower/internal/util/util.go:26.16,28.10 2 1 -github.com/containrrr/watchtower/internal/util/util.go:32.13,34.4 1 1 -github.com/containrrr/watchtower/internal/util/util.go:41.68,44.25 2 1 -github.com/containrrr/watchtower/internal/util/util.go:54.2,54.10 1 1 -github.com/containrrr/watchtower/internal/util/util.go:44.25,45.27 1 1 -github.com/containrrr/watchtower/internal/util/util.go:45.27,46.16 1 1 -github.com/containrrr/watchtower/internal/util/util.go:46.16,48.5 1 1 -github.com/containrrr/watchtower/internal/util/util.go:49.9,51.4 1 1 -github.com/containrrr/watchtower/internal/util/util.go:58.72,61.25 2 1 -github.com/containrrr/watchtower/internal/util/util.go:67.2,67.10 1 1 -github.com/containrrr/watchtower/internal/util/util.go:61.25,62.27 1 1 -github.com/containrrr/watchtower/internal/util/util.go:62.27,64.4 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:6.63,6.90 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:9.43,9.58 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:12.66,13.21 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:17.2,17.44 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:13.21,15.3 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:17.44,18.30 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:23.3,23.15 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:18.30,19.52 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:19.52,21.5 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:28.56,29.44 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:29.44,33.10 2 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:37.3,37.23 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:33.10,35.4 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:42.58,43.44 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:43.44,45.26 2 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:50.3,50.23 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:45.26,48.4 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:55.64,56.17 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:60.2,60.44 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:56.17,58.3 1 0 -github.com/containrrr/watchtower/pkg/filters/filters.go:60.44,62.36 2 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:66.3,66.15 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:62.36,64.4 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:71.75,74.17 3 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:79.2,79.17 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:84.2,85.15 2 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:74.17,78.3 1 1 -github.com/containrrr/watchtower/pkg/filters/filters.go:79.17,83.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:20.50,25.2 4 1 -github.com/containrrr/watchtower/internal/flags/flags.go:28.50,154.2 22 0 -github.com/containrrr/watchtower/internal/flags/flags.go:157.56,299.2 24 1 -github.com/containrrr/watchtower/internal/flags/flags.go:302.20,313.2 10 1 -github.com/containrrr/watchtower/internal/flags/flags.go:317.42,325.53 6 1 -github.com/containrrr/watchtower/internal/flags/flags.go:328.2,328.55 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:331.2,331.63 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:334.2,334.57 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:337.2,337.63 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:340.2,340.67 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:343.2,343.12 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:325.53,327.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:328.55,330.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:331.63,333.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:334.57,336.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:337.63,339.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:340.67,342.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:347.70,356.57 7 0 -github.com/containrrr/watchtower/internal/flags/flags.go:359.2,359.62 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:362.2,362.66 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:365.2,365.66 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:369.2,369.49 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:356.57,358.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:359.62,361.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:362.66,364.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:365.66,367.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:372.49,373.40 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:376.2,377.16 2 1 -github.com/containrrr/watchtower/internal/flags/flags.go:380.2,380.12 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:373.40,375.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:377.16,379.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:383.48,384.9 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:387.2,387.12 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:384.9,386.3 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:392.50,401.33 3 1 -github.com/containrrr/watchtower/internal/flags/flags.go:401.33,403.3 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:407.61,409.16 2 1 -github.com/containrrr/watchtower/internal/flags/flags.go:412.2,412.34 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:409.16,411.3 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:412.34,414.17 2 1 -github.com/containrrr/watchtower/internal/flags/flags.go:417.3,418.17 2 1 -github.com/containrrr/watchtower/internal/flags/flags.go:414.17,416.4 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:418.17,420.4 1 0 -github.com/containrrr/watchtower/internal/flags/flags.go:424.28,426.24 2 1 -github.com/containrrr/watchtower/internal/flags/flags.go:429.2,429.13 1 1 -github.com/containrrr/watchtower/internal/flags/flags.go:426.24,428.3 1 1 -github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:9.60,12.16 3 1 -github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:15.2,19.28 4 1 -github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:12.16,14.3 1 0 -github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:23.57,25.16 2 1 -github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:29.2,29.67 1 1 -github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:33.2,33.16 1 1 -github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:36.2,36.22 1 1 -github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:25.16,27.3 1 0 -github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:29.67,31.3 1 1 -github.com/containrrr/watchtower/pkg/registry/helpers/helpers.go:33.16,35.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:16.46,23.16 5 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:27.2,31.16 3 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:34.2,34.26 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:53.2,53.10 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:23.16,25.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:31.16,33.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:34.26,36.12 2 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:50.3,50.32 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:37.18,38.47 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:39.18,40.47 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:41.20,42.49 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:43.19,44.48 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:45.21,46.50 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:47.11,48.49 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:57.40,58.28 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:58.28,60.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:64.39,65.28 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:65.28,67.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:71.28,72.28 1 0 -github.com/containrrr/watchtower/pkg/notifications/notifier.go:72.28,74.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:36.86,41.16 4 0 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:45.2,59.10 4 0 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:41.16,43.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:62.49,63.30 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:74.2,74.16 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:63.30,66.28 2 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:66.28,67.18 1 0 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:67.18,70.5 1 0 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:77.74,79.59 2 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:83.2,83.22 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:79.59,81.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:86.66,89.2 2 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:91.52,92.22 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:92.22,94.3 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:97.51,98.45 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:102.2,103.17 2 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:98.45,100.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:106.40,113.2 3 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:115.53,117.2 1 0 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:119.61,120.22 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:126.2,126.12 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:120.22,122.3 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:122.8,125.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:129.63,144.35 5 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:152.2,152.16 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:160.2,160.35 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:164.2,164.12 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:144.35,146.3 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:152.16,154.3 1 1 -github.com/containrrr/watchtower/pkg/notifications/shoutrrr.go:160.35,162.3 1 1 -github.com/containrrr/watchtower/pkg/notifications/slack.go:18.83,40.2 9 0 -github.com/containrrr/watchtower/pkg/notifications/slack.go:42.50,42.51 0 0 -github.com/containrrr/watchtower/pkg/notifications/slack.go:44.49,44.50 0 0 -github.com/containrrr/watchtower/pkg/notifications/slack.go:46.38,46.39 0 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:33.110,35.16 2 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:38.2,39.44 2 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:42.2,42.42 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:49.2,49.14 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:56.2,56.36 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:59.2,59.26 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:64.2,65.16 2 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:68.2,69.16 2 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:72.2,73.16 2 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:76.2,76.17 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:35.16,37.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:39.44,41.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:42.42,45.43 3 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:45.43,47.4 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:49.14,50.39 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:50.39,51.35 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:51.35,53.5 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:56.36,58.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:59.26,60.37 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:60.37,62.4 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:65.16,67.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:69.16,71.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/smtp.go:73.16,75.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/util.go:7.42,13.26 5 0 -github.com/containrrr/watchtower/pkg/notifications/util.go:23.2,23.13 1 0 -github.com/containrrr/watchtower/pkg/notifications/util.go:13.26,15.19 2 0 -github.com/containrrr/watchtower/pkg/notifications/util.go:15.19,18.4 2 0 -github.com/containrrr/watchtower/pkg/notifications/util.go:18.9,18.26 1 0 -github.com/containrrr/watchtower/pkg/notifications/util.go:18.26,20.4 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:36.83,65.2 13 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:67.71,70.24 2 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:75.2,75.48 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:78.2,79.32 2 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:84.2,96.27 11 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:100.2,102.50 2 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:106.2,106.24 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:70.24,72.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:72.8,74.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:75.48,77.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:79.32,82.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:96.27,98.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:102.50,104.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:109.63,112.12 2 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:112.12,113.18 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:117.3,118.19 2 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:121.3,122.17 2 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:113.18,115.4 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:118.19,120.4 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:122.17,125.4 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:129.49,130.22 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:130.22,132.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:135.48,136.45 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:140.2,141.17 2 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:136.45,138.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:144.50,146.2 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:148.58,149.22 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:154.2,154.12 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:149.22,151.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:151.8,153.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/email.go:157.38,157.39 0 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:27.84,31.24 3 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:39.2,40.26 2 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:44.2,55.10 4 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:31.24,33.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:33.8,33.99 1 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:33.99,35.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:35.8,35.52 1 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:35.52,37.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:40.26,42.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:58.51,58.52 0 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:60.50,60.51 0 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:62.39,62.40 0 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:64.51,66.2 1 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:68.46,70.34 2 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:73.2,73.50 1 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:70.34,72.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:76.59,78.12 1 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:110.2,110.12 1 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:78.12,84.17 2 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:90.3,99.17 4 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:103.3,105.54 2 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:84.17,87.4 2 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:99.17,102.4 2 0 -github.com/containrrr/watchtower/pkg/notifications/gotify.go:105.54,107.4 1 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:25.87,30.26 3 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:34.2,43.10 4 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:30.26,32.3 1 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:46.52,46.53 0 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:48.51,48.52 0 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:50.40,50.41 0 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:52.52,54.2 1 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:56.60,60.12 2 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:111.2,111.12 1 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:60.12,68.57 2 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:86.3,87.17 2 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:92.3,93.17 2 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:97.3,99.53 2 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:68.57,75.33 3 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:83.4,83.56 1 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:75.33,81.5 2 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:87.17,90.4 2 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:93.17,95.4 1 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:99.53,101.24 2 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:101.24,103.19 2 0 -github.com/containrrr/watchtower/pkg/notifications/msteams.go:103.19,106.6 2 0 -github.com/containrrr/watchtower/pkg/registry/registry.go:9.71,12.16 3 0 -github.com/containrrr/watchtower/pkg/registry/registry.go:16.2,16.16 1 0 -github.com/containrrr/watchtower/pkg/registry/registry.go:19.2,24.8 2 0 -github.com/containrrr/watchtower/pkg/registry/registry.go:12.16,14.3 1 0 -github.com/containrrr/watchtower/pkg/registry/registry.go:16.16,18.3 1 0 -github.com/containrrr/watchtower/pkg/registry/registry.go:30.43,33.2 2 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:20.46,22.16 2 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:25.2,25.18 1 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:22.16,24.3 1 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:31.49,34.38 3 1 -github.com/containrrr/watchtower/pkg/registry/trust.go:43.2,43.93 1 1 -github.com/containrrr/watchtower/pkg/registry/trust.go:34.38,42.3 4 1 -github.com/containrrr/watchtower/pkg/registry/trust.go:50.52,52.16 2 1 -github.com/containrrr/watchtower/pkg/registry/trust.go:56.2,57.21 2 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:60.2,61.16 2 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:65.2,68.34 3 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:72.2,74.25 3 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:52.16,55.3 2 1 -github.com/containrrr/watchtower/pkg/registry/trust.go:57.21,59.3 1 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:61.16,64.3 2 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:68.34,71.3 2 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:78.53,81.16 2 1 -github.com/containrrr/watchtower/pkg/registry/trust.go:85.2,86.22 2 1 -github.com/containrrr/watchtower/pkg/registry/trust.go:81.16,83.3 1 1 -github.com/containrrr/watchtower/pkg/registry/trust.go:91.75,92.39 1 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:95.2,95.46 1 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:92.39,94.3 1 0 -github.com/containrrr/watchtower/pkg/registry/trust.go:99.56,101.2 1 1 -github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:13.68,17.16 3 1 -github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:21.2,22.16 2 1 -github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:25.2,31.26 3 1 -github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:17.16,19.3 1 0 -github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:22.16,24.3 1 0 -github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:34.71,37.46 3 1 -github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:45.2,45.17 1 1 -github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:37.46,41.3 3 1 -github.com/containrrr/watchtower/pkg/registry/manifest/manifest.go:41.8,44.3 2 1 -github.com/containrrr/watchtower/pkg/container/container.go:16.97,21.2 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:33.57,35.2 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:38.32,40.2 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:45.37,47.2 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:50.34,52.2 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:56.37,58.2 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:63.39,66.9 2 1 -github.com/containrrr/watchtower/pkg/container/container.go:70.2,70.39 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:74.2,74.18 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:66.9,68.3 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:70.39,72.3 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:79.43,81.9 2 1 -github.com/containrrr/watchtower/pkg/container/container.go:85.2,86.16 2 1 -github.com/containrrr/watchtower/pkg/container/container.go:90.2,90.25 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:81.9,83.3 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:86.16,88.3 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:95.41,97.9 2 0 -github.com/containrrr/watchtower/pkg/container/container.go:101.2,102.16 2 0 -github.com/containrrr/watchtower/pkg/container/container.go:106.2,106.19 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:97.9,99.3 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:102.16,104.3 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:111.43,113.9 2 0 -github.com/containrrr/watchtower/pkg/container/container.go:117.2,117.24 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:113.9,115.3 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:122.37,127.31 3 1 -github.com/containrrr/watchtower/pkg/container/container.go:132.2,132.69 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:139.2,139.14 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:127.31,130.3 2 1 -github.com/containrrr/watchtower/pkg/container/container.go:132.69,133.57 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:133.57,136.4 2 1 -github.com/containrrr/watchtower/pkg/container/container.go:144.37,146.2 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:152.40,154.2 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:161.43,168.29 5 0 -github.com/containrrr/watchtower/pkg/container/container.go:172.2,172.16 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:168.29,170.3 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:178.40,180.2 1 1 -github.com/containrrr/watchtower/pkg/container/container.go:193.60,198.49 4 0 -github.com/containrrr/watchtower/pkg/container/container.go:202.2,202.37 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:206.2,206.42 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:210.2,210.64 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:217.2,224.37 4 0 -github.com/containrrr/watchtower/pkg/container/container.go:229.2,229.57 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:233.2,234.15 2 0 -github.com/containrrr/watchtower/pkg/container/container.go:198.49,200.3 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:202.37,204.3 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:206.42,208.3 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:210.64,212.51 2 0 -github.com/containrrr/watchtower/pkg/container/container.go:212.51,214.4 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:224.37,225.47 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:225.47,227.4 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:229.57,231.3 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:239.61,242.40 2 0 -github.com/containrrr/watchtower/pkg/container/container.go:249.2,249.19 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:242.40,247.3 3 0 -github.com/containrrr/watchtower/pkg/container/container.go:253.40,255.2 1 0 -github.com/containrrr/watchtower/pkg/container/container.go:257.52,259.2 1 0 -github.com/containrrr/watchtower/pkg/container/metadata.go:19.57,21.2 1 0 -github.com/containrrr/watchtower/pkg/container/metadata.go:24.58,26.2 1 0 -github.com/containrrr/watchtower/pkg/container/metadata.go:29.58,31.2 1 0 -github.com/containrrr/watchtower/pkg/container/metadata.go:34.59,36.2 1 0 -github.com/containrrr/watchtower/pkg/container/metadata.go:40.61,43.2 2 1 -github.com/containrrr/watchtower/pkg/container/metadata.go:45.62,46.57 1 1 -github.com/containrrr/watchtower/pkg/container/metadata.go:49.2,49.11 1 1 -github.com/containrrr/watchtower/pkg/container/metadata.go:46.57,48.3 1 1 -github.com/containrrr/watchtower/pkg/container/metadata.go:52.63,55.2 2 1 -github.com/containrrr/watchtower/pkg/container/client.go:43.101,46.16 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:50.2,56.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:46.16,48.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:67.77,71.27 3 1 -github.com/containrrr/watchtower/pkg/container/client.go:77.2,84.16 3 1 -github.com/containrrr/watchtower/pkg/container/client.go:88.2,88.46 1 1 -github.com/containrrr/watchtower/pkg/container/client.go:100.2,100.16 1 1 -github.com/containrrr/watchtower/pkg/container/client.go:71.27,73.3 1 1 -github.com/containrrr/watchtower/pkg/container/client.go:73.8,75.3 1 1 -github.com/containrrr/watchtower/pkg/container/client.go:84.16,86.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:88.46,91.17 2 1 -github.com/containrrr/watchtower/pkg/container/client.go:95.3,95.12 1 1 -github.com/containrrr/watchtower/pkg/container/client.go:91.17,93.4 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:95.12,97.4 1 1 -github.com/containrrr/watchtower/pkg/container/client.go:103.60,107.27 3 1 -github.com/containrrr/watchtower/pkg/container/client.go:112.2,112.19 1 1 -github.com/containrrr/watchtower/pkg/container/client.go:107.27,110.3 2 1 -github.com/containrrr/watchtower/pkg/container/client.go:115.80,119.16 3 1 -github.com/containrrr/watchtower/pkg/container/client.go:123.2,124.16 2 1 -github.com/containrrr/watchtower/pkg/container/client.go:128.2,128.77 1 1 -github.com/containrrr/watchtower/pkg/container/client.go:119.16,121.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:124.16,126.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:131.84,134.18 3 0 -github.com/containrrr/watchtower/pkg/container/client.go:138.2,138.19 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:146.2,148.43 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:159.2,159.64 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:163.2,163.12 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:134.18,136.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:138.19,140.70 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:140.70,142.4 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:148.43,150.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:150.8,153.144 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:153.144,155.4 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:159.64,161.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:166.72,173.58 5 0 -github.com/containrrr/watchtower/pkg/container/client.go:183.2,187.16 4 0 -github.com/containrrr/watchtower/pkg/container/client.go:191.2,191.40 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:209.2,209.45 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:213.2,213.78 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:173.58,175.51 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:180.3,180.65 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:175.51,178.9 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:187.16,189.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:191.40,193.54 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:200.3,200.51 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:193.54,195.18 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:195.18,197.5 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:200.51,202.18 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:202.18,204.5 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:209.45,211.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:217.131,222.16 4 0 -github.com/containrrr/watchtower/pkg/container/client.go:225.2,225.12 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:222.16,224.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:228.79,232.2 3 0 -github.com/containrrr/watchtower/pkg/container/client.go:234.80,237.24 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:243.2,243.43 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:237.24,239.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:239.8,239.64 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:239.64,241.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:246.96,251.16 4 0 -github.com/containrrr/watchtower/pkg/container/client.go:255.2,255.35 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:260.2,261.18 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:251.16,253.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:255.35,258.3 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:264.86,275.16 6 0 -github.com/containrrr/watchtower/pkg/container/client.go:280.2,281.86 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:288.2,291.16 3 0 -github.com/containrrr/watchtower/pkg/container/client.go:296.2,298.51 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:302.2,302.12 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:275.16,278.3 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:281.86,283.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:283.8,283.18 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:283.18,286.3 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:291.16,294.3 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:298.51,301.3 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:305.61,316.2 3 0 -github.com/containrrr/watchtower/pkg/container/client.go:318.98,329.16 4 0 -github.com/containrrr/watchtower/pkg/container/client.go:333.2,337.22 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:342.2,344.16 3 0 -github.com/containrrr/watchtower/pkg/container/client.go:348.2,349.22 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:362.2,363.16 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:367.2,367.12 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:329.16,331.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:337.22,339.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:344.16,346.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:349.22,353.17 4 0 -github.com/containrrr/watchtower/pkg/container/client.go:353.17,355.4 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:355.9,355.25 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:355.25,357.4 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:363.16,365.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:370.118,374.17 3 0 -github.com/containrrr/watchtower/pkg/container/client.go:381.2,381.6 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:406.2,406.12 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:374.17,377.3 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:377.8,379.3 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:381.6,390.17 3 0 -github.com/containrrr/watchtower/pkg/container/client.go:393.3,393.34 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:397.3,397.26 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:400.3,400.31 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:404.3,404.8 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:390.17,392.4 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:393.34,395.12 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:397.26,399.4 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:400.31,403.4 2 0 -github.com/containrrr/watchtower/pkg/container/client.go:409.92,413.6 3 0 -github.com/containrrr/watchtower/pkg/container/client.go:413.6,414.10 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:424.3,424.30 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:415.18,416.14 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:417.11,418.70 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:418.70,420.5 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:420.10,420.32 1 0 -github.com/containrrr/watchtower/pkg/container/client.go:420.32,422.5 1 0 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:24.121,30.49 5 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:34.2,35.53 2 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:39.2,41.43 3 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:45.2,52.43 4 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:55.2,55.44 1 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:60.2,60.67 1 0 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:30.49,32.3 1 0 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:35.53,37.3 1 0 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:41.43,43.3 1 0 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:52.43,54.3 1 0 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:55.44,58.3 2 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:64.63,67.16 2 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:70.2,72.17 3 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:67.16,69.3 1 0 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:76.139,81.16 4 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:85.2,86.72 2 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:90.2,90.62 1 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:97.2,98.50 2 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:102.2,106.16 4 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:110.2,110.33 1 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:81.16,83.3 1 0 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:86.72,88.3 1 0 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:90.62,93.3 2 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:93.8,95.3 1 0 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:98.50,100.3 1 0 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:106.16,108.3 1 0 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:114.66,120.29 5 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:128.2,128.79 1 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:132.2,140.21 8 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:120.29,126.3 5 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:128.79,130.3 1 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:144.52,147.16 3 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:151.2,156.17 2 1 -github.com/containrrr/watchtower/pkg/registry/auth/auth.go:147.16,149.3 1 0 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:24.124,28.16 4 1 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:32.2,33.16 2 1 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:37.2,37.64 1 1 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:41.2,44.24 3 1 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:48.2,48.40 1 1 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:56.2,56.19 1 0 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:28.16,30.3 1 0 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:33.16,35.3 1 0 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:37.64,39.3 1 0 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:44.24,46.3 1 0 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:48.40,51.28 3 1 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:51.28,53.4 1 1 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:60.79,63.17 3 1 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:69.2,75.16 6 1 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:78.2,78.27 1 1 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:81.2,81.49 1 1 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:63.17,65.3 1 1 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:65.8,67.3 1 0 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:75.16,77.3 1 0 -github.com/containrrr/watchtower/pkg/registry/digest/digest.go:78.27,80.3 1 0 -github.com/containrrr/watchtower/internal/actions/check.go:24.101,28.16 3 1 -github.com/containrrr/watchtower/internal/actions/check.go:33.2,33.26 1 1 -github.com/containrrr/watchtower/internal/actions/check.go:38.2,39.62 2 1 -github.com/containrrr/watchtower/internal/actions/check.go:28.16,31.3 2 0 -github.com/containrrr/watchtower/internal/actions/check.go:33.26,36.3 2 1 -github.com/containrrr/watchtower/internal/actions/check.go:42.110,49.44 5 1 -github.com/containrrr/watchtower/internal/actions/check.go:66.2,66.64 1 1 -github.com/containrrr/watchtower/internal/actions/check.go:49.44,50.65 1 1 -github.com/containrrr/watchtower/internal/actions/check.go:57.3,57.14 1 1 -github.com/containrrr/watchtower/internal/actions/check.go:50.65,54.12 3 0 -github.com/containrrr/watchtower/internal/actions/check.go:57.14,58.62 1 1 -github.com/containrrr/watchtower/internal/actions/check.go:58.62,62.5 2 0 -github.com/containrrr/watchtower/internal/actions/check.go:69.55,70.22 1 1 -github.com/containrrr/watchtower/internal/actions/check.go:74.2,76.11 2 0 -github.com/containrrr/watchtower/internal/actions/check.go:79.2,79.11 1 0 -github.com/containrrr/watchtower/internal/actions/check.go:82.2,82.36 1 0 -github.com/containrrr/watchtower/internal/actions/check.go:70.22,72.3 1 1 -github.com/containrrr/watchtower/internal/actions/check.go:76.11,78.3 1 0 -github.com/containrrr/watchtower/internal/actions/check.go:79.11,81.3 1 0 -github.com/containrrr/watchtower/internal/actions/check.go:85.26,88.2 2 1 -github.com/containrrr/watchtower/internal/actions/update.go:17.71,20.27 2 1 -github.com/containrrr/watchtower/internal/actions/update.go:24.2,25.16 2 1 -github.com/containrrr/watchtower/internal/actions/update.go:29.2,29.45 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:41.2,42.16 2 1 -github.com/containrrr/watchtower/internal/actions/update.go:46.2,49.25 3 1 -github.com/containrrr/watchtower/internal/actions/update.go:57.2,57.27 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:63.2,63.27 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:66.2,66.12 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:20.27,22.3 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:25.16,27.3 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:29.45,31.127 2 1 -github.com/containrrr/watchtower/internal/actions/update.go:34.3,34.17 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:38.3,38.30 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:31.127,33.4 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:34.17,37.4 2 0 -github.com/containrrr/watchtower/internal/actions/update.go:42.16,44.3 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:49.25,50.45 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:50.45,51.38 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:51.38,53.5 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:57.27,59.3 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:59.8,62.3 2 1 -github.com/containrrr/watchtower/internal/actions/update.go:63.27,65.3 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:69.114,72.44 2 0 -github.com/containrrr/watchtower/internal/actions/update.go:79.2,79.20 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:72.44,73.26 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:73.26,76.4 2 0 -github.com/containrrr/watchtower/internal/actions/update.go:79.20,81.3 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:84.122,85.44 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:85.44,87.3 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:90.108,91.30 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:96.2,96.22 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:99.2,99.27 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:107.2,107.72 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:91.30,94.3 2 0 -github.com/containrrr/watchtower/internal/actions/update.go:96.22,98.3 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:99.27,100.78 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:100.78,104.4 3 0 -github.com/containrrr/watchtower/internal/actions/update.go:107.72,109.3 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:112.123,115.44 2 1 -github.com/containrrr/watchtower/internal/actions/update.go:123.2,123.20 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:115.44,116.28 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:119.3,120.44 2 1 -github.com/containrrr/watchtower/internal/actions/update.go:116.28,117.12 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:123.20,125.3 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:128.71,129.32 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:129.32,130.57 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:130.57,132.4 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:136.111,141.30 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:148.2,148.23 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:141.30,142.76 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:142.76,145.4 2 0 -github.com/containrrr/watchtower/internal/actions/update.go:148.23,149.74 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:149.74,151.4 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:151.9,151.54 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:151.54,153.4 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:157.58,159.36 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:159.36,160.25 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:164.2,165.43 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:160.25,161.12 1 1 -github.com/containrrr/watchtower/internal/actions/update.go:165.43,166.37 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:166.37,167.54 1 0 -github.com/containrrr/watchtower/internal/actions/update.go:167.54,169.20 2 0 diff --git a/internal/actions/mocks/client.go b/internal/actions/mocks/client.go index 33c196d..b17c987 100644 --- a/internal/actions/mocks/client.go +++ b/internal/actions/mocks/client.go @@ -82,3 +82,8 @@ func (client MockClient) ExecuteCommand(containerID string, command string, time func (client MockClient) IsContainerStale(c container.Container) (bool, error) { return true, nil } + +// WarnOnHeadPullFailed is always true for the mock client +func (client MockClient) WarnOnHeadPullFailed(c container.Container) bool { + return true +} diff --git a/internal/flags/flags.go b/internal/flags/flags.go index c2dc8ad..80a5a7c 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -307,6 +307,12 @@ Should only be used for testing.`) "", viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"), "The shoutrrr URL to send notifications to") + + flags.String( + "warn-on-head-failure", + viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"), + "When to warn about HEAD pull requests failing. Possible values: always, auto or never") + } // SetDefaults provides default values for environment variables diff --git a/pkg/api/metrics/metrics_test.go b/pkg/api/metrics/metrics_test.go index 156601f..44379ee 100644 --- a/pkg/api/metrics/metrics_test.go +++ b/pkg/api/metrics/metrics_test.go @@ -37,12 +37,10 @@ func getWithToken(c http.Client, url string) (*http.Response, error) { var _ = Describe("the metrics", func() { httpAPI := api.New(Token) m := metricsAPI.New() + httpAPI.RegisterHandler(m.Path, m.Handle) httpAPI.Start(false) - // We should likely split this into multiple tests, but as prometheus requires a restart of the binary - // to reset the metrics and gauges, we'll just do it all at once. - It("should serve metrics", func() { metric := &metrics.Metric{ Scanned: 4, @@ -50,12 +48,15 @@ var _ = Describe("the metrics", func() { Failed: 1, } metrics.RegisterScan(metric) + Eventually(metrics.Default().QueueIsEmpty).Should(BeTrue()) + c := http.Client{} + res, err := getWithToken(c, "http://localhost:8080/v1/metrics") + Expect(err).ToNot(HaveOccurred()) - Expect(err).NotTo(HaveOccurred()) contents, err := ioutil.ReadAll(res.Body) - + Expect(err).ToNot(HaveOccurred()) Expect(string(contents)).To(ContainSubstring("watchtower_containers_updated 3")) Expect(string(contents)).To(ContainSubstring("watchtower_containers_failed 1")) Expect(string(contents)).To(ContainSubstring("watchtower_containers_scanned 4")) @@ -65,11 +66,13 @@ var _ = Describe("the metrics", func() { for i := 0; i < 3; i++ { metrics.RegisterScan(nil) } + Eventually(metrics.Default().QueueIsEmpty).Should(BeTrue()) res, err = getWithToken(c, "http://localhost:8080/v1/metrics") - Expect(err).NotTo(HaveOccurred()) - contents, err = ioutil.ReadAll(res.Body) + Expect(err).ToNot(HaveOccurred()) + contents, err = ioutil.ReadAll(res.Body) + Expect(err).ToNot(HaveOccurred()) Expect(string(contents)).To(ContainSubstring("watchtower_scans_total 4")) Expect(string(contents)).To(ContainSubstring("watchtower_scans_skipped 3")) }) diff --git a/pkg/container/client.go b/pkg/container/client.go index b125ed6..93eacb7 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -33,6 +33,7 @@ type Client interface { IsContainerStale(Container) (bool, error) ExecuteCommand(containerID string, command string, timeout int) error RemoveImageByID(string) error + WarnOnHeadPullFailed(container Container) bool } // NewClient returns a new Client instance which can be used to interact with @@ -41,7 +42,7 @@ type Client interface { // * DOCKER_HOST the docker-engine host to send api requests to // * DOCKER_TLS_VERIFY whether to verify tls certificates // * DOCKER_API_VERSION the minimum docker api version to work with -func NewClient(pullImages bool, includeStopped bool, reviveStopped bool, removeVolumes bool, includeRestarting bool) Client { +func NewClient(pullImages, includeStopped, reviveStopped, removeVolumes, includeRestarting bool, warnOnHeadFailed string) Client { cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv) if err != nil { @@ -55,6 +56,7 @@ func NewClient(pullImages bool, includeStopped bool, reviveStopped bool, removeV includeStopped: includeStopped, reviveStopped: reviveStopped, includeRestarting: includeRestarting, + warnOnHeadFailed: warnOnHeadFailed, } } @@ -65,6 +67,18 @@ type dockerClient struct { includeStopped bool reviveStopped bool includeRestarting bool + warnOnHeadFailed string +} + +func (client dockerClient) WarnOnHeadPullFailed(container Container) bool { + if client.warnOnHeadFailed == "always" { + return true + } + if client.warnOnHeadFailed == "never" { + return false + } + + return registry.WarnOnAPIConsumption(container) } func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) { @@ -275,6 +289,8 @@ func (client dockerClient) HasNewImage(ctx context.Context, container Container) return true, nil } +// 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 { containerName := container.Name() imageName := container.ImageName() @@ -297,12 +313,12 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e log.WithFields(fields).Debugf("Checking if pull is needed") if match, err := digest.CompareDigest(container, opts.RegistryAuth); err != nil { - if registry.WarnOnAPIConsumption(container) { - log.WithFields(fields).Warning("Could not do a head request, falling back to regular pull.") - } else { - log.Debug("Could not do a head request, falling back to regular pull.") + headLevel := log.DebugLevel + if client.WarnOnHeadPullFailed(container) { + headLevel = log.WarnLevel } - log.Debugf("Reason: %s", err.Error()) + log.WithFields(fields).Logf(headLevel, "Could not do a head request for %q, falling back to regular pull.", imageName) + log.WithFields(fields).Log(headLevel, "Reason: ", err) } else if match { log.Debug("No pull needed. Skipping image.") return nil diff --git a/pkg/container/container_test.go b/pkg/container/container_test.go index 16b8922..8ddeb8b 100644 --- a/pkg/container/container_test.go +++ b/pkg/container/container_test.go @@ -1,8 +1,6 @@ package container import ( - "testing" - "github.com/containrrr/watchtower/pkg/container/mocks" "github.com/containrrr/watchtower/pkg/filters" "github.com/docker/docker/api/types" @@ -12,11 +10,6 @@ import ( . "github.com/onsi/gomega" ) -func TestContainer(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Container Suite") -} - var _ = Describe("the container", func() { Describe("the client", func() { var docker *cli.Client @@ -34,6 +27,35 @@ var _ = Describe("the container", func() { It("should return a client for the api", func() { Expect(client).NotTo(BeNil()) }) + Describe("WarnOnHeadPullFailed", func() { + containerUnknown := *mockContainerWithImageName("unknown.repo/prefix/imagename:latest") + containerKnown := *mockContainerWithImageName("docker.io/prefix/imagename:latest") + + When("warn on head failure is set to \"always\"", func() { + c := NewClient(false, false, false, false, false, "always") + It("should always return true", func() { + Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeTrue()) + Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue()) + }) + }) + When("warn on head failure is set to \"auto\"", func() { + c := NewClient(false, false, false, false, false, "auto") + It("should always return true", func() { + Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse()) + }) + It("should", func() { + Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue()) + }) + }) + When("warn on head failure is set to \"never\"", func() { + c := NewClient(false, false, false, false, false, "never") + It("should never return true", func() { + Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse()) + Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeFalse()) + }) + }) + }) + When("listing containers without any filter", func() { It("should return all available containers", func() { containers, err := client.ListContainers(filters.NoFilter) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 3a235af..d8761ba 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -24,6 +24,11 @@ type Metrics struct { skipped prometheus.Counter } +// QueueIsEmpty checks whether any messages are enqueued in the channel +func (metrics *Metrics) QueueIsEmpty() bool { + return len(metrics.channel) == 0 +} + // Register registers metrics for an executed scan func (metrics *Metrics) Register(metric *Metric) { metrics.channel <- metric diff --git a/pkg/registry/digest/digest.go b/pkg/registry/digest/digest.go index 59f4d9b..894c162 100644 --- a/pkg/registry/digest/digest.go +++ b/pkg/registry/digest/digest.go @@ -95,7 +95,11 @@ func GetDigest(url string, token string) (string, error) { defer res.Body.Close() if res.StatusCode != 200 { - return "", fmt.Errorf("registry responded to head request with %v", res) + wwwAuthHeader := res.Header.Get("www-authenticate") + if wwwAuthHeader == "" { + wwwAuthHeader = "not present" + } + return "", fmt.Errorf("registry responded to head request with %q, auth: %q", res.Status, wwwAuthHeader) } return res.Header.Get(ContentDigestHeader), nil } diff --git a/pkg/registry/registry_suite_test.go b/pkg/registry/registry_suite_test.go new file mode 100644 index 0000000..fe31f12 --- /dev/null +++ b/pkg/registry/registry_suite_test.go @@ -0,0 +1,13 @@ +package registry_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestRegistry(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Registry Suite") +} diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go new file mode 100644 index 0000000..5f3f57f --- /dev/null +++ b/pkg/registry/registry_test.go @@ -0,0 +1,45 @@ +package registry_test + +import ( + "github.com/containrrr/watchtower/internal/actions/mocks" + unit "github.com/containrrr/watchtower/pkg/registry" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "time" +) + +var _ = Describe("Registry", func() { + Describe("WarnOnAPIConsumption", func() { + When("Given a container with an image from ghcr.io", func() { + It("should want to warn", func() { + Expect(testContainerWithImage("ghcr.io/containrrr/watchtower")).To(BeTrue()) + }) + }) + When("Given a container with an image implicitly from dockerhub", func() { + It("should want to warn", func() { + Expect(testContainerWithImage("docker:latest")).To(BeTrue()) + }) + }) + 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() { + Expect(testContainerWithImage("docker.fsf.org/docker:latest")).To(BeFalse()) + Expect(testContainerWithImage("altavista.com/docker:latest")).To(BeFalse()) + Expect(testContainerWithImage("gitlab.com/docker:latest")).To(BeFalse()) + }) + }) + }) +}) + +func testContainerWithImage(imageName string) bool { + container := mocks.CreateMockContainer("", "", imageName, time.Now()) + return unit.WarnOnAPIConsumption(container) +} diff --git a/pkg/registry/trust_test.go b/pkg/registry/trust_test.go index 7d4d48d..3dab6ad 100644 --- a/pkg/registry/trust_test.go +++ b/pkg/registry/trust_test.go @@ -4,14 +4,8 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "os" - "testing" ) -func TestTrust(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Trust Suite") -} - var _ = Describe("Testing with Ginkgo", func() { It("encoded env auth_ should return an error if repo envs are unset", func() { _ = os.Unsetenv("REPO_USER") diff --git a/scripts/codecov.sh b/scripts/codecov.sh new file mode 100755 index 0000000..a3bc024 --- /dev/null +++ b/scripts/codecov.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +go test -v -coverprofile coverage.out -covermode atomic ./... + +# Requires CODECOV_TOKEN to be set +bash <(curl -s https://codecov.io/bash) \ No newline at end of file From 6f281b727aa9a3b83ebe8072ce64273d1227d703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Fri, 23 Apr 2021 16:36:09 +0200 Subject: [PATCH 90/98] feat: update shoutrrr to v0.4.4 (#914) --- go.mod | 2 +- go.sum | 4 ++-- pkg/notifications/shoutrrr.go | 7 +++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index bf557f7..35a2c24 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect github.com/cloudflare/cfssl v0.0.0-20190911221928-1a911ca1b1d6 // indirect github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect - github.com/containrrr/shoutrrr v0.4.1 + github.com/containrrr/shoutrrr v0.4.4 github.com/docker/cli v0.0.0-20190327152802-57b27434ea29 github.com/docker/distribution v2.7.1+incompatible github.com/docker/docker v0.0.0-20190404075923-dbe4a30928d4 diff --git a/go.sum b/go.sum index 19fe808..e1d7071 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/cloudflare/cfssl v0.0.0-20190911221928-1a911ca1b1d6 h1:A7RURps5t4yDU0 github.com/cloudflare/cfssl v0.0.0-20190911221928-1a911ca1b1d6/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882bXEDKfWIf0wa8HRvpnBoPszJJXL+TVbBw4M= github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containrrr/shoutrrr v0.4.1 h1:+p5+3Gb5dhzjUf3yriUIK6IeXtElJFFgBUGD9vb9ygE= -github.com/containrrr/shoutrrr v0.4.1/go.mod h1:zqL2BvfC1W4FujrT4b3/ZCLxvD+uoeEpBL7rg9Dqpbg= +github.com/containrrr/shoutrrr v0.4.4 h1:vHZ4E/76pKVY+Jyn/qhBz3X540Bn8NI5ppPHK4PyILY= +github.com/containrrr/shoutrrr v0.4.4/go.mod h1:zqL2BvfC1W4FujrT4b3/ZCLxvD+uoeEpBL7rg9Dqpbg= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index 8376c91..087e4d6 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -3,11 +3,12 @@ package notifications import ( "bytes" "fmt" - "github.com/containrrr/shoutrrr/pkg/types" + stdlog "log" "strings" "text/template" "github.com/containrrr/shoutrrr" + "github.com/containrrr/shoutrrr/pkg/types" t "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -59,7 +60,9 @@ func newShoutrrrNotifierFromURL(c *cobra.Command, url string, levels []log.Level } func createSender(urls []string, levels []log.Level, template *template.Template) t.Notifier { - r, err := shoutrrr.CreateSender(urls...) + + traceWriter := log.StandardLogger().WriterLevel(log.TraceLevel) + r, err := shoutrrr.NewSender(stdlog.New(traceWriter, "Shoutrrr: ", 0), urls...) if err != nil { log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error()) } From dff8378778716100ba2dcb1485122bb4f3e059cb Mon Sep 17 00:00:00 2001 From: Simon Aronsson Date: Sat, 24 Apr 2021 13:14:52 +0200 Subject: [PATCH 91/98] Feat/head failure toggle (#928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: make head pull failure warning toggleable * expect prometheus tests to go through EVENTUALLY * wait for queue to be empty before checking test conditions * clean up new head failure toggle * fixup! clean up new head failure toggle * test: add warn on head failure tests * fix client interface and make tests hit more lines * make all tests use NewClient instead of creating a struct pointer * fix lint issues * see if moving ubuntu out of the matrix solves test issue Co-authored-by: nils måsén --- .github/workflows/pull-request.yml | 22 ++++++++++++++++++++-- pkg/metrics/metrics.go | 5 +++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 099e259..24bfe13 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -33,7 +33,6 @@ jobs: go-version: - 1.15.x platform: - - ubuntu-latest - macos-latest - windows-latest runs-on: ${{ matrix.platform }} @@ -53,7 +52,26 @@ jobs: uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} - + test-ubuntu: + name: Test (Ubuntu) + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.15.x + - name: Run tests + run: | + go test -v -coverprofile coverage.out -covermode atomic ./... + - name: Publish coverage + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} build: name: Build runs-on: ubuntu-latest diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index d8761ba..91be2e6 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -29,6 +29,11 @@ func (metrics *Metrics) QueueIsEmpty() bool { return len(metrics.channel) == 0 } +// QueueIsEmpty checks whether any messages are enqueued in the channel +func (metrics *Metrics) QueueIsEmpty() bool { + return len(metrics.channel) == 0 +} + // Register registers metrics for an executed scan func (metrics *Metrics) Register(metric *Metric) { metrics.channel <- metric From d38e52b5c6a2ebee4e60ca08e5e379851e36845a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 24 Apr 2021 15:42:31 +0200 Subject: [PATCH 92/98] fix: merge artifacts and broken shoutrrr tests (#929) * test: add missing container test suite * fix broken tests * fix: remove duplicate merge artifact Co-authored-by: Simon Aronsson --- go.mod | 2 +- internal/actions/mocks/container.go | 2 ++ pkg/container/container_suite_test.go | 13 +++++++++++++ pkg/metrics/metrics.go | 5 ----- pkg/notifications/notifier_test.go | 26 ++++++++++++++++++++------ 5 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 pkg/container/container_suite_test.go diff --git a/go.mod b/go.mod index 35a2c24..225915b 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/docker/docker v0.0.0-20190404075923-dbe4a30928d4 github.com/docker/docker-credential-helpers v0.6.1 // indirect github.com/docker/go v1.5.1-1 // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-connections v0.4.0 github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82 // indirect github.com/docker/go-units v0.3.3 // indirect github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect diff --git a/internal/actions/mocks/container.go b/internal/actions/mocks/container.go index 1db8652..a4ded79 100644 --- a/internal/actions/mocks/container.go +++ b/internal/actions/mocks/container.go @@ -4,6 +4,7 @@ import ( "github.com/containrrr/watchtower/pkg/container" "github.com/docker/docker/api/types" container2 "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" "time" ) @@ -19,6 +20,7 @@ func CreateMockContainer(id string, name string, image string, created time.Time Config: &container2.Config{ Image: image, Labels: make(map[string]string), + ExposedPorts: map[nat.Port]struct{}{}, }, } return *container.NewContainer( diff --git a/pkg/container/container_suite_test.go b/pkg/container/container_suite_test.go new file mode 100644 index 0000000..292a008 --- /dev/null +++ b/pkg/container/container_suite_test.go @@ -0,0 +1,13 @@ +package container_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestContainer(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Container Suite") +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 91be2e6..d8761ba 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -29,11 +29,6 @@ func (metrics *Metrics) QueueIsEmpty() bool { return len(metrics.channel) == 0 } -// QueueIsEmpty checks whether any messages are enqueued in the channel -func (metrics *Metrics) QueueIsEmpty() bool { - return len(metrics.channel) == 0 -} - // Register registers metrics for an executed scan func (metrics *Metrics) Register(metric *Metric) { metrics.channel <- metric diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go index ecd228f..ba6657a 100644 --- a/pkg/notifications/notifier_test.go +++ b/pkg/notifications/notifier_test.go @@ -47,7 +47,7 @@ var _ = Describe("notifications", func() { token := "abvsihdbau" color := notifications.ColorInt title := url.QueryEscape(notifications.GetTitle()) - expected := fmt.Sprintf("discord://%s@%s?avatar=&color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&splitlines=Yes&title=%s&username=watchtower", token, channel, color, title) + expected := fmt.Sprintf("discord://%s@%s?color=0x%x&colordebug=0x0&colorerror=0x0&colorinfo=0x0&colorwarn=0x0&splitlines=Yes&title=%s&username=watchtower", token, channel, color, title) buildArgs := func(url string) []string { return []string{ "--notifications", @@ -101,7 +101,7 @@ var _ = Describe("notifications", func() { host := "shoutrrr.local" title := url.QueryEscape(notifications.GetTitle()) - expectedOutput := fmt.Sprintf("gotify://%s/%s?disabletls=No&priority=0&title=%s", host, token, title) + expectedOutput := fmt.Sprintf("gotify://%s/%s?title=%s", host, token, title) args := []string{ "--notification-gotify-url", @@ -128,7 +128,7 @@ var _ = Describe("notifications", func() { title := url.QueryEscape(notifications.GetTitle()) hookURL := fmt.Sprintf("https://outlook.office.com/webhook/%s/IncomingWebhook/%s/%s", tokenA, tokenB, tokenC) - expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s&host=outlook.office.com&title=%s", tokenA, tokenB, tokenC, color, title) + expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s&title=%s", tokenA, tokenB, tokenC, color, title) args := []string{ "--notification-msteams-hook", @@ -147,10 +147,18 @@ var _ = Describe("notifications", func() { When("converting an email service config into a shoutrrr url", func() { It("should set the from address in the URL", func() { fromAddress := "lala@example.com" - expectedOutput := buildExpectedURL("", "", "", 25, fromAddress, "", "None") + expectedOutput := buildExpectedURL("containrrrbot", "secret-password", "mail.containrrr.dev", 25, fromAddress, "mail@example.com", "Plain") args := []string{ "--notification-email-from", fromAddress, + "--notification-email-to", + "mail@example.com", + "--notification-email-server-user", + "containrrrbot", + "--notification-email-server-password", + "secret-password", + "--notification-email-server", + "mail.containrrr.dev", } testURL(builderFn, args, expectedOutput) }) @@ -159,13 +167,19 @@ var _ = Describe("notifications", func() { fromAddress := "sender@example.com" toAddress := "receiver@example.com" - expectedOutput := buildExpectedURL("", "", "", 25, fromAddress, toAddress, "None") + expectedOutput := buildExpectedURL("containrrrbot", "secret-password", "mail.containrrr.dev", 25, fromAddress, toAddress, "Plain") args := []string{ "--notification-email-from", fromAddress, "--notification-email-to", toAddress, + "--notification-email-server-user", + "containrrrbot", + "--notification-email-server-password", + "secret-password", + "--notification-email-server", + "mail.containrrr.dev", } testURL(builderFn, args, expectedOutput) @@ -180,7 +194,7 @@ func buildExpectedURL(username string, password string, host string, port int, f subject := fmt.Sprintf("Watchtower updates on %s", hostname) - var template = "smtp://%s:%s@%s:%d/?auth=%s&encryption=Auto&fromaddress=%s&fromname=Watchtower&starttls=Yes&subject=%s&toaddresses=%s&usehtml=No" + var template = "smtp://%s:%s@%s:%d/?auth=%s&fromaddress=%s&fromname=Watchtower&subject=%s&toaddresses=%s" return fmt.Sprintf(template, url.QueryEscape(username), url.QueryEscape(password), From fdf6e46e7bcd848b44c49b62e211a55585a56c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 24 Apr 2021 18:25:05 +0200 Subject: [PATCH 93/98] fix: use default http transport for head (#926) note: still disables TLS verification to enable use with local regisitries --- pkg/registry/digest/digest.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pkg/registry/digest/digest.go b/pkg/registry/digest/digest.go index 894c162..4634688 100644 --- a/pkg/registry/digest/digest.go +++ b/pkg/registry/digest/digest.go @@ -10,8 +10,10 @@ import ( "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 @@ -69,7 +71,17 @@ func TransformAuth(registryAuth string) string { // GetDigest from registry using a HEAD request to prevent rate limiting func GetDigest(url string, token string) (string, error) { tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } client := &http.Client{Transport: tr} From 12467712a19ff5b99319b3b55cc798afb3da5ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 24 Apr 2021 18:29:05 +0200 Subject: [PATCH 94/98] feat: check container config before update (#925) * feat: check container config before restart * fix: only skip when hostconfig and config differ * fix: update test mocks to not fail tests * test: add verify config tests --- cmd/root.go | 7 ++- internal/actions/mocks/container.go | 7 ++- internal/actions/update.go | 19 ++++-- pkg/container/container.go | 29 +++++++++ pkg/container/container_test.go | 95 +++++++++++++++++++++++++++-- pkg/container/errors.go | 7 +++ 6 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 pkg/container/errors.go diff --git a/cmd/root.go b/cmd/root.go index 6f84727..707c4fd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -269,6 +269,9 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) { } log.Info("Watchtower ", version, "\n", notifs, "\n", filtering, "\n", schedMessage) + if log.IsLevelEnabled(log.TraceLevel) { + log.Warn("trace level enabled: log will include sensitive information as credentials and tokens") + } } } @@ -330,8 +333,10 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric { } metricResults, err := actions.Update(client, updateParams) if err != nil { - log.Println(err) + log.Error(err) } notifier.SendNotification() + log.Debugf("Session done: %v scanned, %v updated, %v failed", + metricResults.Scanned, metricResults.Updated, metricResults.Failed) return metricResults } diff --git a/internal/actions/mocks/container.go b/internal/actions/mocks/container.go index a4ded79..0c0ee94 100644 --- a/internal/actions/mocks/container.go +++ b/internal/actions/mocks/container.go @@ -16,10 +16,13 @@ func CreateMockContainer(id string, name string, image string, created time.Time Image: image, Name: name, Created: created.String(), + HostConfig: &container2.HostConfig{ + PortBindings: map[nat.Port][]nat.PortBinding{}, + }, }, Config: &container2.Config{ - Image: image, - Labels: make(map[string]string), + Image: image, + Labels: make(map[string]string), ExposedPorts: map[nat.Port]struct{}{}, }, } diff --git a/internal/actions/update.go b/internal/actions/update.go index 06bb345..66a28f1 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -1,7 +1,6 @@ package actions import ( - "errors" "github.com/containrrr/watchtower/internal/util" "github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/lifecycle" @@ -33,11 +32,23 @@ func Update(client container.Client, params types.UpdateParams) (*metrics2.Metri for i, targetContainer := range containers { stale, err := client.IsContainerStale(targetContainer) - if stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly() && !targetContainer.HasImageInfo() { - err = errors.New("no available image info") + shouldUpdate := stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly() + if err == nil && shouldUpdate { + // Check to make sure we have all the necessary information for recreating the container + err = targetContainer.VerifyConfiguration() + // If the image information is incomplete and trace logging is enabled, log it for further diagnosis + if err != nil && log.IsLevelEnabled(log.TraceLevel) { + imageInfo := targetContainer.ImageInfo() + log.Tracef("Image info: %#v", imageInfo) + log.Tracef("Container info: %#v", targetContainer.ContainerInfo()) + if imageInfo != nil { + log.Tracef("Image config: %#v", imageInfo.Config) + } + } } + if err != nil { - log.Infof("Unable to update container %q: %v. Proceeding to next.", containers[i].Name(), err) + log.Infof("Unable to update container %q: %v. Proceeding to next.", targetContainer.Name(), err) stale = false staleCheckFailed++ metric.Failed++ diff --git a/pkg/container/container.go b/pkg/container/container.go index 7631b5e..92abec2 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -258,3 +258,32 @@ func (c Container) HasImageInfo() bool { func (c Container) ImageInfo() *types.ImageInspect { return c.imageInfo } + +// VerifyConfiguration checks the container and image configurations for nil references to make sure +// that the container can be recreated once deleted +func (c Container) VerifyConfiguration() error { + if c.imageInfo == nil { + return errorNoImageInfo + } + + containerInfo := c.ContainerInfo() + if containerInfo == nil { + return errorInvalidConfig + } + + containerConfig := containerInfo.Config + if containerConfig == nil { + return errorInvalidConfig + } + + hostConfig := containerInfo.HostConfig + if hostConfig == nil { + return errorInvalidConfig + } + + if len(hostConfig.PortBindings) > 0 && containerConfig.ExposedPorts == nil { + return errorNoExposedPorts + } + + return nil +} diff --git a/pkg/container/container_test.go b/pkg/container/container_test.go index 8ddeb8b..8f22044 100644 --- a/pkg/container/container_test.go +++ b/pkg/container/container_test.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" cli "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) @@ -32,14 +33,14 @@ var _ = Describe("the container", func() { containerKnown := *mockContainerWithImageName("docker.io/prefix/imagename:latest") When("warn on head failure is set to \"always\"", func() { - c := NewClient(false, false, false, false, false, "always") + c := newClientNoAPI(false, false, false, false, false, "always") It("should always return true", func() { Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeTrue()) Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue()) }) }) When("warn on head failure is set to \"auto\"", func() { - c := NewClient(false, false, false, false, false, "auto") + c := newClientNoAPI(false, false, false, false, false, "auto") It("should always return true", func() { Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse()) }) @@ -48,7 +49,7 @@ var _ = Describe("the container", func() { }) }) When("warn on head failure is set to \"never\"", func() { - c := NewClient(false, false, false, false, false, "never") + c := newClientNoAPI(false, false, false, false, false, "never") It("should never return true", func() { Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse()) Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeFalse()) @@ -130,6 +131,63 @@ 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.imageInfo = nil + err := c.VerifyConfiguration() + Expect(err).To(Equal(errorNoImageInfo)) + }) + }) + When("verifying a container with no container info", func() { + It("should return an error", func() { + c := mockContainerWithPortBindings() + c.containerInfo = nil + err := c.VerifyConfiguration() + Expect(err).To(Equal(errorInvalidConfig)) + }) + }) + When("verifying a container with no config", func() { + It("should return an error", func() { + c := mockContainerWithPortBindings() + c.containerInfo.Config = nil + err := c.VerifyConfiguration() + Expect(err).To(Equal(errorInvalidConfig)) + }) + }) + When("verifying a container with no host config", func() { + It("should return an error", func() { + c := mockContainerWithPortBindings() + c.containerInfo.HostConfig = nil + err := c.VerifyConfiguration() + Expect(err).To(Equal(errorInvalidConfig)) + }) + }) + When("verifying a container with no port bindings", func() { + It("should not return an error", func() { + c := mockContainerWithPortBindings() + err := c.VerifyConfiguration() + Expect(err).ToNot(HaveOccurred()) + }) + }) + When("verifying a container with port bindings, but no exposed ports", func() { + It("should return an error", func() { + c := mockContainerWithPortBindings("80/tcp") + c.containerInfo.Config.ExposedPorts = nil + err := c.VerifyConfiguration() + Expect(err).To(Equal(errorNoExposedPorts)) + }) + }) + 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.containerInfo.Config.ExposedPorts = map[nat.Port]struct{}{"80/tcp": {}} + err := c.VerifyConfiguration() + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) When("asked for metadata", func() { var c *Container BeforeEach(func() { @@ -281,10 +339,23 @@ 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 { - container := mockContainerWithLabels(nil) - container.containerInfo.Config.Image = name - return container + mockContainer := mockContainerWithLabels(nil) + mockContainer.containerInfo.Config.Image = name + return mockContainer } func mockContainerWithLinks(links []string) *Container { @@ -317,3 +388,15 @@ func mockContainerWithLabels(labels map[string]string) *Container { } return NewContainer(&content, nil) } + +func newClientNoAPI(pullImages, includeStopped, reviveStopped, removeVolumes, includeRestarting bool, warnOnHeadFailed string) Client { + return dockerClient{ + api: nil, + pullImages: pullImages, + removeVolumes: removeVolumes, + includeStopped: includeStopped, + reviveStopped: reviveStopped, + includeRestarting: includeRestarting, + warnOnHeadFailed: warnOnHeadFailed, + } +} diff --git a/pkg/container/errors.go b/pkg/container/errors.go new file mode 100644 index 0000000..b927220 --- /dev/null +++ b/pkg/container/errors.go @@ -0,0 +1,7 @@ +package container + +import "errors" + +var errorNoImageInfo = errors.New("no available image info") +var errorNoExposedPorts = errors.New("exposed ports does not match port bindings") +var errorInvalidConfig = errors.New("container configuration missing or invalid") From d0148ab796a7c84b69790b295d443d8c04fda8da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 24 Apr 2021 19:04:16 +0200 Subject: [PATCH 95/98] fix manifest creation in release job --- .github/workflows/release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 39e43fc..0393f0a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -95,7 +95,8 @@ jobs: containrrr/watchtower:amd64-$DH_TAG \ containrrr/watchtower:i386-$DH_TAG \ containrrr/watchtower:armhf-$DH_TAG \ - containrrr/watchtower:arm64v8-$DH_TAG \ + containrrr/watchtower:arm64v8-$DH_TAG + docker manifest create \ ghcr.io/containrrr/watchtower:$DH_TAG \ ghcr.io/containrrr/watchtower:amd64-$DH_TAG \ ghcr.io/containrrr/watchtower:i386-$DH_TAG \ @@ -132,7 +133,8 @@ jobs: containrrr/watchtower:amd64-latest \ containrrr/watchtower:i386-latest \ containrrr/watchtower:armhf-latest \ - containrrr/watchtower:arm64v8-latest \ + containrrr/watchtower:arm64v8-latest + docker manifest create \ ghcr.io/containrrr/watchtower:latest \ ghcr.io/containrrr/watchtower:amd64-latest \ ghcr.io/containrrr/watchtower:i386-latest \ From 692f66bace8d8ba6bc19c9e0b325432ae1afa56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 24 Apr 2021 19:43:17 +0200 Subject: [PATCH 96/98] fix goreleaser tags for ghcr.io --- goreleaser.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/goreleaser.yml b/goreleaser.yml index 9b738e6..3f5e95d 100644 --- a/goreleaser.yml +++ b/goreleaser.yml @@ -38,6 +38,8 @@ dockers: image_templates: - containrrr/watchtower:amd64-{{ .Version }} - containrrr/watchtower:amd64-latest + - ghcr.io/containrrr/watchtower:amd64-{{ .Version }} + - ghcr.io/containrrr/watchtower:amd64-latest binaries: - watchtower - @@ -50,6 +52,8 @@ dockers: image_templates: - containrrr/watchtower:i386-{{ .Version }} - containrrr/watchtower:i386-latest + - ghcr.io/containrrr/watchtower:i386-{{ .Version }} + - ghcr.io/containrrr/watchtower:i386-latest binaries: - watchtower - @@ -62,6 +66,8 @@ dockers: image_templates: - containrrr/watchtower:armhf-{{ .Version }} - containrrr/watchtower:armhf-latest + - ghcr.io/containrrr/watchtower:armhf-{{ .Version }} + - ghcr.io/containrrr/watchtower:armhf-latest binaries: - watchtower - @@ -74,5 +80,7 @@ dockers: image_templates: - containrrr/watchtower:arm64v8-{{ .Version }} - containrrr/watchtower:arm64v8-latest + - ghcr.io/containrrr/watchtower:arm64v8-{{ .Version }} + - ghcr.io/containrrr/watchtower:arm64v8-latest binaries: - watchtower From ef4873b16611b11b2c6a0293e7a96a3a578c784a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 24 Apr 2021 20:11:36 +0200 Subject: [PATCH 97/98] fix goreleaser GHCR login --- .github/workflows/release.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0393f0a..99e30fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,6 +76,12 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GHCR + uses: docker/login-action@v1 + with: + username: ${{ secrets.BOT_USERNAME }} + password: ${{ secrets.BOT_GHCR_PAT }} + registry: ghcr.io - name: Build uses: goreleaser/goreleaser-action@v2 with: @@ -173,8 +179,11 @@ jobs: docker manifest push containrrr/watchtower:$(echo $TAG | sed 's/^v*//') && \ docker manifest push containrrr/watchtower:latest - name: Push manifests to GitHub Container Registry + env: + DOCKER_USER: ${{ secrets.BOT_USERNAME }} + DOCKER_TOKEN: ${{ secrets.BOT_GHCR_PAT }} run: | - echo "$BOT_GHCR_PAT" | docker login -u $BOT_USERNAME --password-stdin && \ + docker login -u $DOCKER_USER -p $DOCKER_TOKEN ghcr.io && \ docker manifest push ghcr.io/containrrr/watchtower:$(echo $TAG | sed 's/^v*//') && \ docker manifest push ghcr.io/containrrr/watchtower:latest From cc3ff5a58803e327dc811d06e9fe3fe5ce512704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 24 Apr 2021 20:33:13 +0200 Subject: [PATCH 98/98] fix more spelling mistakes --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99e30fb..871ac9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -110,7 +110,7 @@ jobs: ghcr.io/containrrr/watchtower:arm64v8-$DH_TAG - name: Annotate manifest for version run: | - for REPO in '' ghrc.io/ ; do + for REPO in '' ghcr.io/ ; do docker manifest annotate \ ${REPO}containrrr/watchtower:$(echo $TAG | sed 's/^v*//') \ @@ -148,7 +148,7 @@ jobs: ghcr.io/containrrr/watchtower:arm64v8-latest - name: Annotate manifest for latest run: | - for REPO in '' ghrc.io/ ; do + for REPO in '' ghcr.io/ ; do docker manifest annotate \ ${REPO}containrrr/watchtower:latest \