Skip to content

Commit

Permalink
Monitor-only for individual containers (#652)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dhet committed Oct 3, 2020
1 parent 98b5186 commit bde421b
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 10 deletions.
4 changes: 3 additions & 1 deletion docs/arguments.md
Expand Up @@ -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
>
Expand All @@ -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.
Expand Down
29 changes: 28 additions & 1 deletion 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
Expand Down Expand Up @@ -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;
- 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.


19 changes: 19 additions & 0 deletions internal/actions/mocks/container.go
Expand Up @@ -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,
},
)
}
18 changes: 10 additions & 8 deletions internal/actions/update.go
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
70 changes: 70 additions & 0 deletions internal/actions/update_test.go
Expand Up @@ -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"

Expand Down Expand Up @@ -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))
})
})

})
})
16 changes: 16 additions & 0 deletions pkg/container/container.go
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions pkg/container/metadata.go
Expand Up @@ -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"
Expand Down

0 comments on commit bde421b

Please sign in to comment.