From bde421be0d16ef2d973438ba3a9160a5a8d83f5b Mon Sep 17 00:00:00 2001 From: David H <2668621+dhet@users.noreply.github.com> Date: Sat, 3 Oct 2020 22:00:02 +0200 Subject: [PATCH] Monitor-only for individual containers (#652) * Add monitor-only label * Add tests for monitor-only * Treat missing monitor-only label as if the option was set to false * Add docs for container-based monitor-only * Add function doc * Fix monitor-only logic --- docs/arguments.md | 4 +- docs/container-selection.md | 29 +++++++++++- internal/actions/mocks/container.go | 19 ++++++++ internal/actions/update.go | 18 ++++---- internal/actions/update_test.go | 70 +++++++++++++++++++++++++++++ pkg/container/container.go | 16 +++++++ pkg/container/metadata.go | 1 + 7 files changed, 147 insertions(+), 10 deletions(-) diff --git a/docs/arguments.md b/docs/arguments.md index c8e23a8..b80257a 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -162,7 +162,7 @@ Environment Variable: WATCHTOWER_LABEL_ENABLE **Do not** update containers that have `com.centurylinklabs.watchtower.enable` label set to false and no `--label-enable` argument is passed. Note that only one or the other (targeting by enable label) can be used at the same time to target containers. ## Without updating containers -Will only monitor for new images, not update the containers. +Will only monitor for new images, send notifications and invoke the [pre-check/post-check hooks](https://containrrr.dev/watchtower/lifecycle-hooks/), but will **not** update the containers. > ### ⚠️ Please note > @@ -175,6 +175,8 @@ Environment Variable: WATCHTOWER_MONITOR_ONLY Default: false ``` +Note that monitor-only can also be specified on a per-container basis with the `com.centurylinklabs.watchtower.monitor-only` label set on those containers. + ## Without restarting containers Do not restart containers after updating. This option can be useful when the start of the containers is managed by an external system such as systemd. diff --git a/docs/container-selection.md b/docs/container-selection.md index 0aeb897..799091f 100644 --- a/docs/container-selection.md +++ b/docs/container-selection.md @@ -1,5 +1,12 @@ By default, watchtower will watch all containers. However, sometimes only some containers should be updated. +There are two options: + +- **Fully exclude**: You can choose to exclude containers entirely from being watched by watchtower. +- **Monitor only**: In this mode, watchtower checks for container updates, sends notifications and invokes the [pre-check/post-check hooks](https://containrrr.dev/watchtower/lifecycle-hooks/) on the containers but does **not** perform the update. + +## Full Exclude + If you need to exclude some containers, set the _com.centurylinklabs.watchtower.enable_ label to `false`. ```docker @@ -28,4 +35,24 @@ If you wish to create a monitoring scope, you will need to [run multiple instanc Watchtower filters running containers by testing them against each configured criteria. A container is monitored if all criteria are met. For example: - If a container's name is on the monitoring name list (not empty `--name` argument) but it is not enabled (_centurylinklabs.watchtower.enable=false_), it won't be monitored; -- If a container's name is not on the monitoring name list (not empty `--name` argument), even if it is enabled (_centurylinklabs.watchtower.enable=true_ and `--label-enable` flag is set), it won't be monitored; \ No newline at end of file +- If a container's name is not on the monitoring name list (not empty `--name` argument), even if it is enabled (_centurylinklabs.watchtower.enable=true_ and `--label-enable` flag is set), it won't be monitored; + +## Monitor Only + +Individual containers can be marked to only be monitored (without being updated). + +To do so, set the *com.centurylinklabs.watchtower.monitor-only* label to `true` on that container. + +```docker +LABEL com.centurylinklabs.watchtower.monitor-only="true" +``` + +Or, it can be specified as part of the `docker run` command line: + +```bash +docker run -d --label=com.centurylinklabs.watchtower.monitor-only=true someimage +``` + +When the label is specified on a container, watchtower treats that container exactly as if [`WATCHTOWER_MONITOR_ONLY`](https://containrrr.dev/watchtower/arguments/#without_updating_containers) was set, but the effect is limited to the individual container. + + diff --git a/internal/actions/mocks/container.go b/internal/actions/mocks/container.go index 060340e..e92ee1c 100644 --- a/internal/actions/mocks/container.go +++ b/internal/actions/mocks/container.go @@ -27,3 +27,22 @@ func CreateMockContainer(id string, name string, image string, created time.Time }, ) } + +// CreateMockContainerWithConfig creates a container substitute valid for testing +func CreateMockContainerWithConfig(id string, name string, image string, created time.Time, config *container2.Config) container.Container { + content := types.ContainerJSON{ + ContainerJSONBase: &types.ContainerJSONBase{ + ID: id, + Image: image, + Name: name, + Created: created.String(), + }, + Config: config, + } + return *container.NewContainer( + &content, + &types.ImageInspect{ + ID: image, + }, + ) +} diff --git a/internal/actions/update.go b/internal/actions/update.go index 003449f..61f6433 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -28,7 +28,7 @@ func Update(client container.Client, params types.UpdateParams) error { for i, targetContainer := range containers { stale, err := client.IsContainerStale(targetContainer) - if stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.HasImageInfo() { + if stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly() && !targetContainer.HasImageInfo() { err = errors.New("no available image info") } if err != nil { @@ -45,18 +45,20 @@ func Update(client container.Client, params types.UpdateParams) error { checkDependencies(containers) - if params.MonitorOnly { - if params.LifecycleHooks { - lifecycle.ExecutePostChecks(client, params) + containersToUpdate := []container.Container{} + if !params.MonitorOnly { + for i := len(containers) - 1; i >= 0; i-- { + if !containers[i].IsMonitorOnly() { + containersToUpdate = append(containersToUpdate, containers[i]) + } } - return nil } if params.RollingRestart { - performRollingRestart(containers, client, params) + performRollingRestart(containersToUpdate, client, params) } else { - stopContainersInReversedOrder(containers, client, params) - restartContainersInSortedOrder(containers, client, params) + stopContainersInReversedOrder(containersToUpdate, client, params) + restartContainersInSortedOrder(containersToUpdate, client, params) } if params.LifecycleHooks { lifecycle.ExecutePostChecks(client, params) diff --git a/internal/actions/update_test.go b/internal/actions/update_test.go index 62945dc..d599cde 100644 --- a/internal/actions/update_test.go +++ b/internal/actions/update_test.go @@ -5,6 +5,7 @@ import ( "github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/container/mocks" "github.com/containrrr/watchtower/pkg/types" + container2 "github.com/docker/docker/api/types/container" cli "github.com/docker/docker/client" "time" @@ -80,4 +81,73 @@ var _ = Describe("the update action", func() { }) }) }) + + When("watchtower has been instructed to monitor only", func() { + When("certain containers are set to monitor only", func() { + BeforeEach(func() { + client = CreateMockClient( + &TestData{ + NameOfContainerToKeep: "test-container-02", + Containers: []container.Container{ + CreateMockContainer( + "test-container-01", + "test-container-01", + "fake-image1:latest", + time.Now()), + CreateMockContainerWithConfig( + "test-container-02", + "test-container-02", + "fake-image2:latest", + time.Now(), + &container2.Config{ + Labels: map[string]string{ + "com.centurylinklabs.watchtower.monitor-only": "true", + }, + }), + }, + }, + dockerClient, + false, + false, + ) + }) + + It("should not update those containers", func() { + err := actions.Update(client, types.UpdateParams{Cleanup: true}) + Expect(err).NotTo(HaveOccurred()) + Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) + }) + }) + + When("monitor only is set globally", func() { + BeforeEach(func() { + client = CreateMockClient( + &TestData{ + Containers: []container.Container{ + CreateMockContainer( + "test-container-01", + "test-container-01", + "fake-image:latest", + time.Now()), + CreateMockContainer( + "test-container-02", + "test-container-02", + "fake-image:latest", + time.Now()), + }, + }, + dockerClient, + false, + false, + ) + }) + + It("should not update any containers", func() { + err := actions.Update(client, types.UpdateParams{MonitorOnly: true}) + Expect(err).NotTo(HaveOccurred()) + Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0)) + }) + }) + + }) }) diff --git a/pkg/container/container.go b/pkg/container/container.go index dc105d7..9e339c3 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -90,6 +90,22 @@ func (c Container) Enabled() (bool, bool) { return parsedBool, true } +// IsMonitorOnly returns the value of the monitor-only label. If the label +// is not set then false is returned. +func (c Container) IsMonitorOnly() bool { + rawBool, ok := c.getLabelValue(monitorOnlyLabel) + 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 f86317c..215cccb 100644 --- a/pkg/container/metadata.go +++ b/pkg/container/metadata.go @@ -4,6 +4,7 @@ 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"