Repository: containrrr/watchtower (go)

Issue:
Make watchtower skip update if pre-update lifecycle hook exits with a non-zero exit code
> Yeah, to be totally honest, I already thought we did this! 😅  This is the intended behavior, so we'll need to fix that. Thanks for reporting!

_Originally posted by @simskij in https://github.com/containrrr/watchtower/discussions/647#discussioncomment-81488_

Pull request:
Pre-update lifecycle hook
Will close #649 

Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 56cf25a..58246fd 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -758,6 +758,15 @@
       "contributions": [
         "code"
       ]
+    },
+    {
+      "login": "zenjabba",
+      "name": "zenjabba",
+      "avatar_url": "https://avatars.githubusercontent.com/u/679864?v=4",
+      "profile": "https://github.com/zenjabba",
+      "contributions": [
+        "doc"
+      ]
     }
   ],
   "contributorsPerLine": 7,
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 24bfe13..80eb3e3 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -35,6 +35,7 @@ jobs:
         platform:
           - macos-latest
           - windows-latest
+          - ubuntu-latest
     runs-on: ${{ matrix.platform }}
     steps:
       - name: Checkout
@@ -52,26 +53,6 @@ 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/README.md b/README.md
index be92875..8a18a58 100644
--- a/README.md
+++ b/README.md
@@ -145,6 +145,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
     <td align="center"><a href="https://indigo.re/"><img src="https://avatars.githubusercontent.com/u/2804645?v=4?s=100" width="100px;" alt=""/><br /><sub><b>hydrargyrum</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=hydrargyrum" title="Documentation">📖</a></td>
     <td align="center"><a href="https://reinout.vanrees.org"><img src="https://avatars.githubusercontent.com/u/121433?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Reinout van Rees</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=reinout" title="Documentation">📖</a></td>
     <td align="center"><a href="https://github.com/DasSkelett"><img src="https://avatars.githubusercontent.com/u/28812678?v=4?s=100" width="100px;" alt=""/><br /><sub><b>DasSkelett</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=DasSkelett" title="Code">💻</a></td>
+    <td align="center"><a href="https://github.com/zenjabba"><img src="https://avatars.githubusercontent.com/u/679864?v=4?s=100" width="100px;" alt=""/><br /><sub><b>zenjabba</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zenjabba" title="Documentation">📖</a></td>
   </tr>
 </table>
 
diff --git a/build.sh b/build.sh
index 47d2b5c..304786d 100644
--- a/build.sh
+++ b/build.sh
@@ -2,4 +2,4 @@
 
 VERSION=$(git describe --tags)
 echo "Building $VERSION..."
-go build -o watchtower -ldflags "-X github.com/containrrr/watchtower/cmd.version=$VERSION"
+go build -o watchtower -ldflags "-X github.com/containrrr/watchtower/internal/meta.Version=$VERSION"
diff --git a/cmd/root.go b/cmd/root.go
index e4c24b3..cf752ae 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -1,6 +1,7 @@
 package cmd
 
 import (
+	"github.com/containrrr/watchtower/internal/meta"
 	"math"
 	"os"
 	"os/signal"
@@ -39,7 +40,6 @@ var (
 	rollingRestart bool
 	scope          string
 	// Set on build using ldflags
-	version = "v0.0.0-unknown"
 )
 
 var rootCmd = NewRootCommand()
@@ -273,7 +273,7 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) {
 			notifs = "Using notifications: " + notifList
 		}
 
-		log.Info("Watchtower ", version, "\n", notifs, "\n", filtering, "\n", schedMessage)
+		log.Info("Watchtower ", meta.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")
 		}
diff --git a/docs/index.md b/docs/index.md
index fd1ca1f..d59b3b1 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -59,6 +59,7 @@ the following command:
     version: "3"
     services:
     watchtower:
-    image: containrrr/watchtower volumes:
+    image: containrrr/watchtower 
+    volumes:
     - /var/run/docker.sock:/var/run/docker.sock
-    ```
\ No newline at end of file
+    ```
diff --git a/docs/lifecycle-hooks.md b/docs/lifecycle-hooks.md
index 5c53d15..cab0485 100644
--- a/docs/lifecycle-hooks.md
+++ b/docs/lifecycle-hooks.md
@@ -4,6 +4,9 @@
     These are shell commands executed with `sh`, and therefore require the container to provide the `sh`
     executable.
 
+> **DO NOTE**: If the container is not running then lifecycle hooks can not run and therefore 
+> the update is executed without running any lifecycle hooks.
+
 It is possible to execute _pre/post\-check_ and _pre/post\-update_ commands
 **inside** every container updated by watchtower.
 
@@ -63,5 +66,5 @@ If the label value is explicitly set to `0`, the timeout will be disabled.
 ### Execution failure
 
 The failure of a command to execute, identified by an exit code different than
-0, will not prevent watchtower from updating the container. Only an error
+0 or 75 (EX_TEMPFAIL), will not prevent watchtower from updating the container. Only an error
 log statement containing the exit code will be reported.
diff --git a/docs/notifications.md b/docs/notifications.md
index 5a5feb2..f783866 100644
--- a/docs/notifications.md
+++ b/docs/notifications.md
@@ -25,6 +25,7 @@ comma-separated list of values to the `--notifications` option
 ## 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`.
