Repository: containrrr/watchtower (go)

Issue:
Add "Finished" event to log after One-time run or otherwise
**Is your feature request related to a problem? Please describe.**
When using watchtower, you are informed by the log file that is  fed to docker (e.g. docker logs watchtower) that the 
1) Process has started - Docker reports that it is Checking all containers.
2) Container upgrades have been found.
3) Containers have been serviced.

**Describe the solution you'd like**
4) Process has completed - Docker reports it is done Checking all containers.

**Describe alternatives you've considered**
N/A

**Additional context**
N/A


Pull request:
feat: improve session result logging
- logs the session result as an info level message without notification instead of debug
- does not log that no notification was sent if there are no notifications enabled

Fixes #1122 

<!--

Thank you for contributing to the watchtower project! 🙏

We truly appreciate all the contributions we get from the community.

To make your PR experience as smooth as possible, make sure that you
include the following in your PR:

- What your PR contributes
- Which issues it solves (preferrably using auto closing instructions like "closes #123".
- Tests that verify the code your contributing
- Updates to the documentation

Thank you again! ✨

-->


Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index b3fdd3a..f7f0cb8 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -785,6 +785,15 @@
       "contributions": [
         "doc"
       ]
+    },
+    {
+      "login": "hypnoglow",
+      "name": "Igor Zibarev",
+      "avatar_url": "https://avatars.githubusercontent.com/u/4853075?v=4",
+      "profile": "https://github.com/hypnoglow",
+      "contributions": [
+        "code"
+      ]
     }
   ],
   "contributorsPerLine": 7,
diff --git a/README.md b/README.md
index ec8524c..479a7f7 100644
--- a/README.md
+++ b/README.md
@@ -25,9 +25,9 @@ With watchtower you can update the running version of your containerized app sim
 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:
 
 ```
-$ docker run -d \
+$ docker run --detach \
     --name watchtower \
-    -v /var/run/docker.sock:/var/run/docker.sock \
+    --volume /var/run/docker.sock:/var/run/docker.sock \
     containrrr/watchtower
 ```
 
@@ -150,6 +150,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
     <td align="center"><a href="https://quan.io"><img src="https://avatars.githubusercontent.com/u/3526705?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dan Quan</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=djquan" title="Documentation">📖</a></td>
     <td align="center"><a href="https://github.com/modem7"><img src="https://avatars.githubusercontent.com/u/4349962?v=4?s=100" width="100px;" alt=""/><br /><sub><b>modem7</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=modem7" title="Documentation">📖</a></td>
   </tr>
+  <tr>
+    <td align="center"><a href="https://github.com/hypnoglow"><img src="https://avatars.githubusercontent.com/u/4853075?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Igor Zibarev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hypnoglow" title="Code">💻</a></td>
+  </tr>
 </table>
 
 <!-- markdownlint-restore -->
diff --git a/cmd/root.go b/cmd/root.go
index 47f7133..ff1cac3 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -364,7 +364,10 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
 	}
 	notifier.SendNotification(result)
 	metricResults := metrics.NewMetric(result)
-	log.Debugf("Session done: %v scanned, %v updated, %v failed",
-		metricResults.Scanned, metricResults.Updated, metricResults.Failed)
+	notifications.LocalLog.WithFields(log.Fields{
+		"Scanned": metricResults.Scanned,
+		"Updated": metricResults.Updated,
+		"Failed":  metricResults.Failed,
+	}).Info("Session done")
 	return metricResults
 }
diff --git a/go.sum b/go.sum
index 71ea26f..33e6c19 100644
--- a/go.sum
+++ b/go.sum
@@ -54,8 +54,6 @@ github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg3
 github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=
 github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00=
 github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600=
-github.com/Microsoft/hcsshim v0.8.18 h1:cYnKADiM1869gvBpos3YCteeT6sZLB48lB5dmMMs8Tg=
-github.com/Microsoft/hcsshim v0.8.18/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=
 github.com/Microsoft/hcsshim v0.8.21 h1:btRfUDThBE5IKcvI8O8jOiIkujUsAMBSRsYDYmEi6oM=
 github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=
 github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=
@@ -148,8 +146,6 @@ github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo
 github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI=
 github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=
 github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g=
