Repository: containrrr/watchtower
Pull request #545 (2020-05-12 20:52:18)

Allows flags containing sensitive stuff to be passed as files
Potential fix for #532.

I've built in a check to see if the following sensitive options are passed to Watchtower as a file instead of a plaintext secret:
* notification-email-server-password
* notification-slack-hook-url
* notification-msteams-hook
* notification-gotify-token
* ~notification-url~

If this is the case, the runtime value is converted to the contents of the file instead. As the potential for an unexpected conflict is really low (aka a plaintext password that's actually the same as a fully-qualified existing file on the system), it seemed a better option than adding 5 extra flags.

@simskij if you hate this idea and/or have better suggestions, let me know!

tammert: @simskij thanks for the feedback! I've added tests and fixed the linting issue. While creating the tests I found out that `--notification-url` is actually an array (would not have expected that from the name, maybe `--notications-urls` would be better?). Because of that, I couldn't really get an elegant solution in place for passing it as a file, so I've removed it from that functionality for now (and updated docs).

Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 3b8a285..ec0124a 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -513,7 +513,8 @@
       "avatar_url": "https://avatars2.githubusercontent.com/u/807383?v=4",
       "profile": "https://piksel.se",
       "contributions": [
-        "doc"
+        "doc",
+        "code"
       ]
     },
     {
@@ -582,6 +583,34 @@
       "contributions": [
         "doc"
       ]
+    },
+    {
+      "login": "andrewjmetzger",
+      "name": "Andrew Metzger",
+      "avatar_url": "https://avatars3.githubusercontent.com/u/590246?v=4",
+      "profile": "https://mtz.gr",
+      "contributions": [
+        "bug",
+        "example"
+      ]
+    },
+    {
+      "login": "pgrimaud",
+      "name": "Pierre Grimaud",
+      "avatar_url": "https://avatars1.githubusercontent.com/u/1866496?v=4",
+      "profile": "https://github.com/pgrimaud",
+      "contributions": [
+        "doc"
+      ]
+    },
+    {
+      "login": "mattdoran",
+      "name": "Matt Doran",
+      "avatar_url": "https://avatars0.githubusercontent.com/u/577779?v=4",
+      "profile": "https://github.com/mattdoran",
+      "contributions": [
+        "doc"
+      ]
     }
   ],
   "contributorsPerLine": 7,
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..01cc777
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+  - name: Ask a question
+    url: https://github.com/containrrr/watchtower/discussions
+    about: Ask questions and discuss with other community members
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
deleted file mode 100644
index bce7228..0000000
--- a/.github/ISSUE_TEMPLATE/question.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Question
-about: Questions regarding usage or specific use cases.
-title: ''
-labels: 'Priority: Medium, Status: Available, Type: Question'
-assignees: ''
-
----
-
-
diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml
index 7034c9e..899bb52 100644
--- a/.github/workflows/greetings.yml
+++ b/.github/workflows/greetings.yml
@@ -1,6 +1,6 @@
 name: Greetings
 
