Skip to content

Commit

Permalink
feat(filters): Add a flag/env to explicitly exclude containers by name
Browse files Browse the repository at this point in the history
Resolves #1566
  • Loading branch information
rdamazio committed Oct 3, 2023
1 parent 9180e95 commit b1620f1
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 19 deletions.
28 changes: 15 additions & 13 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,19 @@ import (
)

var (
client container.Client
scheduleSpec string
cleanup bool
noRestart bool
monitorOnly bool
enableLabel bool
notifier t.Notifier
timeout time.Duration
lifecycleHooks bool
rollingRestart bool
scope string
labelPrecedence bool
client container.Client
scheduleSpec string
cleanup bool
noRestart bool
monitorOnly bool
enableLabel bool
disableContainers []string
notifier t.Notifier
timeout time.Duration
lifecycleHooks bool
rollingRestart bool
scope string
labelPrecedence bool
)

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

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

// Run is the main execution flow of the command
func Run(c *cobra.Command, names []string) {
filter, filterDesc := filters.BuildFilter(names, enableLabel, scope)
filter, filterDesc := filters.BuildFilter(names, disableContainers, enableLabel, scope)
runOnce, _ := c.PersistentFlags().GetBool("run-once")
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update")
enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics")
Expand Down
13 changes: 13 additions & 0 deletions docs/arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,19 @@ __Do not__ Monitor and update containers that have `com.centurylinklabs.watchtow
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.

## Filter by disabling specific container names
Monitor and update containers whose names are not in a given set of names.

This can be used to exclude specific containers, when setting labels is not an option.
The listed containers will be excluded even if they have the enable filter set to true.

```text
Argument: --disable-containers, -x
Environment Variable: WATCHTOWER_DISABLE_CONTAINERS
Type: Comma- or space-separated string list
Default: ""
```

## Without updating 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
Expand Down
12 changes: 10 additions & 2 deletions internal/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
"regexp"
"strings"
"time"

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

flags.StringSliceP(
"disable-containers",
"x",
// Due to issue spf13/viper#380, can't use viper.GetStringSlice:
regexp.MustCompile("[, ]+").Split(envString("WATCHTOWER_DISABLE_CONTAINERS"), -1),
"Comma-separated list of containers to explicitly exclude from watching.")

flags.StringP(
"log-format",
"l",
Expand Down Expand Up @@ -197,8 +205,8 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
"",
false,
"Do health check and exit")
flags.BoolP(

flags.BoolP(
"label-take-precedence",
"",
envBool("WATCHTOWER_LABEL_TAKE_PRECEDENCE"),
Expand Down
31 changes: 29 additions & 2 deletions pkg/filters/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatch
// NoFilter will not filter out any containers
func NoFilter(t.FilterableContainer) bool { return true }

// FilterByNames returns all containers that match the specified name
// FilterByNames returns all containers that match one of the specified names
func FilterByNames(names []string, baseFilter t.Filter) t.Filter {
if len(names) == 0 {
return baseFilter
Expand Down Expand Up @@ -41,6 +41,22 @@ func FilterByNames(names []string, baseFilter t.Filter) t.Filter {
}
}

// FilterByDisableNames returns all containers that don't match any of the specified names
func FilterByDisableNames(disableNames []string, baseFilter t.Filter) t.Filter {
if len(disableNames) == 0 {
return baseFilter
}

return func(c t.FilterableContainer) bool {
for _, name := range disableNames {
if name == c.Name() || name == c.Name()[1:] {
return false
}
}
return baseFilter(c)
}
}

// FilterByEnableLabel returns all containers that have the enabled label set
func FilterByEnableLabel(baseFilter t.Filter) t.Filter {
return func(c t.FilterableContainer) bool {
Expand Down Expand Up @@ -103,10 +119,11 @@ func FilterByImage(images []string, baseFilter t.Filter) t.Filter {
}

// BuildFilter creates the needed filter of containers
func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, string) {
func BuildFilter(names []string, disableNames []string, enableLabel bool, scope string) (t.Filter, string) {
sb := strings.Builder{}
filter := NoFilter
filter = FilterByNames(names, filter)
filter = FilterByDisableNames(disableNames, filter)

if len(names) > 0 {
sb.WriteString("which name matches \"")
Expand All @@ -118,6 +135,16 @@ func BuildFilter(names []string, enableLabel bool, scope string) (t.Filter, stri
}
sb.WriteString(`", `)
}
if len(disableNames) > 0 {
sb.WriteString("not named one of \"")
for i, n := range disableNames {
sb.WriteString(n)
if i < len(disableNames)-1 {
sb.WriteString(`" or "`)
}
}
sb.WriteString(`", `)
}

if enableLabel {
// If label filtering is enabled, containers should only be considered
Expand Down
53 changes: 51 additions & 2 deletions pkg/filters/filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func TestFilterByImage(t *testing.T) {
func TestBuildFilter(t *testing.T) {
names := []string{"test", "valid"}

filter, desc := BuildFilter(names, false, "")
filter, desc := BuildFilter(names, []string{}, false, "")
assert.Contains(t, desc, "test")
assert.Contains(t, desc, "or")
assert.Contains(t, desc, "valid")
Expand Down Expand Up @@ -210,7 +210,7 @@ func TestBuildFilterEnableLabel(t *testing.T) {
var names []string
names = append(names, "test")

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

container := new(mocks.FilterableContainer)
Expand All @@ -235,3 +235,52 @@ func TestBuildFilterEnableLabel(t *testing.T) {
assert.False(t, filter(container))
container.AssertExpectations(t)
}

func TestBuildFilterDisableContainer(t *testing.T) {
filter, desc := BuildFilter([]string{}, []string{"excluded", "notfound"}, false, "")
assert.Contains(t, desc, "not named")
assert.Contains(t, desc, "excluded")
assert.Contains(t, desc, "or")
assert.Contains(t, desc, "notfound")

container := new(mocks.FilterableContainer)
container.On("Name").Return("Another")
container.On("Enabled").Return(false, false)
assert.True(t, filter(container))
container.AssertExpectations(t)

container = new(mocks.FilterableContainer)
container.On("Name").Return("AnotherOne")
container.On("Enabled").Return(true, true)
assert.True(t, filter(container))
container.AssertExpectations(t)

container = new(mocks.FilterableContainer)
container.On("Name").Return("test")
container.On("Enabled").Return(false, false)
assert.True(t, filter(container))
container.AssertExpectations(t)

container = new(mocks.FilterableContainer)
container.On("Name").Return("excluded")
container.On("Enabled").Return(true, true)
assert.False(t, filter(container))
container.AssertExpectations(t)

container = new(mocks.FilterableContainer)
container.On("Name").Return("excludedAsSubstring")
container.On("Enabled").Return(true, true)
assert.True(t, filter(container))
container.AssertExpectations(t)

container = new(mocks.FilterableContainer)
container.On("Name").Return("notfound")
container.On("Enabled").Return(true, true)
assert.False(t, filter(container))
container.AssertExpectations(t)

container = new(mocks.FilterableContainer)
container.On("Enabled").Return(false, true)
assert.False(t, filter(container))
container.AssertExpectations(t)
}

0 comments on commit b1620f1

Please sign in to comment.