+- `--notifications-hostname` (env. `WATCHTOWER_NOTIFICATIONS_HOSTNAME`): Custom hostname specified in subject/title. Useful to override the operating system hostname.
 - 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
@@ -112,8 +113,6 @@ networks:
 
 ### Slack
 
-If watchtower is monitoring the same Docker daemon under which the watchtower container itself is running (i.e. if you volume-mounted _/var/run/docker.sock_ into the watchtower container) then it has the ability to update itself. If a new version of the _containrrr/watchtower_ image is pushed to the Docker Hub, your watchtower will pull down the new image and restart itself automatically.
-
 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. This option can also reference a file, in which case the contents of the file are used.
diff --git a/docs/updating.md b/docs/updating.md
new file mode 100644
index 0000000..952a0f9
--- /dev/null
+++ b/docs/updating.md
@@ -0,0 +1,6 @@
+## Updating Watchtower
+
+If watchtower is monitoring the same Docker daemon under which the watchtower container itself is running (i.e. if you 
+volume-mounted `/var/run/docker.sock` into the watchtower container) then it has the ability to update itself.  
+If a new version of the `containrrr/watchtower` image is pushed to the Docker Hub, your watchtower will pull down the 
+new image and restart itself automatically.
diff --git a/goreleaser.yml b/goreleaser.yml
index 3f5e95d..1904d5e 100644
--- a/goreleaser.yml
+++ b/goreleaser.yml
@@ -10,7 +10,7 @@ build:
     - arm
     - arm64
   ldflags:
-    - -s -w -X github.com/containrrr/watchtower/cmd.version={{.Version}}
+    - -s -w -X github.com/containrrr/watchtower/internal/meta.Version={{.Version}}
 archives:
   - 
     name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}"