-on: [pull_request, issues]
+on: [issues]
 
 jobs:
   greeting:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 82b1b1b..0202384 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -3,7 +3,7 @@ To contribute code changes to this project you will need the following developme
  * [Go](https://golang.org/doc/install)
  * [Docker](https://docs.docker.com/engine/installation/)
  
-As watchtower utilizes go modules for vendor locking, you'll need atleast Go 1.11.
+As watchtower utilizes go modules for vendor locking, you'll need at least Go 1.11.
 You can check your current version of the go language as follows:
 ```bash
   ~ $ go version
diff --git a/README.md b/README.md
index 8df662c..2a8903d 100644
--- a/README.md
+++ b/README.md
@@ -139,7 +139,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
     <td align="center"><a href="https://github.com/mbrandau"><img src="https://avatars3.githubusercontent.com/u/12972798?v=4" width="100px;" alt=""/><br /><sub><b>Maximilian Brandau</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=mbrandau" title="Tests">⚠️</a></td>
     <td align="center"><a href="https://github.com/aneisch"><img src="https://avatars1.githubusercontent.com/u/6991461?v=4" width="100px;" alt=""/><br /><sub><b>Andrew</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=aneisch" title="Documentation">📖</a></td>
     <td align="center"><a href="https://github.com/sixcorners"><img src="https://avatars0.githubusercontent.com/u/585501?v=4" width="100px;" alt=""/><br /><sub><b>sixcorners</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixcorners" title="Documentation">📖</a></td>
-    <td align="center"><a href="https://piksel.se"><img src="https://avatars2.githubusercontent.com/u/807383?v=4" width="100px;" alt=""/><br /><sub><b>nils måsén</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Documentation">📖</a></td>
+    <td align="center"><a href="https://piksel.se"><img src="https://avatars2.githubusercontent.com/u/807383?v=4" width="100px;" alt=""/><br /><sub><b>nils måsén</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Documentation">📖</a> <a href="https://github.com/containrrr/watchtower/commits?author=piksel" title="Code">💻</a></td>
     <td align="center"><a href="https://arnested.dk"><img src="https://avatars2.githubusercontent.com/u/190005?v=4" width="100px;" alt=""/><br /><sub><b>Arne Jørgensen</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=arnested" title="Tests">⚠️</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Aarnested" title="Reviewed Pull Requests">👀</a></td>
   </tr>
   <tr>
@@ -149,6 +149,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
     <td align="center"><a href="http://www.teqneers.de"><img src="https://avatars0.githubusercontent.com/u/788989?v=4" width="100px;" alt=""/><br /><sub><b>Oliver Mueller</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=ogmueller" title="Documentation">📖</a></td>
     <td align="center"><a href="https://github.com/tammert"><img src="https://avatars0.githubusercontent.com/u/8885250?v=4" width="100px;" alt=""/><br /><sub><b>Sebastiaan Tammer</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=tammert" title="Code">💻</a></td>
     <td align="center"><a href="https://github.com/Miosame"><img src="https://avatars1.githubusercontent.com/u/8201077?v=4" width="100px;" alt=""/><br /><sub><b>miosame</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=miosame" title="Documentation">📖</a></td>
+    <td align="center"><a href="https://mtz.gr"><img src="https://avatars3.githubusercontent.com/u/590246?v=4" width="100px;" alt=""/><br /><sub><b>Andrew Metzger</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/issues?q=author%3Aandrewjmetzger" title="Bug reports">🐛</a> <a href="#example-andrewjmetzger" title="Examples">💡</a></td>
+  </tr>
+  <tr>
+    <td align="center"><a href="https://github.com/pgrimaud"><img src="https://avatars1.githubusercontent.com/u/1866496?v=4" width="100px;" alt=""/><br /><sub><b>Pierre Grimaud</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pgrimaud" title="Documentation">📖</a></td>
+    <td align="center"><a href="https://github.com/mattdoran"><img src="https://avatars0.githubusercontent.com/u/577779?v=4" width="100px;" alt=""/><br /><sub><b>Matt Doran</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mattdoran" title="Documentation">📖</a></td>
   </tr>
 </table>
 
diff --git a/cmd/root.go b/cmd/root.go
index 5e5c4e0..bef694c 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -81,6 +81,7 @@ func PreRun(cmd *cobra.Command, args []string) {
 		scheduleSpec = "@every " + strconv.Itoa(interval) + "s"
 	}
 
+	flags.GetSecretsFromFiles(cmd)
 	cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd)
 
 	if timeout < 0 {
diff --git a/docs/notifications.md b/docs/notifications.md
index 005193c..afa23bd 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -33,7 +33,7 @@ To receive notifications by email, the following command-line options, or their
 - `--notification-email-server-tls-skip-verify` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY`): Do not verify the TLS certificate of the mail server. This should be used only for testing.
 - `--notification-email-server-port` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT`): The port used to connect to the SMTP server to send e-mails through. Defaults to `25`.
 - `--notification-email-server-user` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER`): The username to authenticate with the SMTP server with.
-- `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with.
+- `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with. Can also reference a file, in which case the contents of the file are used.
 - `--notification-email-delay` (env. `WATCHTOWER_NOTIFICATION_EMAIL_DELAY`): Delay before sending notifications expressed in seconds.
 - `--notification-email-subjecttag` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG`): Prefix to include in the subject tag. Useful when running multiple watchtowers.
 
@@ -47,6 +47,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_USER=fromaddress@gmail.com \
   -e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=app_password \
   -e WATCHTOWER_NOTIFICATION_EMAIL_DELAY=2 \
@@ -110,7 +111,7 @@ If watchtower is monitoring the same Docker daemon under which the watchtower co
 
 To receive notifications in Slack, add `slack` to the `--notifications` option or the `WATCHTOWER_NOTIFICATIONS` environment variable.
 
-Additionally, you should set the Slack webhook URL using the `--notification-slack-hook-url` option or the `WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL` environment variable.
+Additionally, you should set the Slack webhook URL using the `--notification-slack-hook-url` option or the `WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL` environment variable. This option can also reference a file, in which case the contents of the file are used.
 
 By default, watchtower will send messages under the name `watchtower`, you can customize this string through the `--notification-slack-identifier` option or the `WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER` environment variable.
 
@@ -139,7 +140,7 @@ docker run -d \
 
 To receive notifications in MSTeams channel, add `msteams` to the `--notifications` option or the `WATCHTOWER_NOTIFICATIONS` environment variable.
 
-Additionally, you should set the MSTeams webhook URL using the `--notification-msteams-hook` option or the `WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL` environment variable.
+Additionally, you should set the MSTeams webhook URL using the `--notification-msteams-hook` option or the `WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL` environment variable. This option can also reference a file, in which case the contents of the file are used.
 
 MSTeams notifier could send keys/values filled by `log.WithField` or `log.WithFields` as MSTeams message facts. To enable this feature add `--notification-msteams-data` flag or set `WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA=true` environment variable.
 
@@ -159,7 +160,6 @@ docker run -d \
 
 To push a notification to your Gotify instance, register a Gotify app and specify the Gotify URL and app token:
 
-
 ```bash
 docker run -d \
   --name watchtower \
