Repository: containrrr/watchtower (go)

Issue:
Proposal: Specify no-pull per service/container via label
I am watching multiple containers with watchtower. One of them only gets local image updates (built via cron). It would be nice if watchtower could restart/update that container. But currently, I get this error message:

`Unable to update container "/name_name_1": Error response from daemon: pull access denied for custom/name, repository does not exist or may require 'docker login': denied: requested access to the resource is denied. Proceeding to next.`

It would be nice to be able to specify the no-pull option just for this container via a label.

Pull request description:
feat: add no-pull label for containers
This pull request adds support for a "no-pull" label  (`com.centurylinklabs.watchtower.no-pull`) to enable the global `--no-pull`  functionality on a per-container basis.

It is based off @nedzadalibegovic's work in #1415, but tests and documentation were missing in their PR, so I added them. Note that I didn't add tests for `IsContainerStale` function because this function is untested in the existing codebase, and it is tedious to test (which is probably the reason why it's untested yet).

Fixes #1256
Closes #1415

Patch:
diff --git a/docs/arguments.md b/docs/arguments.md
index 58e7e7b..d0e46ae 100644
--- a/docs/arguments.md
+++ b/docs/arguments.md
@@ -234,6 +234,9 @@ Environment Variable: WATCHTOWER_NO_PULL
              Default: false
 ```
 
+Note that no-pull can also be specified on a per-container basis with the
+`com.centurylinklabs.watchtower.no-pull` label set on those containers.
+
 ## Without sending a startup message
 Do not send a message after watchtower started. Otherwise there will be an info-level notification.
 
diff --git a/pkg/container/client.go b/pkg/container/client.go
index 7447828..7f133e1 100644
--- a/pkg/container/client.go
+++ b/pkg/container/client.go
@@ -280,7 +280,7 @@ func (client dockerClient) RenameContainer(c Container, newName string) error {
 func (client dockerClient) IsContainerStale(container Container) (stale bool, latestImage t.ImageID, err error) {
 	ctx := context.Background()
 
-	if !client.PullImages {
+	if !client.PullImages || container.IsNoPull() {
 		log.Debugf("Skipping image pull.")
 	} else if err := client.PullImage(ctx, container); err != nil {
 		return false, container.SafeImageID(), err
diff --git a/pkg/container/container.go b/pkg/container/container.go
index 0bbea16..36abe8c 100644
--- a/pkg/container/container.go
+++ b/pkg/container/container.go
@@ -125,6 +125,22 @@ func (c Container) IsMonitorOnly() bool {
 	return parsedBool
 }
 
+// IsNoPull returns the value of the no-pull label. If the label is not set
+// then false is returned.
+func (c Container) IsNoPull() bool {
+	rawBool, ok := c.getLabelValue(noPullLabel)
+	if !ok {
+		return false
+	}
+
+	parsedBool, err := strconv.ParseBool(rawBool)
+	if err != nil {
+		return false
+	}
+
+	return parsedBool
+}
+
 // Scope returns the value of the scope UID label and if the label
 // was set.
 func (c Container) Scope() (string, bool) {
diff --git a/pkg/container/metadata.go b/pkg/container/metadata.go
index ee9fddf..74a04cb 100644
--- a/pkg/container/metadata.go
+++ b/pkg/container/metadata.go
@@ -1,18 +1,19 @@
 package container
 
 const (
-	watchtowerLabel       = "com.centurylinklabs.watchtower"
-	signalLabel           = "com.centurylinklabs.watchtower.stop-signal"
-	enableLabel           = "com.centurylinklabs.watchtower.enable"
-	monitorOnlyLabel      = "com.centurylinklabs.watchtower.monitor-only"
-	dependsOnLabel        = "com.centurylinklabs.watchtower.depends-on"
-	zodiacLabel           = "com.centurylinklabs.zodiac.original-image"
-	scope                 = "com.centurylinklabs.watchtower.scope"
-	preCheckLabel         = "com.centurylinklabs.watchtower.lifecycle.pre-check"
-	postCheckLabel        = "com.centurylinklabs.watchtower.lifecycle.post-check"
-	preUpdateLabel        = "com.centurylinklabs.watchtower.lifecycle.pre-update"
-	postUpdateLabel       = "com.centurylinklabs.watchtower.lifecycle.post-update"
-	preUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout"
+	watchtowerLabel        = "com.centurylinklabs.watchtower"
+	signalLabel            = "com.centurylinklabs.watchtower.stop-signal"
+	enableLabel            = "com.centurylinklabs.watchtower.enable"
+	monitorOnlyLabel       = "com.centurylinklabs.watchtower.monitor-only"
+	noPullLabel            = "com.centurylinklabs.watchtower.no-pull"
+	dependsOnLabel         = "com.centurylinklabs.watchtower.depends-on"
+	zodiacLabel            = "com.centurylinklabs.zodiac.original-image"
+	scope                  = "com.centurylinklabs.watchtower.scope"
+	preCheckLabel          = "com.centurylinklabs.watchtower.lifecycle.pre-check"
+	postCheckLabel         = "com.centurylinklabs.watchtower.lifecycle.post-check"
+	preUpdateLabel         = "com.centurylinklabs.watchtower.lifecycle.pre-update"
+	postUpdateLabel        = "com.centurylinklabs.watchtower.lifecycle.post-update"
+	preUpdateTimeoutLabel  = "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout"
 	postUpdateTimeoutLabel = "com.centurylinklabs.watchtower.lifecycle.post-update-timeout"
 )
 


Test patch:
diff --git a/pkg/container/container_test.go b/pkg/container/container_test.go
index e75871b..46cf658 100644
--- a/pkg/container/container_test.go
+++ b/pkg/container/container_test.go
@@ -207,6 +207,39 @@ var _ = Describe("the container", func() {
 			})
 		})
 
+		When("checking no-pull label", func() {
+			When("no-pull label is true", func() {
+				c := MockContainer(WithLabels(map[string]string{
+					"com.centurylinklabs.watchtower.no-pull": "true",
+				}))
+				It("should return true", func() {
+					Expect(c.IsNoPull()).To(Equal(true))
+				})
+			})
+			When("no-pull label is false", func() {
+				c := MockContainer(WithLabels(map[string]string{
+					"com.centurylinklabs.watchtower.no-pull": "false",
+				}))
+				It("should return false", func() {
+					Expect(c.IsNoPull()).To(Equal(false))
+				})
+			})
+			When("no-pull label is set to an invalid value", func() {
+				c := MockContainer(WithLabels(map[string]string{
+					"com.centurylinklabs.watchtower.no-pull": "maybe",
+				}))
+				It("should return false", func() {
+					Expect(c.IsNoPull()).To(Equal(false))
+				})
+			})
+			When("no-pull label is unset", func() {
+				c = MockContainer(WithLabels(map[string]string{}))
+				It("should return false", func() {
+					Expect(c.IsNoPull()).To(Equal(false))
+				})
+			})
+		})
+
 		When("there is a pre or post update timeout", func() {
 			It("should return minute values", func() {
 				c = MockContainer(WithLabels(map[string]string{