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.


Discussion:
Hi there! 👋🏼 As you're new to this repo, we'd like to suggest that you read our [code of conduct](https://github.com/containrrr/.github/blob/master/CODE_OF_CONDUCT.md) as well as our [contribution guidelines](https://github.com/containrrr/watchtower/blob/master/CONTRIBUTING.md). Thanks a bunch for opening your first issue! 🙏
Sounds like a good idea! Feel free to open a PR and I'll make sure to review it!
@simskij I would love to, but I am quite busy at the moment. Don't expect anything soon. Maybe we can wait a bit until others show interest for this particular functionality.
So lovely to see this implemented. Thanks to everyone involved @gilbsgilbs @nedzadalibegovic @piksel 



Patch:
diff --git a/docs/arguments.md b/docs/arguments.md
index 58e7e7b61..d0e46ae15 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 7447828b1..7f133e1e5 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 0bbea16ea..36abe8c6b 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 ee9fddfd6..74a04cbc5 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 e75871b91..46cf65842 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{