diff --git a/cmd/root.go b/cmd/root.go index 90c338c..48961d2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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() @@ -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") @@ -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") diff --git a/docs/arguments.md b/docs/arguments.md index a467538..8b00de9 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -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 diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 819f6f3..c11cdae 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "regexp" "strings" "time" @@ -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", @@ -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"), diff --git a/pkg/filters/filters.go b/pkg/filters/filters.go index fa5ed2a..04520cc 100644 --- a/pkg/filters/filters.go +++ b/pkg/filters/filters.go @@ -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 @@ -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 { @@ -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 \"") @@ -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 diff --git a/pkg/filters/filters_test.go b/pkg/filters/filters_test.go index 951e7ca..3d33cdf 100644 --- a/pkg/filters/filters_test.go +++ b/pkg/filters/filters_test.go @@ -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") @@ -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) @@ -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) +}