-github.com/containerd/containerd v1.5.5 h1:q1gxsZsGZ8ddVe98yO6pR21b5xQSMiR61lD0W96pgQo=
-github.com/containerd/containerd v1.5.5/go.mod h1:oSTh0QpT1w6jYcGmbiSbxv9OSQYaa88mPyWIuU79zyo=
 github.com/containerd/containerd v1.5.7 h1:rQyoYtj4KddB3bxG6SAqd4+08gePNyJjRqvOIfV3rkM=
 github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=
 github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
@@ -553,8 +549,6 @@ github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59P
 github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
 github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
 github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0=
-github.com/opencontainers/runc v1.0.1 h1:G18PGckGdAm3yVQRWDVQ1rLSLntiniKJ0cNRT2Tm5gs=
-github.com/opencontainers/runc v1.0.1/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
 github.com/opencontainers/runc v1.0.2 h1:opHZMaswlyxz1OuGpBE53Dwe4/xF7EZTY0A2L/FpCOg=
 github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
 github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
diff --git a/internal/flags/flags.go b/internal/flags/flags.go
index 8dd128e..a02dbd7 100644
--- a/internal/flags/flags.go
+++ b/internal/flags/flags.go
@@ -31,31 +31,31 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
 		"interval",
 		"i",
 		viper.GetInt("WATCHTOWER_POLL_INTERVAL"),