@@ -170,6 +170,8 @@ docker run -d \
   containrrr/watchtower
 ```
 
+`-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`. 
 
 ### [containrrr/shoutrrr](https://github.com/containrrr/shoutrrr)
@@ -199,4 +201,4 @@ docker run -d \
   -e WATCHTOWER_NOTIFICATION_URL="discord://token@channel slack://watchtower@token-a/token-b/token-c" \
   -e WATCHTOWER_NOTIFICATION_TEMPLATE="{{range .}}{{.Time.Format \"2006-01-02 15:04:05\"}} ({{.Level}}): {{.Message}}{{println}}{{end}}" \
   containrrr/watchtower
-```
\ No newline at end of file
+```
diff --git a/docs/private-registries.md b/docs/private-registries.md
index 3606c2a..dab0c46 100644
--- a/docs/private-registries.md
+++ b/docs/private-registries.md
@@ -133,4 +133,4 @@ and for `<PATH_TO_HOME_DIR>/.docker/config.json`:
   }
 ```
 
-*Note:* `osxkeychain` can be changed to your prefered credentials helper.
+*Note:* `osxkeychain` can be changed to your preferred credentials helper.
diff --git a/docs/usage-overview.md b/docs/usage-overview.md
index f74a9a7..2fa8fe7 100644
--- a/docs/usage-overview.md
+++ b/docs/usage-overview.md
@@ -37,6 +37,8 @@ docker run -d \
   containrrr/watchtower container_to_watch --debug
 ```
 
+> 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.
 
 ```json
diff --git a/go.mod b/go.mod
index 9d0138d..300eb31 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,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-20200404203330-157bd996ea13
+	github.com/containrrr/shoutrrr v0.0.0-20200601144753-78bb9685bc2f
 	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
@@ -48,6 +48,7 @@ require (
 	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/stretchr/testify v1.3.0
 	github.com/theupdateframework/notary v0.6.1 // indirect
diff --git a/go.sum b/go.sum
index ceec403..7285415 100644
--- a/go.sum
+++ b/go.sum
@@ -43,8 +43,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.0.0-20200404203330-157bd996ea13 h1:5KIwcRac24xehTL/xrhXNIiI9JnV2Mbfl52OgGbloIM=
-github.com/containrrr/shoutrrr v0.0.0-20200404203330-157bd996ea13/go.mod h1:eotQeC9bHbsf9eMUnXOU/y5bskegseWNB4PwmxRO7Wc=
+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/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/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
diff --git a/internal/flags/flags.go b/internal/flags/flags.go
index ee1d8e3..8d8d2ab 100644
--- a/internal/flags/flags.go
+++ b/internal/flags/flags.go
@@ -1,11 +1,14 @@
 package flags
 
 import (
+	"io/ioutil"
 	"os"
+	"strings"
 	"time"
 
 	log "github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 	"github.com/spf13/viper"
 )
 
@@ -366,3 +369,45 @@ func setEnvOptBool(env string, opt bool) error {
 	}
 	return nil
 }
+
+// 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()
+
+	secrets := []string{
+		"notification-email-server-password",
+		"notification-slack-hook-url",
+		"notification-msteams-hook",
+		"notification-gotify-token",
+	}
+	for _, secret := range secrets {
+		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(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)
+		}
+		err = flags.Set(secret, strings.TrimSpace(string(file)))
+		if err != nil {
+			log.Error(err)
+		}
+	}
+}
+
+func isFile(s string) bool {
+	_, err := os.Stat(s)
+	if os.IsNotExist(err) {
+		return false
+	}
+	return true
+}
diff --git a/pkg/container/client.go b/pkg/container/client.go
index a74bfba..5c6e5d6 100644
--- a/pkg/container/client.go
+++ b/pkg/container/client.go
@@ -119,12 +119,7 @@ func (client dockerClient) GetContainer(containerID string) (Container, error) {
 		return Container{}, err
 	}
 
-	imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image)
-	if err != nil {
-		return Container{}, err
-	}
-
-	container := Container{containerInfo: &containerInfo, imageInfo: &imageInfo}
+	container := Container{containerInfo: &containerInfo}
 	return container, nil
 }
 
