Skip to content

Commit 623f4e6

Browse files
authored
feat(filters): Add a flag/env to explicitly exclude containers by name (#1784)
1 parent 9180e95 commit 623f4e6

File tree

5 files changed

+118
-19
lines changed

5 files changed

+118
-19
lines changed

cmd/root.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,19 @@ import (
2929
)
3030

3131
var (
32-
client container.Client
33-
scheduleSpec string
34-
cleanup bool
35-
noRestart bool
36-
monitorOnly bool
37-
enableLabel bool
38-
notifier t.Notifier
39-
timeout time.Duration
40-
lifecycleHooks bool
41-
rollingRestart bool
42-
scope string
43-
labelPrecedence bool
32+
client container.Client
33+
scheduleSpec string
34+
cleanup bool
35+
noRestart bool
36+
monitorOnly bool
37+
enableLabel bool
38+
disableContainers []string
39+
notifier t.Notifier
40+
timeout time.Duration
41+
lifecycleHooks bool
42+
rollingRestart bool
43+
scope string
44+
labelPrecedence bool
4445
)
4546

4647
var rootCmd = NewRootCommand()
@@ -93,6 +94,7 @@ func PreRun(cmd *cobra.Command, _ []string) {
9394
}
9495

9596
enableLabel, _ = f.GetBool("label-enable")
97+
disableContainers, _ = f.GetStringSlice("disable-containers")
9698
lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks")
9799
rollingRestart, _ = f.GetBool("rolling-restart")
98100
scope, _ = f.GetString("scope")
@@ -134,7 +136,7 @@ func PreRun(cmd *cobra.Command, _ []string) {
134136

135137
// Run is the main execution flow of the command
136138
func Run(c *cobra.Command, names []string) {
137-
filter, filterDesc := filters.BuildFilter(names, enableLabel, scope)
139+
filter, filterDesc := filters.BuildFilter(names, disableContainers, enableLabel, scope)
138140
runOnce, _ := c.PersistentFlags().GetBool("run-once")
139141
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update")
140142
enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics")

docs/arguments.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,19 @@ __Do not__ Monitor and update containers that have `com.centurylinklabs.watchtow
230230
no `--label-enable` argument is passed. Note that only one or the other (targeting by enable label) can be
231231
used at the same time to target containers.
232232

233+
## Filter by disabling specific container names
234+
Monitor and update containers whose names are not in a given set of names.
235+
236+
This can be used to exclude specific containers, when setting labels is not an option.
237+
The listed containers will be excluded even if they have the enable filter set to true.
238+
239+
```text
240+
Argument: --disable-containers, -x
241+
Environment Variable: WATCHTOWER_DISABLE_CONTAINERS
242+
Type: Comma- or space-separated string list
243+
Default: ""
244+
```
245+
233246
## Without updating containers
234247
Will only monitor for new images, send notifications and invoke
235248
the [pre-check/post-check hooks](https://containrrr.dev/watchtower/lifecycle-hooks/), but will __not__ update the

internal/flags/flags.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"os"
8+
"regexp"
89
"strings"
910
"time"
1011

@@ -85,6 +86,13 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
8586
envBool("WATCHTOWER_LABEL_ENABLE"),
8687
"Watch containers where the com.centurylinklabs.watchtower.enable label is true")
8788

89+
flags.StringSliceP(
90+
"disable-containers",
91+
"x",
92+
// Due to issue spf13/viper#380, can't use viper.GetStringSlice:
93+
regexp.MustCompile("[, ]+").Split(envString("WATCHTOWER_DISABLE_CONTAINERS"), -1),
94+
"Comma-separated list of containers to explicitly exclude from watching.")
95+
8896
flags.StringP(
8997
"log-format",
9098
"l",
@@ -197,8 +205,8 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
197205
"",
198206
false,
199207
"Do health check and exit")
200-
201-
flags.BoolP(
208+
209+
flags.BoolP(
202210
"label-take-precedence",
203211
"",
204212
envBool("WATCHTOWER_LABEL_TAKE_PRECEDENCE"),

pkg/filters/filters.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatch
1313
// NoFilter will not filter out any containers
1414
func NoFilter(t.FilterableContainer) bool { return true }
1515

16-
// FilterByNames returns all containers that match the specified name
16+
// FilterByNames returns all containers that match one of the specified names
1717
func FilterByNames(names []string, baseFilter t.Filter) t.Filter {
1818
if len(names) == 0 {
1919
return baseFilter
@@ -41,6 +41,22 @@ func FilterByNames(names []string, baseFilter t.Filter) t.Filter {
4141
}
4242
}
4343

44+
// FilterByDisableNames returns all containers that don't match any of the specified names
45+
func FilterByDisableNames(disableNames []string, baseFilter t.Filter) t.Filter {
46+
if len(disableNames) == 0 {
47+
return baseFilter
48+
}
49+
50+
return func(c t.FilterableContainer) bool {
51+
for _, name := range disableNames {
52+
if name == c.Name() || name == c.Name()[1:] {
53+
return false
54+
}
55+
}
56+
return baseFilter(c)
57+
}
58+
}
59+
4460
// FilterByEnableLabel returns all containers that have the enabled label set
4561
func FilterByEnableLabel(baseFilter t.Filter) t.Filter {
4662
return func(c t.FilterableContainer) bool {
@@ -103,10 +119,11 @@ func FilterByImage(images []string, baseFilter t.Filter) t.Filter {
103119
}
104120

105121
// BuildFilter creates the needed filter of containers
106-
func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, string) {
122+
func BuildFilter(names []string, disableNames []string, enableLabel bool, scope string) (t.Filter, string) {
107123
sb := strings.Builder{}
108124
filter := NoFilter
109125
filter = FilterByNames(names, filter)
126+
filter = FilterByDisableNames(disableNames, filter)
110127

111128
if len(names) > 0 {
112129
sb.WriteString("which name matches \"")
@@ -118,6 +135,16 @@ func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, stri
118135
}
119136
sb.WriteString(`", `)
120137
}
138+
if len(disableNames) > 0 {
139+
sb.WriteString("not named one of \"")
140+
for i, n := range disableNames {
141+
sb.WriteString(n)
142+
if i < len(disableNames)-1 {
143+
sb.WriteString(`" or "`)
144+
}
145+
}
146+
sb.WriteString(`", `)
147+
}
121148

122149
if enableLabel {
123150
// If label filtering is enabled, containers should only be considered

pkg/filters/filters_test.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ func TestFilterByImage(t *testing.T) {
171171
func TestBuildFilter(t *testing.T) {
172172
names := []string{"test", "valid"}
173173

174-
filter, desc := BuildFilter(names, false, "")
174+
filter, desc := BuildFilter(names, []string{}, false, "")
175175
assert.Contains(t, desc, "test")
176176
assert.Contains(t, desc, "or")
177177
assert.Contains(t, desc, "valid")
@@ -210,7 +210,7 @@ func TestBuildFilterEnableLabel(t *testing.T) {
210210
var names []string
211211
names = append(names, "test")
212212

213-
filter, desc := BuildFilter(names, true, "")
213+
filter, desc := BuildFilter(names, []string{}, true, "")
214214
assert.Contains(t, desc, "using enable label")
215215

216216
container := new(mocks.FilterableContainer)
@@ -235,3 +235,52 @@ func TestBuildFilterEnableLabel(t *testing.T) {
235235
assert.False(t, filter(container))
236236
container.AssertExpectations(t)
237237
}
238+
239+
func TestBuildFilterDisableContainer(t *testing.T) {
240+
filter, desc := BuildFilter([]string{}, []string{"excluded", "notfound"}, false, "")
241+
assert.Contains(t, desc, "not named")
242+
assert.Contains(t, desc, "excluded")
243+
assert.Contains(t, desc, "or")
244+
assert.Contains(t, desc, "notfound")
245+
246+
container := new(mocks.FilterableContainer)
247+
container.On("Name").Return("Another")
248+
container.On("Enabled").Return(false, false)
249+
assert.True(t, filter(container))
250+
container.AssertExpectations(t)
251+
252+
container = new(mocks.FilterableContainer)
253+
container.On("Name").Return("AnotherOne")
254+
container.On("Enabled").Return(true, true)
255+
assert.True(t, filter(container))
256+
container.AssertExpectations(t)
257+
258+
container = new(mocks.FilterableContainer)
259+
container.On("Name").Return("test")
260+
container.On("Enabled").Return(false, false)
261+
assert.True(t, filter(container))
262+
container.AssertExpectations(t)
263+
264+
container = new(mocks.FilterableContainer)
265+
container.On("Name").Return("excluded")
266+
container.On("Enabled").Return(true, true)
267+
assert.False(t, filter(container))
268+
container.AssertExpectations(t)
269+
270+
container = new(mocks.FilterableContainer)
271+
container.On("Name").Return("excludedAsSubstring")
272+
container.On("Enabled").Return(true, true)
273+
assert.True(t, filter(container))
274+
container.AssertExpectations(t)
275+
276+
container = new(mocks.FilterableContainer)
277+
container.On("Name").Return("notfound")
278+
container.On("Enabled").Return(true, true)
279+
assert.False(t, filter(container))
280+
container.AssertExpectations(t)
281+
282+
container = new(mocks.FilterableContainer)
283+
container.On("Enabled").Return(false, true)
284+
assert.False(t, filter(container))
285+
container.AssertExpectations(t)
286+
}

0 commit comments

Comments
 (0)