From dd54055143c92c29e0dbade7e9bcb43c89d793be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 21 Oct 2023 20:57:58 +0200 Subject: [PATCH] feat: add support for "none" scope (#1800) --- docs/arguments.md | 5 ++++ docs/running-multiple-instances.md | 38 ++++++++++++++++-------- pkg/filters/filters.go | 21 ++++++++----- pkg/filters/filters_test.go | 47 ++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 19 deletions(-) diff --git a/docs/arguments.md b/docs/arguments.md index 8b00de9..d7ed0b0 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -359,6 +359,11 @@ Environment Variable: WATCHTOWER_HTTP_API_PERIODIC_POLLS Update containers that have a `com.centurylinklabs.watchtower.scope` label set with the same value as the given argument. This enables [running multiple instances](https://containrrr.dev/watchtower/running-multiple-instances). +!!! note "Filter by lack of scope" + If you want other instances of watchtower to ignore the scoped containers, set this argument to `none`. + When omitted, watchtower will update all containers regardless of scope. + + ```text Argument: --scope Environment Variable: WATCHTOWER_SCOPE diff --git a/docs/running-multiple-instances.md b/docs/running-multiple-instances.md index 3899095..5a82c80 100644 --- a/docs/running-multiple-instances.md +++ b/docs/running-multiple-instances.md @@ -1,10 +1,11 @@ By default, Watchtower will clean up other instances and won't allow multiple instances running on the same Docker host or swarm. It is possible to override this behavior by defining a [scope](https://containrrr.github.io/watchtower/arguments/#filter_by_scope) to each running instance. -Notice that: -- Multiple instances can't run with the same scope; -- An instance without a scope will clean up other running instances, even if they have a defined scope; +!!! note + - Multiple instances can't run with the same scope; + - An instance without a scope will clean up other running instances, even if they have a defined scope; + - Supplying `none` as the scope will treat `com.centurylinklabs.watchtower.scope=none`, `com.centurylinklabs.watchtower.scope=` and the lack of a `com.centurylinklabs.watchtower.scope` label as the scope `none`. This effectly enables you to run both scoped and unscoped watchtower instances on the same machine. -To define an instance monitoring scope, use the `--scope` argument or the `WATCHTOWER_SCOPE` environment variable on startup and set the _com.centurylinklabs.watchtower.scope_ label with the same value for the containers you want to include in this instance's scope (including the instance itself). +To define an instance monitoring scope, use the `--scope` argument or the `WATCHTOWER_SCOPE` environment variable on startup and set the `com.centurylinklabs.watchtower.scope` label with the same value for the containers you want to include in this instance's scope (including the instance itself). For example, in a Docker Compose config file: @@ -12,16 +13,29 @@ For example, in a Docker Compose config file: version: '3' services: - app-monitored-by-watchtower: + app-with-scope: image: myapps/monitored-by-watchtower - labels: - - "com.centurylinklabs.watchtower.scope=myscope" + labels: [ "com.centurylinklabs.watchtower.scope=myscope" ] - watchtower: + scoped-watchtower: image: containrrr/watchtower - volumes: - - /var/run/docker.sock:/var/run/docker.sock + volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ] command: --interval 30 --scope myscope - labels: - - "com.centurylinklabs.watchtower.scope=myscope" + labels: [ "com.centurylinklabs.watchtower.scope=myscope" ] + + unscoped-app-a: + image: myapps/app-a + + unscoped-app-b: + image: myapps/app-b + labels: [ "com.centurylinklabs.watchtower.scope=none" ] + + unscoped-app-c: + image: myapps/app-b + labels: [ "com.centurylinklabs.watchtower.scope=" ] + + unscoped-watchtower: + image: containrrr/watchtower + volumes: [ "/var/run/docker.sock:/var/run/docker.sock" ] + command: --interval 30 --scope none ``` diff --git a/pkg/filters/filters.go b/pkg/filters/filters.go index 04520cc..4fa0bcd 100644 --- a/pkg/filters/filters.go +++ b/pkg/filters/filters.go @@ -86,13 +86,14 @@ func FilterByDisabledLabel(baseFilter t.Filter) t.Filter { // FilterByScope returns all containers that belongs to a specific scope func FilterByScope(scope string, baseFilter t.Filter) t.Filter { - if scope == "" { - return baseFilter - } - return func(c t.FilterableContainer) bool { - containerScope, ok := c.Scope() - if ok && containerScope == scope { + containerScope, containerHasScope := c.Scope() + + if !containerHasScope || containerScope == "" { + containerScope = "none" + } + + if containerScope == scope { return baseFilter(c) } @@ -152,7 +153,13 @@ func BuildFilter(names []string, disableNames []string, enableLabel bool, scope filter = FilterByEnableLabel(filter) sb.WriteString("using enable label, ") } - if scope != "" { + + if scope == "none" { + // If a scope has explicitly defined as "none", containers should only be considered + // if they do not have a scope defined, or if it's explicitly set to "none". + filter = FilterByScope(scope, filter) + sb.WriteString(`without a scope, "`) + } else if scope != "" { // If a scope has been defined, containers should only be considered // if the scope is specifically set. filter = FilterByScope(scope, filter) diff --git a/pkg/filters/filters_test.go b/pkg/filters/filters_test.go index 3d33cdf..2b5cb5e 100644 --- a/pkg/filters/filters_test.go +++ b/pkg/filters/filters_test.go @@ -111,6 +111,53 @@ func TestFilterByScope(t *testing.T) { container.AssertExpectations(t) } +func TestFilterByNoneScope(t *testing.T) { + scope := "none" + + filter := FilterByScope(scope, NoFilter) + assert.NotNil(t, filter) + + container := new(mocks.FilterableContainer) + container.On("Scope").Return("anyscope", true) + assert.False(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Scope").Return("", false) + assert.True(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Scope").Return("", true) + assert.True(t, filter(container)) + container.AssertExpectations(t) + + container = new(mocks.FilterableContainer) + container.On("Scope").Return("none", true) + assert.True(t, filter(container)) + container.AssertExpectations(t) +} + +func TestBuildFilterNoneScope(t *testing.T) { + filter, desc := BuildFilter(nil, nil, false, "none") + + assert.Contains(t, desc, "without a scope") + + scoped := new(mocks.FilterableContainer) + scoped.On("Enabled").Return(false, false) + scoped.On("Scope").Return("anyscope", true) + + unscoped := new(mocks.FilterableContainer) + unscoped.On("Enabled").Return(false, false) + unscoped.On("Scope").Return("", false) + + assert.False(t, filter(scoped)) + assert.True(t, filter(unscoped)) + + scoped.AssertExpectations(t) + unscoped.AssertExpectations(t) +} + func TestFilterByDisabledLabel(t *testing.T) { filter := FilterByDisabledLabel(NoFilter) assert.NotNil(t, filter)