diff --git a/internal/actions/mocks/client.go b/internal/actions/mocks/client.go
index b17c987..372abce 100644
--- a/internal/actions/mocks/client.go
+++ b/internal/actions/mocks/client.go
@@ -2,6 +2,7 @@ package mocks
 
 import (
 	"errors"
+	"fmt"
 	"github.com/containrrr/watchtower/pkg/container"
 	"time"
 
@@ -70,12 +71,21 @@ func (client MockClient) RemoveImageByID(id string) error {
 
 // GetContainer is a mock method
 func (client MockClient) GetContainer(containerID string) (container.Container, error) {
-	return container.Container{}, nil
+	return client.TestData.Containers[0], nil
 }
 
 // ExecuteCommand is a mock method
-func (client MockClient) ExecuteCommand(containerID string, command string, timeout int) error {
-	return nil
+func (client MockClient) ExecuteCommand(containerID string, command string, timeout int) (SkipUpdate bool, err error) {
+	switch command {
+	case "/PreUpdateReturn0.sh":
+		return false, nil
+	case "/PreUpdateReturn1.sh":
+		return false, fmt.Errorf("command exited with code 1")
+	case "/PreUpdateReturn75.sh":
+		return true, nil
+	default:
+		return false, nil
+	}
 }
 
 // IsContainerStale is always true for the mock client
diff --git a/internal/actions/mocks/container.go b/internal/actions/mocks/container.go
index 0c0ee94..07b19c3 100644
--- a/internal/actions/mocks/container.go
+++ b/internal/actions/mocks/container.go
@@ -65,13 +65,20 @@ func CreateMockContainerWithDigest(id string, name string, image string, created
 }
 
 // CreateMockContainerWithConfig creates a container substitute valid for testing
-func CreateMockContainerWithConfig(id string, name string, image string, created time.Time, config *container2.Config) container.Container {
+func CreateMockContainerWithConfig(id string, name string, image string, running bool, restarting bool, created time.Time, config *container2.Config) container.Container {
 	content := types.ContainerJSON{
 		ContainerJSONBase: &types.ContainerJSONBase{
-			ID:      id,
-			Image:   image,
-			Name:    name,
+			ID:    id,
+			Image: image,
+			Name:  name,
+			State: &types.ContainerState{
+				Running: running,
+				Restarting: restarting,
+			},
 			Created: created.String(),
+			HostConfig: &container2.HostConfig{
+				PortBindings: map[nat.Port][]nat.PortBinding{},
+			},
 		},
 		Config: config,
 	}
diff --git a/internal/actions/update.go b/internal/actions/update.go
index 66a28f1..189501a 100644
--- a/internal/actions/update.go
+++ b/internal/actions/update.go
@@ -1,6 +1,7 @@
 package actions
 
 import (
+	"errors"
 	"github.com/containrrr/watchtower/internal/util"
 	"github.com/containrrr/watchtower/pkg/container"
 	"github.com/containrrr/watchtower/pkg/lifecycle"
@@ -81,8 +82,9 @@ func Update(client container.Client, params types.UpdateParams) (*metrics2.Metri
 	if params.RollingRestart {
 		metric.Failed += performRollingRestart(containersToUpdate, client, params)
 	} else {
-		metric.Failed += stopContainersInReversedOrder(containersToUpdate, client, params)
-		metric.Failed += restartContainersInSortedOrder(containersToUpdate, client, params)
+		imageIDsOfStoppedContainers := make(map[string]bool)
+		metric.Failed, imageIDsOfStoppedContainers = stopContainersInReversedOrder(containersToUpdate, client, params)
+		metric.Failed += restartContainersInSortedOrder(containersToUpdate, client, params, imageIDsOfStoppedContainers)
 	}
 
 	metric.Updated = staleCount - (metric.Failed - staleCheckFailed)
@@ -99,13 +101,15 @@ func performRollingRestart(containers []container.Container, client container.Cl
 
 	for i := len(containers) - 1; i >= 0; i-- {
 		if containers[i].ToRestart() {
-			if err := stopStaleContainer(containers[i], client, params); err != nil {
-				failed++
-			}
-			if err := restartStaleContainer(containers[i], client, params); err != nil {
+			err := stopStaleContainer(containers[i], client, params)
+			if err != nil {
 				failed++
+			} else {
+				if err := restartStaleContainer(containers[i], client, params); err != nil {
+					failed++
+				}
+				cleanupImageIDs[containers[i].ImageID()] = true
 			}
-			cleanupImageIDs[containers[i].ImageID()] = true
 		}
 	}
 
@@ -115,14 +119,18 @@ func performRollingRestart(containers []container.Container, client container.Cl
 	return failed
 }
 
-func stopContainersInReversedOrder(containers []container.Container, client container.Client, params types.UpdateParams) int {
+func stopContainersInReversedOrder(containers []container.Container, client container.Client, params types.UpdateParams) (int, map[string]bool) {
+	imageIDsOfStoppedContainers := make(map[string]bool)
 	failed := 0
 	for i := len(containers) - 1; i >= 0; i-- {
 		if err := stopStaleContainer(containers[i], client, params); err != nil {
 			failed++
+		} else {
+			imageIDsOfStoppedContainers[containers[i].ImageID()] = true
 		}
+
 	}
-	return failed
+	return failed, imageIDsOfStoppedContainers
 }
 
 func stopStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error {
@@ -135,11 +143,16 @@ func stopStaleContainer(container container.Container, client container.Client,
 		return nil
 	}
 	if params.LifecycleHooks {
-		if err := lifecycle.ExecutePreUpdateCommand(client, container); err != nil {
+		SkipUpdate, err := lifecycle.ExecutePreUpdateCommand(client, container)
+		if err != nil {
 			log.Error(err)
 			log.Info("Skipping container as the pre-update command failed")
 			return err
 		}
+		if SkipUpdate {
+			log.Debug("Skipping container as the pre-update command returned exit code 75 (EX_TEMPFAIL)")
+			return errors.New("Skipping container as the pre-update command returned exit code 75 (EX_TEMPFAIL)")
+		}
 	}
 
 	if err := client.StopContainer(container, params.Timeout); err != nil {
@@ -149,7 +162,7 @@ func stopStaleContainer(container container.Container, client container.Client,
 	return nil
 }
 
-func restartContainersInSortedOrder(containers []container.Container, client container.Client, params types.UpdateParams) int {
+func restartContainersInSortedOrder(containers []container.Container, client container.Client, params types.UpdateParams, imageIDsOfStoppedContainers map[string]bool) int {
 	imageIDs := make(map[string]bool)
 
 	failed := 0
@@ -158,10 +171,12 @@ func restartContainersInSortedOrder(containers []container.Container, client con
 		if !c.ToRestart() {
 			continue
 		}
-		if err := restartStaleContainer(c, client, params); err != nil {
-			failed++
+		if imageIDsOfStoppedContainers[c.ImageID()] {
+			if err := restartStaleContainer(c, client, params); err != nil {
+				failed++
+			}
+			imageIDs[c.ImageID()] = true
 		}
-		imageIDs[c.ImageID()] = true
 	}
 
 	if params.Cleanup {
diff --git a/internal/flags/flags.go b/internal/flags/flags.go
index 177073d..9df42c1 100644
--- a/internal/flags/flags.go
+++ b/internal/flags/flags.go
@@ -185,6 +185,12 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) {
 		viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"),
 		"The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug")
 
+	flags.StringP(
+		"notifications-hostname",
+		"",
+		viper.GetString("WATCHTOWER_NOTIFICATIONS_HOSTNAME"),
+		"Custom hostname for notification titles")
+
 	flags.StringP(
 		"notification-email-from",
 		"",
diff --git a/internal/meta/meta.go b/internal/meta/meta.go
new file mode 100644
index 0000000..1571291
--- /dev/null
+++ b/internal/meta/meta.go
@@ -0,0 +1,13 @@
+package meta
+
+var (
+	// Version is the compile-time set version of Watchtower
+	Version = "v0.0.0-unknown"
+
+	// UserAgent is the http client identifier derived from Version
+	UserAgent string
+)
+
+func init() {
+	UserAgent = "Watchtower/" + Version
+}
diff --git a/pkg/container/client.go b/pkg/container/client.go
index 93eacb7..7138587 100644
--- a/pkg/container/client.go
+++ b/pkg/container/client.go
@@ -31,7 +31,7 @@ type Client interface {
 	StartContainer(Container) (string, error)
 	RenameContainer(Container, string) error
 	IsContainerStale(Container) (bool, error)
-	ExecuteCommand(containerID string, command string, timeout int) error
+	ExecuteCommand(containerID string, command string, timeout int) (SkipUpdate bool, err error)
 	RemoveImageByID(string) error
 	WarnOnHeadPullFailed(container Container) bool
 }
@@ -356,7 +356,7 @@ func (client dockerClient) RemoveImageByID(id string) error {
 	return err
 }
 
-func (client dockerClient) ExecuteCommand(containerID string, command string, timeout int) error {
+func (client dockerClient) ExecuteCommand(containerID string, command string, timeout int) (SkipUpdate bool, err error) {
 	bg := context.Background()
 
 	// Create the exec
@@ -368,7 +368,7 @@ func (client dockerClient) ExecuteCommand(containerID string, command string, ti
 
 	exec, err := client.api.ContainerExecCreate(bg, containerID, execConfig)
 	if err != nil {
-		return err
+		return false, err
 	}
 
 	response, attachErr := client.api.ContainerExecAttach(bg, exec.ID, types.ExecStartCheck{
@@ -383,7 +383,7 @@ func (client dockerClient) ExecuteCommand(containerID string, command string, ti
 	execStartCheck := types.ExecStartCheck{Detach: false, Tty: true}
 	err = client.api.ContainerExecStart(bg, exec.ID, execStartCheck)
 	if err != nil {
-		return err
+		return false, err
 	}
 
 	var output string
@@ -400,15 +400,16 @@ func (client dockerClient) ExecuteCommand(containerID string, command string, ti
 
 	// Inspect the exec to get the exit code and print a message if the
 	// exit code is not success.
-	err = client.waitForExecOrTimeout(bg, exec.ID, output, timeout)
+	skipUpdate, err := client.waitForExecOrTimeout(bg, exec.ID, output, timeout)
 	if err != nil {
-		return err
+		return true, err
 	}
 
-	return nil
+	return skipUpdate, nil
 }
 
-func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, execOutput string, timeout int) error {
+func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, execOutput string, timeout int) (SkipUpdate bool, err error) {
+	const ExTempFail = 75
 	var ctx context.Context
 	var cancel context.CancelFunc
 
@@ -430,7 +431,7 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e
 		}).Debug("Awaiting timeout or completion")
 
 		if err != nil {
-			return err
+			return false, err
 		}
 		if execInspect.Running == true {
 			time.Sleep(1 * time.Second)
@@ -439,13 +440,17 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e
 		if len(execOutput) > 0 {
 			log.Infof("Command output:\n%v", execOutput)
 		}
+
+		if execInspect.ExitCode == ExTempFail {
+			return true, nil
+		}
+
 		if execInspect.ExitCode > 0 {
-			log.Errorf("Command exited with code %v.", execInspect.ExitCode)
-			log.Error(execOutput)
+			return false, fmt.Errorf("Command exited with code %v  %s", execInspect.ExitCode, execOutput)
 		}
 		break
 	}
-	return nil
+	return false, nil
 }
 
 func (client dockerClient) waitForStopOrTimeout(c Container, waitTime time.Duration) error {
diff --git a/pkg/container/container.go b/pkg/container/container.go
index 92abec2..42fa917 100644
--- a/pkg/container/container.go
+++ b/pkg/container/container.go
@@ -46,6 +46,13 @@ func (c Container) IsRunning() bool {
 	return c.containerInfo.State.Running
 }
 
+// IsRestarting returns a boolean flag indicating whether or not the current
+// container is restarting. The status is determined by the value of the
+// container's "State.Restarting" property.
+func (c Container) IsRestarting() bool {
+	return c.containerInfo.State.Restarting
+}
+
 // Name returns the Docker container name.
 func (c Container) Name() string {
 	return c.containerInfo.Name
diff --git a/pkg/lifecycle/lifecycle.go b/pkg/lifecycle/lifecycle.go
index df639d7..9f9488c 100644
--- a/pkg/lifecycle/lifecycle.go
+++ b/pkg/lifecycle/lifecycle.go
@@ -37,7 +37,8 @@ func ExecutePreCheckCommand(client container.Client, container container.Contain
 	}
 
 	log.Debug("Executing pre-check command.")
-	if err := client.ExecuteCommand(container.ID(), command, 1); err != nil {
+	_,err := client.ExecuteCommand(container.ID(), command, 1);
+	if  err != nil {
 		log.Error(err)
 	}
 }
@@ -51,18 +52,24 @@ func ExecutePostCheckCommand(client container.Client, container container.Contai
 	}
 
 	log.Debug("Executing post-check command.")
-	if err := client.ExecuteCommand(container.ID(), command, 1); err != nil {
+	_,err := client.ExecuteCommand(container.ID(), command, 1);
+	if err != nil {
 		log.Error(err)
 	}
 }
 
 // ExecutePreUpdateCommand tries to run the pre-update lifecycle hook for a single container.
-func ExecutePreUpdateCommand(client container.Client, container container.Container) error {
+func ExecutePreUpdateCommand(client container.Client, container container.Container) (SkipUpdate bool,err error) {
 	timeout := container.PreUpdateTimeout()
 	command := container.GetLifecyclePreUpdateCommand()
 	if len(command) == 0 {
 		log.Debug("No pre-update command supplied. Skipping")
-		return nil
+		return false,nil
+	}
+
+	if !container.IsRunning() || container.IsRestarting() {
+		log.Debug("Container is not running. Skipping pre-update command.")
+		return false,nil
 	}
 
 	log.Debug("Executing pre-update command.")
@@ -84,7 +91,9 @@ func ExecutePostUpdateCommand(client container.Client, newContainerID string) {
 	}
 
 	log.Debug("Executing post-update command.")
-	if err := client.ExecuteCommand(newContainerID, command, 1); err != nil {
+	_,err = client.ExecuteCommand(newContainerID, command, 1);
+
+	if  err != nil {
 		log.Error(err)
 	}
 }
diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go
index 4984139..e26ca97 100644
--- a/pkg/notifications/email.go
+++ b/pkg/notifications/email.go
@@ -60,14 +60,14 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert
 	return n
 }
 
-func (e *emailTypeNotifier) GetURL() (string, error) {
+func (e *emailTypeNotifier) GetURL(c *cobra.Command) (string, error) {
 	conf := &shoutrrrSmtp.Config{
 		FromAddress: e.From,
 		FromName:    "Watchtower",
 		ToAddresses: []string{e.To},
 		Port:        uint16(e.Port),
 		Host:        e.Server,
-		Subject:     e.getSubject(),
+		Subject:     e.getSubject(c),
 		Username:    e.User,
 		Password:    e.Password,
 		UseStartTLS: !e.tlsSkipVerify,
@@ -87,8 +87,8 @@ func (e *emailTypeNotifier) GetURL() (string, error) {
 	return conf.GetURL().String(), nil
 }
 
-func (e *emailTypeNotifier) getSubject() string {
-	subject := GetTitle()
+func (e *emailTypeNotifier) getSubject(c *cobra.Command) string {
+	subject := GetTitle(c)
 
 	if e.SubjectTag != "" {
 		subject = e.SubjectTag + " " + subject
diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go
index 7a6009b..85f59b1 100644
--- a/pkg/notifications/gotify.go
+++ b/pkg/notifications/gotify.go
@@ -67,7 +67,7 @@ func getGotifyURL(flags *pflag.FlagSet) string {
 	return gotifyURL
 }
 
-func (n *gotifyTypeNotifier) GetURL() (string, error) {
+func (n *gotifyTypeNotifier) GetURL(c *cobra.Command) (string, error) {
 	apiURL, err := url.Parse(n.gotifyURL)
 	if err != nil {
 		return "", err
@@ -77,7 +77,7 @@ func (n *gotifyTypeNotifier) GetURL() (string, error) {
 		Host:       apiURL.Host,
 		Path:       apiURL.Path,
 		DisableTLS: apiURL.Scheme == "http",
-		Title:      GetTitle(),
+		Title:      GetTitle(c),
 		Token:      n.gotifyAppToken,
 	}
 
diff --git a/pkg/notifications/msteams.go b/pkg/notifications/msteams.go
index 6c47229..282ce05 100644
--- a/pkg/notifications/msteams.go
+++ b/pkg/notifications/msteams.go
@@ -42,7 +42,7 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Con
 	return n
 }
 
-func (n *msTeamsTypeNotifier) GetURL() (string, error) {
+func (n *msTeamsTypeNotifier) GetURL(c *cobra.Command) (string, error) {
 	webhookURL, err := url.Parse(n.webHookURL)
 	if err != nil {
 		return "", err
@@ -54,7 +54,7 @@ func (n *msTeamsTypeNotifier) GetURL() (string, error) {
 	}
 
 	config.Color = ColorHex
-	config.Title = GetTitle()
+	config.Title = GetTitle(c)
 
 	return config.GetURL().String(), nil
 }
diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go
index c4e962f..358c5f3 100644
--- a/pkg/notifications/notifier.go
+++ b/pkg/notifications/notifier.go
@@ -98,7 +98,7 @@ func (n *Notifier) getNotificationTypes(cmd *cobra.Command, levels []log.Level,
 			continue
 		}
 
-		shoutrrrURL, err := legacyNotifier.GetURL()
+		shoutrrrURL, err := legacyNotifier.GetURL(cmd)
 		if err != nil {
 			log.Fatal("failed to create notification config:", err)
 		}
@@ -139,10 +139,16 @@ func (n *Notifier) Close() {
 }
 
 // GetTitle returns a common notification title with hostname appended
-func GetTitle() (title string) {
+func GetTitle(c *cobra.Command) (title string) {
 	title = "Watchtower updates"
 
-	if hostname, err := os.Hostname(); err == nil {
+	f := c.PersistentFlags()
+
+	hostname, _ := f.GetString("notifications-hostname")
+
+	if hostname != "" {
+		title += " on " + hostname
+	} else if hostname, err := os.Hostname(); err == nil {
 		title += " on " + hostname
 	}
 
diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go
index b3df119..63cb44c 100644
--- a/pkg/notifications/slack.go
+++ b/pkg/notifications/slack.go
@@ -46,7 +46,7 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Convert
 	return n
 }
 
-func (s *slackTypeNotifier) GetURL() (string, error) {
+func (s *slackTypeNotifier) GetURL(c *cobra.Command) (string, error) {
 	trimmedURL := strings.TrimRight(s.HookURL, "/")
 	trimmedURL = strings.TrimLeft(trimmedURL, "https://")
 	parts := strings.Split(trimmedURL, "/")
@@ -57,7 +57,7 @@ func (s *slackTypeNotifier) GetURL() (string, error) {
 			Channel:    parts[len(parts)-3],
 			Token:      parts[len(parts)-2],
 			Color:      ColorInt,
-			Title:      GetTitle(),
+			Title:      GetTitle(c),
 			SplitLines: true,
 			Username:   s.Username,
 		}
@@ -71,7 +71,7 @@ func (s *slackTypeNotifier) GetURL() (string, error) {
 		BotName: s.Username,
 		Token:   tokens,
 		Color:   ColorHex,
-		Title:   GetTitle(),
+		Title:   GetTitle(c),
 	}
 
 	return conf.GetURL().String(), nil
diff --git a/pkg/registry/digest/digest.go b/pkg/registry/digest/digest.go
index 4634688..858bf33 100644
--- a/pkg/registry/digest/digest.go
+++ b/pkg/registry/digest/digest.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"github.com/containrrr/watchtower/internal/meta"
 	"github.com/containrrr/watchtower/pkg/registry/auth"
 	"github.com/containrrr/watchtower/pkg/registry/manifest"
 	"github.com/containrrr/watchtower/pkg/types"
@@ -86,6 +87,7 @@ func GetDigest(url string, token string) (string, error) {
 	client := &http.Client{Transport: tr}
 
 	req, _ := http.NewRequest("HEAD", url, nil)
+	req.Header.Set("User-Agent", meta.UserAgent)
 
 	if token != "" {
 		logrus.WithField("token", token).Trace("Setting request token")
diff --git a/pkg/types/convertible_notifier.go b/pkg/types/convertible_notifier.go
index 2614d12..87f8659 100644
--- a/pkg/types/convertible_notifier.go
+++ b/pkg/types/convertible_notifier.go
@@ -1,6 +1,8 @@
 package types
 
+import "github.com/spf13/cobra"
+
 // ConvertibleNotifier is a notifier capable of creating a shoutrrr URL
 type ConvertibleNotifier interface {
-	GetURL() (string, error)
+	GetURL(c *cobra.Command) (string, error)
 }


Test patch:
diff --git a/internal/actions/update_test.go b/internal/actions/update_test.go
index f1b8e85..4b03001 100644
--- a/internal/actions/update_test.go
+++ b/internal/actions/update_test.go
@@ -7,6 +7,7 @@ import (
 	"github.com/containrrr/watchtower/pkg/types"
 	container2 "github.com/docker/docker/api/types/container"
 	cli "github.com/docker/docker/client"
+	"github.com/docker/go-connections/nat"
 	"time"
 
 	. "github.com/containrrr/watchtower/internal/actions/mocks"
@@ -106,6 +107,8 @@ var _ = Describe("the update action", func() {
 								"test-container-02",
 								"test-container-02",
 								"fake-image2:latest",
+								false,
+								false,
 								time.Now(),
 								&container2.Config{
 									Labels: map[string]string{
@@ -158,4 +161,187 @@ var _ = Describe("the update action", func() {
 		})
 
 	})
+
+	When("watchtower has been instructed to run lifecycle hooks", func() {
+
+		When("prupddate script returns 1", func() {
+			BeforeEach(func() {
+				client = CreateMockClient(
+					&TestData{
+						//NameOfContainerToKeep: "test-container-02",
+						Containers: []container.Container{
+							CreateMockContainerWithConfig(
+								"test-container-02",
+								"test-container-02",
+								"fake-image2:latest",
+								true,
+								false,
+								time.Now(),
+								&container2.Config{
+									Labels: map[string]string{
+										"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout": "190",
+										"com.centurylinklabs.watchtower.lifecycle.pre-update":         "/PreUpdateReturn1.sh",
+									},
+									ExposedPorts: map[nat.Port]struct{}{},
+								}),
+						},
+					},
+					dockerClient,
+					false,
+					false,
+				)
+			})
+
+			It("should not update those containers", func() {
+				_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
+				Expect(err).NotTo(HaveOccurred())
+				Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
+			})
+
+		})
+
+		When("prupddate script returns 75", func() {
+			BeforeEach(func() {
+				client = CreateMockClient(
+					&TestData{
+						//NameOfContainerToKeep: "test-container-02",
+						Containers: []container.Container{
+							CreateMockContainerWithConfig(
+								"test-container-02",
+								"test-container-02",
+								"fake-image2:latest",
+								true,
+								false,
+								time.Now(),
+								&container2.Config{
+									Labels: map[string]string{
+										"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout": "190",
+										"com.centurylinklabs.watchtower.lifecycle.pre-update":         "/PreUpdateReturn75.sh",
+									},
+									ExposedPorts: map[nat.Port]struct{}{},
+								}),
+						},
+					},
+					dockerClient,
+					false,
+					false,
+				)
+			})
+
+			It("should not update those containers", func() {
+				_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
+				Expect(err).NotTo(HaveOccurred())
+				Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
+			})
+
+		})
+
+		When("prupddate script returns 0", func() {
+			BeforeEach(func() {
+				client = CreateMockClient(
+					&TestData{
+						//NameOfContainerToKeep: "test-container-02",
+						Containers: []container.Container{
+							CreateMockContainerWithConfig(
+								"test-container-02",
+								"test-container-02",
+								"fake-image2:latest",
+								true,
+								false,
+								time.Now(),
+								&container2.Config{
+									Labels: map[string]string{
+										"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout": "190",
+										"com.centurylinklabs.watchtower.lifecycle.pre-update":         "/PreUpdateReturn0.sh",
+									},
+									ExposedPorts: map[nat.Port]struct{}{},
+								}),
+						},
+					},
+					dockerClient,
+					false,
+					false,
+				)
+			})
+
+			It("should update those containers", func() {
+				_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
+				Expect(err).NotTo(HaveOccurred())
+				Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
+			})
+		})
+
+		When("container is not running", func() {
+			BeforeEach(func() {
+				client = CreateMockClient(
+					&TestData{
+						//NameOfContainerToKeep: "test-container-02",
+						Containers: []container.Container{
+							CreateMockContainerWithConfig(
+								"test-container-02",
+								"test-container-02",
+								"fake-image2:latest",
+								false,
+								false,
+								time.Now(),
+								&container2.Config{
+									Labels: map[string]string{
+										"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout": "190",
+										"com.centurylinklabs.watchtower.lifecycle.pre-update":         "/PreUpdateReturn1.sh",
+									},
+									ExposedPorts: map[nat.Port]struct{}{},
+								}),
+						},
+					},
+					dockerClient,
+					false,
+					false,
+				)
+			})
+
+			It("skip running preupdate", func() {
+				_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
+				Expect(err).NotTo(HaveOccurred())
+				Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
+			})
+
+		})
+
+		When("container is restarting", func() {
+			BeforeEach(func() {
+				client = CreateMockClient(
+					&TestData{
+						//NameOfContainerToKeep: "test-container-02",
+						Containers: []container.Container{
+							CreateMockContainerWithConfig(
+								"test-container-02",
+								"test-container-02",
+								"fake-image2:latest",
+								false,
+								true,
+								time.Now(),
+								&container2.Config{
+									Labels: map[string]string{
+										"com.centurylinklabs.watchtower.lifecycle.pre-update-timeout": "190",
+										"com.centurylinklabs.watchtower.lifecycle.pre-update":         "/PreUpdateReturn1.sh",
+									},
+									ExposedPorts: map[nat.Port]struct{}{},
+								}),
+						},
+					},
+					dockerClient,
+					false,
+					false,
+				)
+			})
+
+			It("skip running preupdate", func() {
+				_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true})
+				Expect(err).NotTo(HaveOccurred())
+				Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
+			})
+
+		})
+
+	})
 })
diff --git a/pkg/api/metrics/metrics_test.go b/pkg/api/metrics/metrics_test.go
index 44379ee..5120f8d 100644
--- a/pkg/api/metrics/metrics_test.go
+++ b/pkg/api/metrics/metrics_test.go
@@ -2,78 +2,89 @@ package metrics_test
 
 import (
 	"fmt"
-	"github.com/containrrr/watchtower/pkg/metrics"
 	"io/ioutil"
 	"net/http"
+	"net/http/httptest"
+	"strings"
 	"testing"
 
 	"github.com/containrrr/watchtower/pkg/api"
 	metricsAPI "github.com/containrrr/watchtower/pkg/api/metrics"
+	"github.com/containrrr/watchtower/pkg/metrics"
 
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
 )
 
-const Token = "123123123"
+const (
+	token  = "123123123"
+	getURL = "http://localhost:8080/v1/metrics"
+)
 
-func TestContainer(t *testing.T) {
+func TestMetrics(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(handler http.Handler) map[string]string {
+	metricMap := map[string]string{}
+	respWriter := httptest.NewRecorder()
 
-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)
+	req := httptest.NewRequest("GET", getURL, nil)
+	req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
+
+	handler.ServeHTTP(respWriter, req)
+
+	res := respWriter.Result()
+	body, _ := ioutil.ReadAll(res.Body)
+
+	for _, line := range strings.Split(string(body), "\n") {
+		if len(line) < 1 || line[0] == '#' {
+			continue
+		}
+		parts := strings.Split(line, " ")
+		metricMap[parts[0]] = parts[1]
+	}
+
+	return metricMap
 }
 
-var _ = Describe("the metrics", func() {
-	httpAPI := api.New(Token)
+var _ = Describe("the metrics API", func() {
+	httpAPI := api.New(token)
 	m := metricsAPI.New()
 
-	httpAPI.RegisterHandler(m.Path, m.Handle)
-	httpAPI.Start(false)
+	handleReq := httpAPI.RequireToken(m.Handle)
+	tryGetMetrics := func() map[string]string { return getWithToken(handleReq) }
 
 	It("should serve metrics", func() {
+
+		Expect(tryGetMetrics()).To(HaveKeyWithValue("watchtower_containers_updated", "0"))
+
 		metric := &metrics.Metric{
 			Scanned: 4,
 			Updated: 3,
 			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())
-
-		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"))
-		Expect(string(contents)).To(ContainSubstring("watchtower_scans_total 1"))
-		Expect(string(contents)).To(ContainSubstring("watchtower_scans_skipped 0"))
+		Eventually(tryGetMetrics).Should(SatisfyAll(
+			HaveKeyWithValue("watchtower_containers_updated", "3"),
+			HaveKeyWithValue("watchtower_containers_failed", "1"),
+			HaveKeyWithValue("watchtower_containers_scanned", "4"),
+			HaveKeyWithValue("watchtower_scans_total", "1"),
+			HaveKeyWithValue("watchtower_scans_skipped", "0"),
+		))
 
 		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).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"))
+		Eventually(tryGetMetrics).Should(SatisfyAll(
+			HaveKeyWithValue("watchtower_scans_total", "4"),
+			HaveKeyWithValue("watchtower_scans_skipped", "3"),
+		))
 	})
 })
diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go
index ba6657a..f95ecbc 100644
--- a/pkg/notifications/notifier_test.go
+++ b/pkg/notifications/notifier_test.go
@@ -43,10 +43,13 @@ var _ = Describe("notifications", func() {
 		builderFn := notifications.NewSlackNotifier
 
 		When("passing a discord url to the slack notifier", func() {
+			command := cmd.NewRootCommand()
+			flags.RegisterNotificationFlags(command)
+
 			channel := "123456789"
 			token := "abvsihdbau"
 			color := notifications.ColorInt
-			title := url.QueryEscape(notifications.GetTitle())
+			title := url.QueryEscape(notifications.GetTitle(command))
 			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{
@@ -69,13 +72,15 @@ var _ = Describe("notifications", func() {
 		When("converting a slack service config into a shoutrrr url", func() {
 
 			It("should return the expected URL", func() {
+				command := cmd.NewRootCommand()
+				flags.RegisterNotificationFlags(command)
 
 				username := "containrrrbot"
 				tokenA := "aaa"
 				tokenB := "bbb"
 				tokenC := "ccc"
 				color := url.QueryEscape(notifications.ColorHex)
-				title := url.QueryEscape(notifications.GetTitle())
+				title := url.QueryEscape(notifications.GetTitle(command))
 
 				hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC)
 				expectedOutput := fmt.Sprintf("slack://%s@%s/%s/%s?color=%s&title=%s", username, tokenA, tokenB, tokenC, color, title)
@@ -97,9 +102,12 @@ var _ = Describe("notifications", func() {
 			builderFn := notifications.NewGotifyNotifier
 
 			It("should return the expected URL", func() {
+				command := cmd.NewRootCommand()
+				flags.RegisterNotificationFlags(command)
+
 				token := "aaa"
 				host := "shoutrrr.local"
-				title := url.QueryEscape(notifications.GetTitle())
+				title := url.QueryEscape(notifications.GetTitle(command))
 
 				expectedOutput := fmt.Sprintf("gotify://%s/%s?title=%s", host, token, title)
 
@@ -120,12 +128,14 @@ var _ = Describe("notifications", func() {
 			builderFn := notifications.NewMsTeamsNotifier
 
 			It("should return the expected URL", func() {
+				command := cmd.NewRootCommand()
+				flags.RegisterNotificationFlags(command)
 
 				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())
+				title := url.QueryEscape(notifications.GetTitle(command))
 
 				hookURL := fmt.Sprintf("https://outlook.office.com/webhook/%s/IncomingWebhook/%s/%s", tokenA, tokenB, tokenC)
 				expectedOutput := fmt.Sprintf("teams://%s/%s/%s?color=%s&title=%s", tokenA, tokenB, tokenC, color, title)
@@ -215,7 +225,7 @@ func testURL(builder builderFn, args []string, expectedURL string) {
 	Expect(err).NotTo(HaveOccurred())
 
 	notifier := builder(command, []log.Level{})
-	actualURL, err := notifier.GetURL()
+	actualURL, err := notifier.GetURL(command)
 
 	Expect(err).NotTo(HaveOccurred())
 
diff --git a/pkg/registry/digest/digest_test.go b/pkg/registry/digest/digest_test.go
index 0de6025..0321c1f 100644
--- a/pkg/registry/digest/digest_test.go
+++ b/pkg/registry/digest/digest_test.go
@@ -7,6 +7,8 @@ import (
 	wtTypes "github.com/containrrr/watchtower/pkg/types"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
+	"github.com/onsi/gomega/ghttp"
+	"net/http"
 	"os"
 	"testing"
 	"time"
@@ -18,14 +20,16 @@ func TestDigest(t *testing.T) {
 	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"),
-}
+var (
+	DockerHubCredentials = &wtTypes.RegistryCredentials{
+		Username: os.Getenv("CI_INTEGRATION_TEST_REGISTRY_DH_USERNAME"),
+		Password: os.Getenv("CI_INTEGRATION_TEST_REGISTRY_DH_PASSWORD"),
+	}
+	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 == "" {
@@ -84,4 +88,32 @@ var _ = Describe("Digests", func() {
 			}),
 		)
 	})
+	When("sending a HEAD request", func() {
+		var server *ghttp.Server
+		BeforeEach(func() {
+			server = ghttp.NewServer()
+		})
+		AfterEach(func() {
+			server.Close()
+		})
+		It("should use a custom user-agent", func() {
+			server.AppendHandlers(
+				ghttp.CombineHandlers(
+					ghttp.VerifyHeader(http.Header{
+						"User-Agent": []string{"Watchtower/v0.0.0-unknown"},
+					}),
+					ghttp.RespondWith(http.StatusOK, "", http.Header{
+						digest.ContentDigestHeader: []string{
+							mockDigest,
+						},
+					}),
+				),
+			)
+			dig, err := digest.GetDigest(server.URL(), "token")
+			println(dig)
+			Expect(server.ReceivedRequests()).Should(HaveLen(1))
+			Expect(err).NotTo(HaveOccurred())
+			Expect(dig).To(Equal(mockDigest))
+		})
+	})
 })