-		"poll interval (in seconds)")
+		"Poll interval (in seconds)")
 
 	flags.StringP(
 		"schedule",
 		"s",
 		viper.GetString("WATCHTOWER_SCHEDULE"),
-		"the cron expression which defines when to update")
+		"The cron expression which defines when to update")
 
 	flags.DurationP(
 		"stop-timeout",
 		"t",
 		viper.GetDuration("WATCHTOWER_TIMEOUT"),
-		"timeout before a container is forcefully stopped")
+		"Timeout before a container is forcefully stopped")
 
 	flags.BoolP(
 		"no-pull",
 		"",
 		viper.GetBool("WATCHTOWER_NO_PULL"),
-		"do not pull any new images")
+		"Do not pull any new images")
 
 	flags.BoolP(
 		"no-restart",
 		"",
 		viper.GetBool("WATCHTOWER_NO_RESTART"),
-		"do not restart any containers")
+		"Do not restart any containers")
 
 	flags.BoolP(
 		"no-startup-message",
@@ -67,31 +67,31 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
 		"cleanup",
 		"c",
 		viper.GetBool("WATCHTOWER_CLEANUP"),
-		"remove previously used images after updating")
+		"Remove previously used images after updating")
 
 	flags.BoolP(
 		"remove-volumes",
 		"",
 		viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"),
-		"remove attached volumes before updating")
+		"Remove attached volumes before updating")
 
 	flags.BoolP(
 		"label-enable",
 		"e",
 		viper.GetBool("WATCHTOWER_LABEL_ENABLE"),
-		"watch containers where the com.centurylinklabs.watchtower.enable label is true")
+		"Watch containers where the com.centurylinklabs.watchtower.enable label is true")
 
 	flags.BoolP(
 		"debug",
 		"d",
 		viper.GetBool("WATCHTOWER_DEBUG"),
-		"enable debug mode with verbose logging")
+		"Enable debug mode with verbose logging")
 
 	flags.BoolP(
 		"trace",
 		"",
 		viper.GetBool("WATCHTOWER_TRACE"),
-		"enable trace mode with very verbose logging - caution, exposes credentials")
+		"Enable trace mode with very verbose logging - caution, exposes credentials")
 
 	flags.BoolP(
 		"monitor-only",
@@ -177,7 +177,7 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) {
 		"notifications",
 		"n",
 		viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"),
-		" notification types to send (valid: email, slack, msteams, gotify, shoutrrr)")
+		" Notification types to send (valid: email, slack, msteams, gotify, shoutrrr)")
 
 	flags.String(
 		"notifications-level",
diff --git a/pkg/api/api.go b/pkg/api/api.go
index b2279e1..756db90 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -25,9 +25,12 @@ func New(token string) *API {
 // 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.Tracef("Invalid token \"%s\"", r.Header.Get("Authorization"))
-			log.Tracef("Expected token to be \"%s\"", api.Token)
+		auth := r.Header.Get("Authorization")
+		want := fmt.Sprintf("Bearer %s", api.Token)
+		if auth != want {
+			log.Tracef("Invalid Authorization header \"%s\"", auth)
+			log.Tracef("Expected Authorization header to be \"%s\"", want)
+			w.WriteHeader(http.StatusUnauthorized)
 			return
 		}
 		log.Debug("Valid token found.")
diff --git a/pkg/container/container.go b/pkg/container/container.go
index 4ea3e9f..c55394a 100644
--- a/pkg/container/container.go
+++ b/pkg/container/container.go
@@ -285,7 +285,7 @@ func (c Container) VerifyConfiguration() error {
 
 	containerInfo := c.ContainerInfo()
 	if containerInfo == nil {
-		return errorInvalidConfig
+		return errorNoContainerInfo
 	}
 
 	containerConfig := containerInfo.Config
diff --git a/pkg/container/errors.go b/pkg/container/errors.go
index b927220..1937430 100644
--- a/pkg/container/errors.go
+++ b/pkg/container/errors.go
@@ -3,5 +3,6 @@ package container
 import "errors"
 
 var errorNoImageInfo = errors.New("no available image info")
+var errorNoContainerInfo = errors.New("no available container info")
 var errorNoExposedPorts = errors.New("exposed ports does not match port bindings")
 var errorInvalidConfig = errors.New("container configuration missing or invalid")
diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go
index ac293fb..1611a04 100644
--- a/pkg/notifications/shoutrrr.go
+++ b/pkg/notifications/shoutrrr.go
@@ -152,7 +152,7 @@ func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report t.Report
 		go func() {
 			if err != nil {
 				LocalLog.WithError(err).Fatal("Notification template error")
-			} else {
+			} else if len(n.Urls) > 1 {
 				LocalLog.Info("Skipping notification due to empty message")
 			}
 		}()


Test patch:
diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go
new file mode 100644
index 0000000..4e9110b
--- /dev/null
+++ b/pkg/api/api_test.go
@@ -0,0 +1,65 @@
+package api
+
+import (
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+)
+
+const (
+	token  = "123123123"
+)
+
+func TestAPI(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "API Suite")
+}
+
+var _ = Describe("API", func() {
+	api := New(token)
+
+	Describe("RequireToken middleware", func() {
+		It("should return 401 Unauthorized when token is not provided", func() {
+			handlerFunc := api.RequireToken(testHandler)
+
+			rec := httptest.NewRecorder()
+			req := httptest.NewRequest("GET", "/hello", nil)
+
+			handlerFunc(rec, req)
+
+			Expect(rec.Code).To(Equal(http.StatusUnauthorized))
+		})
+
+		It("should return 401 Unauthorized when token is invalid", func() {
+			handlerFunc := api.RequireToken(testHandler)
+
+			rec := httptest.NewRecorder()
+			req := httptest.NewRequest("GET", "/hello", nil)
+			req.Header.Set("Authorization", "Bearer 123")
+
+			handlerFunc(rec, req)
+
+			Expect(rec.Code).To(Equal(http.StatusUnauthorized))
+		})
+
+		It("should return 200 OK when token is valid", func() {
+			handlerFunc := api.RequireToken(testHandler)
+
+			rec := httptest.NewRecorder()
+			req := httptest.NewRequest("GET", "/hello", nil)
+			req.Header.Set("Authorization", "Bearer " + token)
+
+			handlerFunc(rec, req)
+
+			Expect(rec.Code).To(Equal(http.StatusOK))
+		})
+	})
+})
+
+func testHandler(w http.ResponseWriter, req *http.Request) {
+	_, _ = io.WriteString(w, "Hello!")
+}
diff --git a/pkg/container/container_test.go b/pkg/container/container_test.go
index 5204f7d..08cba7a 100644
--- a/pkg/container/container_test.go
+++ b/pkg/container/container_test.go
@@ -23,7 +23,7 @@ var _ = Describe("the container", func() {
 				c := mockContainerWithPortBindings()
 				c.containerInfo = nil
 				err := c.VerifyConfiguration()
-				Expect(err).To(Equal(errorInvalidConfig))
+				Expect(err).To(Equal(errorNoContainerInfo))
 			})
 		})
 		When("verifying a container with no config", func() {