@@ -244,7 +239,7 @@ func (client dockerClient) IsContainerStale(container Container) (bool, error) {
 }
 
 func (client dockerClient) HasNewImage(ctx context.Context, container Container) (bool, error) {
-	oldImageID := container.imageInfo.ID
+	oldImageID := container.containerInfo.ContainerJSONBase.Image
 	imageName := container.ImageName()
 
 	newImageInfo, _, err := client.api.ImageInspectWithRaw(ctx, imageName)
diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go
index 04fbf8b..8cf3d7c 100644
--- a/pkg/notifications/shoutrrr.go
+++ b/pkg/notifications/shoutrrr.go
@@ -30,7 +30,10 @@ func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Noti
 	flags := c.PersistentFlags()
 
 	urls, _ := flags.GetStringArray("notification-url")
-	r, _ := shoutrrr.CreateSender(urls...)
+	r, err := shoutrrr.CreateSender(urls...)
+	if err != nil {
+		log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error())
+	}
 
 	n := &shoutrrrTypeNotifier{
 		Urls:      urls,
@@ -46,14 +49,18 @@ func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Noti
 
 func (e *shoutrrrTypeNotifier) buildMessage(entries []*log.Entry) string {
 	var body bytes.Buffer
-	e.template.Execute(&body, entries)
+	if err := e.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) {
-	// Do the sending in a separate goroutine so we don't block the main process.
+
 	msg := e.buildMessage(entries)
+
+	// Do the sending in a separate goroutine so we don't block the main process.
 	go func() {
 		errs := e.Router.Send(msg, nil)
 


Test patch:
diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go
index 42b5ce5..b659a96 100644
--- a/internal/flags/flags_test.go
+++ b/internal/flags/flags_test.go
@@ -1,6 +1,7 @@
 package flags
 
 import (
+	"io/ioutil"
 	"os"
 	"testing"
 
@@ -39,3 +40,42 @@ func TestEnvConfig_Custom(t *testing.T) {
 	// Re-enable this test when we've moved to github actions.
 	// assert.Equal(t, "1.99", os.Getenv("DOCKER_API_VERSION"))
 }
+
+func TestGetSecretsFromFilesWithString(t *testing.T) {
+	value := "supersecretstring"
+
+	err := os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", value)
+	require.NoError(t, err)
+
+	testGetSecretsFromFiles(t, "notification-email-server-password", value)
+}
+
+func TestGetSecretsFromFilesWithFile(t *testing.T) {
+	value := "megasecretstring"
+
+	// Create the temporary file which will contain a secret.
+	file, err := ioutil.TempFile(os.TempDir(), "watchtower-")
+	require.NoError(t, err)
+	defer os.Remove(file.Name()) // Make sure to remove the temporary file later.
+
+	// Write the secret to the temporary file.
+	secret := []byte(value)
+	_, err = file.Write(secret)
+	require.NoError(t, err)
+
+	err = os.Setenv("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD", file.Name())
+	require.NoError(t, err)
+
+	testGetSecretsFromFiles(t, "notification-email-server-password", value)
+}
+
+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)
+
+	assert.Equal(t, expected, value)
+}
diff --git a/scripts/lifecycle-tests.sh b/scripts/lifecycle-tests.sh
index dd41823..9b2dbd7 100755
--- a/scripts/lifecycle-tests.sh
+++ b/scripts/lifecycle-tests.sh
@@ -89,7 +89,7 @@ builddocker
 # Run container
 docker run -d -p 0.0.0.0:8888:8888 --name $CONTAINER $IMAGE:latest >> /dev/null
 sleep 1
-echo "Container $CONTAINER is runnning"
+echo "Container $CONTAINER is running"
 
 # Test default value
 RESP=$(curl -s http://localhost:8888)
@@ -126,7 +126,7 @@ docker run -d -p 0.0.0.0:8888:8888 \
 	--label=com.centurylinklabs.watchtower.lifecycle.post-update="echo container > /opt/test/value.txt" \
 	--name $CONTAINER $IMAGE:latest >> /dev/null
 sleep 1
-echo "Container $CONTAINER is runnning"
+echo "Container $CONTAINER is running"
 
 # Test default value
 RESP=$(curl -s http://localhost:8888)
@@ -170,7 +170,7 @@ docker run -d -p 0.0.0.0:8989:8888 \
 	--link $CONTAINER \
 	--name $LINKED_CONTAINER $LINKED_IMAGE:latest >> /dev/null
 sleep 1
-echo "Container $CONTAINER and $LINKED_CONTAINER are runnning"
+echo "Container $CONTAINER and $LINKED_CONTAINER are running"
 
 # Test default value
 RESP=$(curl -s http://localhost:8888)