Repository: containrrr/watchtower
Pull request #888 (2021-04-08 21:12:50)

feat(log): use short image/container IDs in logs
This should clean up the logging output and only refer to images and containers using their short hashes.

ref #873

codecov[bot]: # [Codecov](https://codecov.io/gh/containrrr/watchtower/pull/888?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`main@da56c21`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/containrrr/watchtower/pull/888/graphs/tree.svg?width=650&height=150&src=pr&token=8pxWgB380Y)](https://codecov.io/gh/containrrr/watchtower/pull/888?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##             main     #888   +/-   ##
=======================================
  Coverage        ?   43.49%           
=======================================
  Files           ?       25           
  Lines           ?     1391           
  Branches        ?        0           
=======================================
  Hits            ?      605           
  Misses          ?      725           
  Partials        ?       61           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/containrrr/watchtower/pull/888?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/containrrr/watchtower/pull/888?src=pr&el=footer). Last update [da56c21...aea07d7](https://codecov.io/gh/containrrr/watchtower/pull/888?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml
index 5005b2e..7928e45 100644
--- a/.github/workflows/release-dev.yaml
+++ b/.github/workflows/release-dev.yaml
@@ -1,23 +1,47 @@
-name: Release (Develop)
+name: Push to main
 
 on:
+  workflow_dispatch: {}
   push:
     branches:
       - main
 
 jobs:
-
   build:
-
     runs-on: ubuntu-latest
-
     steps:
-    - uses: actions/checkout@v2
-
-    - uses: jerray/publish-docker-action@master
-      with:
-        username: ${{ secrets.DOCKERHUB_USERNAME }}
-        password: ${{ secrets.DOCKERHUB_PASSWORD }}
-        file: dockerfiles/Dockerfile.self-contained
-        repository: containrrr/watchtower
-        tags: latest-dev
+      - uses: actions/checkout@v2
+      - name: Set up Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: 1.15
+      - name: Build
+        run: go build -v ./...
+  test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: 1.15
+      - name: Test
+        run: go test -v -coverprofile coverage.out -covermode atomic ./... 
+      - name: Publish coverage
+        uses: codecov/codecov-action@v1
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
+  publish:
+    needs:
+      - build
+      - test
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - uses: jerray/publish-docker-action@master
+        with:
+          username: ${{ secrets.DOCKERHUB_USERNAME }}
+          password: ${{ secrets.DOCKERHUB_PASSWORD }}
+          file: dockerfiles/Dockerfile.self-contained
+          repository: containrrr/watchtower
+          tags: latest-dev
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 3083abb..de72102 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,6 +1,7 @@
 name: Release (Production)
 
 on:
+  workflow_dispatch: {}
   release:
     types:
       - created
diff --git a/docs/arguments.md b/docs/arguments.md
index 01de345..70efc21 100644
--- a/docs/arguments.md
+++ b/docs/arguments.md
@@ -39,7 +39,7 @@ Environment Variable: N/A
 
 ## Time Zone
 Sets the time zone to be used by WatchTower's logs and the optional Cron scheduling argument (--schedule). If this environment variable is not set, Watchtower will use the default time zone: UTC.
-To find out the right value, see [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), find your location and use the value in _TZ Database Name_, e.g _Europe/Rome_. The timezone can alternatively be set by volume mounting your hosts /etc/timezone file. `-v /etc/timezone:/etc/timezone:ro`
+To find out the right value, see [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones), find your location and use the value in _TZ Database Name_, e.g _Europe/Rome_. The timezone can alternatively be set by volume mounting your hosts /etc/localtime file. `-v /etc/localtime:/etc/localtime:ro`
 
 ```
             Argument: N/A
diff --git a/pkg/container/client.go b/pkg/container/client.go
index 635aa3e..b125ed6 100644
--- a/pkg/container/client.go
+++ b/pkg/container/client.go
@@ -147,8 +147,10 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
 		signal = defaultStopSignal
 	}
 
+	shortID := ShortID(c.ID())
+
 	if c.IsRunning() {
-		log.Infof("Stopping %s (%s) with %s", c.Name(), c.ID(), signal)
+		log.Infof("Stopping %s (%s) with %s", c.Name(), shortID, signal)
 		if err := client.api.ContainerKill(bg, c.ID(), signal); err != nil {
 			return err
 		}
@@ -158,9 +160,9 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
 	_ = client.waitForStopOrTimeout(c, timeout)
 
 	if c.containerInfo.HostConfig.AutoRemove {
-		log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", c.ID())
+		log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", shortID)
 	} else {
-		log.Debugf("Removing container %s", c.ID())
+		log.Debugf("Removing container %s", shortID)
 
 		if err := client.api.ContainerRemove(bg, c.ID(), types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.removeVolumes}); err != nil {
 			return err
@@ -169,7 +171,7 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
 
 	// Wait for container to be removed. In this case an error is a good thing
 	if err := client.waitForStopOrTimeout(c, timeout); err == nil {
-		return fmt.Errorf("container %s (%s) could not be removed", c.Name(), c.ID())
+		return fmt.Errorf("container %s (%s) could not be removed", c.Name(), shortID)
 	}
 
 	return nil
@@ -229,7 +231,7 @@ func (client dockerClient) StartContainer(c Container) (string, error) {
 func (client dockerClient) doStartContainer(bg context.Context, c Container, creation container.ContainerCreateCreatedBody) error {
 	name := c.Name()
 
-	log.Debugf("Starting container %s (%s)", name, creation.ID)
+	log.Debugf("Starting container %s (%s)", name, ShortID(creation.ID))
 	err := client.api.ContainerStart(bg, creation.ID, types.ContainerStartOptions{})
 	if err != nil {
 		return err
@@ -239,7 +241,7 @@ func (client dockerClient) doStartContainer(bg context.Context, c Container, cre
 
 func (client dockerClient) RenameContainer(c Container, newName string) error {
 	bg := context.Background()
-	log.Debugf("Renaming container %s (%s) to %s", c.Name(), c.ID(), newName)
+	log.Debugf("Renaming container %s (%s) to %s", c.Name(), ShortID(c.ID()), newName)
 	return client.api.ContainerRename(bg, c.ID(), newName)
 }
 
@@ -269,7 +271,7 @@ func (client dockerClient) HasNewImage(ctx context.Context, container Container)
 		return false, nil
 	}
 
-	log.Infof("Found new %s image (%s)", imageName, newImageInfo.ID)
+	log.Infof("Found new %s image (%s)", imageName, ShortID(newImageInfo.ID))
 	return true, nil
 }
 
@@ -284,13 +286,13 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e
 
 	log.WithFields(fields).Debugf("Trying to load authentication credentials.")
 	opts, err := registry.GetPullOptions(imageName)
-	if opts.RegistryAuth != "" {
-		log.Debug("Credentials loaded")
-	}
 	if err != nil {
 		log.Debugf("Error loading authentication credentials %s", err)
 		return err
 	}
+	if opts.RegistryAuth != "" {
+		log.Debug("Credentials loaded")
+	}
 
 	log.WithFields(fields).Debugf("Checking if pull is needed")
 
@@ -326,7 +328,7 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e
 }
 
 func (client dockerClient) RemoveImageByID(id string) error {
-	log.Infof("Removing image %s", id)
+	log.Infof("Removing image %s", ShortID(id))
 
 	_, err := client.api.ImageRemove(
 		context.Background(),
@@ -404,6 +406,7 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e
 	for {
 		execInspect, err := client.api.ContainerExecInspect(ctx, ID)
 
+		//goland:noinspection GoNilness
 		log.WithFields(log.Fields{
 			"exit-code": execInspect.ExitCode,
 			"exec-id":   execInspect.ExecID,
diff --git a/pkg/container/util.go b/pkg/container/util.go
new file mode 100644
index 0000000..261316f
--- /dev/null
+++ b/pkg/container/util.go
@@ -0,0 +1,23 @@
+package container
+
+import "strings"
+
+// ShortID returns the 12-character (hex) short version of an image ID hash, removing any "sha256:" prefix if present
+func ShortID(imageID string) (short string) {
+	prefixSep := strings.IndexRune(imageID, ':')
+	offset := 0
+	length := 12
+	if prefixSep >= 0 {
+		if imageID[0:prefixSep] == "sha256" {
+			offset = prefixSep + 1
+		} else {
+			length += prefixSep + 1
+		}
+	}
+
+	if len(imageID) >= offset+length {
+		return imageID[offset : offset+length]
+	}
+
+	return imageID
+}
diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go
index b9e322e..ec313a5 100644
--- a/pkg/notifications/notifier.go
+++ b/pkg/notifications/notifier.go
@@ -55,6 +55,12 @@ func (n *Notifier) String() string {
 			sb.WriteString(", ")
 		}
 	}
+
+	if sb.Len() < 2 {
+		// No notification services are configured, return early as the separator strip is not applicable
+		return "none"
+	}
+
 	names := sb.String()
 
 	// remove the last separator


Test patch:
diff --git a/pkg/container/util_test.go b/pkg/container/util_test.go
new file mode 100644
index 0000000..8cb0328
--- /dev/null
+++ b/pkg/container/util_test.go
@@ -0,0 +1,46 @@
+package container_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+
+	. "github.com/containrrr/watchtower/pkg/container"
+)
+
+var _ = Describe("container utils", func() {
+	Describe("ShortID", func() {
+		When("given a normal image ID", func() {
+			When("it contains a sha256 prefix", func() {
+				It("should return that ID in short version", func() {
+					actual := ShortID("sha256:0123456789abcd00000000001111111111222222222233333333334444444444")
+					Expect(actual).To(Equal("0123456789ab"))
+				})
+			})
+			When("it doesn't contain a prefix", func() {
+				It("should return that ID in short version", func() {
+					actual := ShortID("0123456789abcd00000000001111111111222222222233333333334444444444")
+					Expect(actual).To(Equal("0123456789ab"))
+				})
+			})
+		})
+		When("given a short image ID", func() {
+			When("it contains no prefix", func() {
+				It("should return the same string", func() {
+					Expect(ShortID("0123456789ab")).To(Equal("0123456789ab"))
+				})
+			})
+			When("it contains a the sha256 prefix", func() {
+				It("should return the ID without the prefix", func() {
+					Expect(ShortID("sha256:0123456789ab")).To(Equal("0123456789ab"))
+				})
+			})
+		})
+		When("given an ID with an unknown prefix", func() {
+			It("should return a short version of that ID including the prefix", func() {
+				Expect(ShortID("md5:0123456789ab")).To(Equal("md5:0123456789ab"))
+				Expect(ShortID("md5:0123456789abcdefg")).To(Equal("md5:0123456789ab"))
+				Expect(ShortID("md5:01")).To(Equal("md5:01"))
+			})
+		})
+	})
+})
diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go
index 5ef75a0..ecd228f 100644
--- a/pkg/notifications/notifier_test.go
+++ b/pkg/notifications/notifier_test.go
@@ -23,6 +23,22 @@ func TestActions(t *testing.T) {
 }
 
 var _ = Describe("notifications", func() {
+	Describe("the notifier", func() {
+		When("only empty notifier types are provided", func() {
+
+			command := cmd.NewRootCommand()
+			flags.RegisterNotificationFlags(command)
+
+			err := command.ParseFlags([]string{
+				"--notifications",
+				"shoutrrr",
+			})
+			Expect(err).NotTo(HaveOccurred())
+			notif := notifications.NewNotifier(command)
+
+			Expect(notif.String()).To(Equal("none"))
+		})
+	})
 	Describe("the slack notifier", func() {
 		builderFn := notifications.NewSlackNotifier