Repository: containrrr/watchtower (go)

Issue:
Watchtower API Mode always returns 200 OK status code
**Describe the bug**
When using watchtower in API mode sending any request to `v1/update` endpoint it will always return status `200 OK`.

**To Reproduce**
1. Setup watchtower in API mode
2. Make a request to `http://ip:port/v1/update` with missing/invalid `Authorization` header
3. Look at response code

**Expected behavior**
The API returns `401 Unauthorized` whenever the `Authorization` header is missing/invalid

**Screenshots**
![image](https://user-images.githubusercontent.com/29180158/137642693-1810038c-d639-40de-b8a4-78a3a023b657.png)

**Environment**
Unrelated

<details>
 <summary><b> Logs from running watchtower with the <code>--debug</code> option </b></summary>

```
"Updates triggered by HTTP API request." only triggered when providing a valid token
```

</details>

**Additional context**
Would be nice to have meaningful status codes instead of just silently failing

Pull request description:
fix(api): return appropriate status for unauthorized requests
Fixes #1095

Also added the test for both cases 😊


Patch:
diff --git a/README.md b/README.md
index ec8524c..25aeb65 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
 ```
 
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.")


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/client_test.go b/pkg/container/client_test.go
index d9bf86f..2f0157d 100644
--- a/pkg/container/client_test.go
+++ b/pkg/container/client_test.go
@@ -22,14 +22,14 @@ import (
 var _ = Describe("the client", func() {
 	var docker *cli.Client
 	var mockServer *ghttp.Server
-	BeforeSuite(func() {
+	BeforeEach(func() {
 		mockServer = ghttp.NewServer()
 		docker, _ = cli.NewClientWithOpts(
 			cli.WithHost(mockServer.URL()),
 			cli.WithHTTPClient(mockServer.HTTPTestServer.Client()))
 	})
 	AfterEach(func() {
-		mockServer.Reset()
+		mockServer.Close()
 	})
 	Describe("WarnOnHeadPullFailed", func() {
 		containerUnknown := *mockContainerWithImageName("unknown.repo/prefix/imagename:latest")