mirror of
https://github.com/containrrr/watchtower.git
synced 2025-09-21 21:30:48 +02:00
Adds scopeUID config to enable multiple instances of Watchtower (#511)
* Adds scopeUID config to enable multiple instances of Watchtower * Adds tests for multiple instance support with scopeuid * Adds docs on scope monitoring and multiple instance support * Adds multiple instances docs to mkdocs config file * Changes multiple instances check and refactors naming for scope feature * Applies linter suggestions * Fixes documentation on Watchtower monitoring scope
This commit is contained in:
parent
5efb249a86
commit
6a18ee911e
14 changed files with 160 additions and 24 deletions
30
cmd/root.go
30
cmd/root.go
|
@ -30,6 +30,7 @@ var (
|
||||||
notifier *notifications.Notifier
|
notifier *notifications.Notifier
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
lifecycleHooks bool
|
lifecycleHooks bool
|
||||||
|
scope string
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
|
@ -90,6 +91,9 @@ func PreRun(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
enableLabel, _ = f.GetBool("label-enable")
|
enableLabel, _ = f.GetBool("label-enable")
|
||||||
lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks")
|
lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks")
|
||||||
|
scope, _ = f.GetString("scope")
|
||||||
|
|
||||||
|
log.Debug(scope)
|
||||||
|
|
||||||
// configure environment vars for client
|
// configure environment vars for client
|
||||||
err := flags.EnvConfig(cmd)
|
err := flags.EnvConfig(cmd)
|
||||||
|
@ -118,21 +122,10 @@ func PreRun(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
// Run is the main execution flow of the command
|
// Run is the main execution flow of the command
|
||||||
func Run(c *cobra.Command, names []string) {
|
func Run(c *cobra.Command, names []string) {
|
||||||
filter := filters.BuildFilter(names, enableLabel)
|
filter := filters.BuildFilter(names, enableLabel, scope)
|
||||||
runOnce, _ := c.PersistentFlags().GetBool("run-once")
|
runOnce, _ := c.PersistentFlags().GetBool("run-once")
|
||||||
httpAPI, _ := c.PersistentFlags().GetBool("http-api")
|
httpAPI, _ := c.PersistentFlags().GetBool("http-api")
|
||||||
|
|
||||||
if httpAPI {
|
|
||||||
apiToken, _ := c.PersistentFlags().GetString("http-api-token")
|
|
||||||
|
|
||||||
if err := api.SetupHTTPUpdates(apiToken, func() { runUpdatesWithNotifications(filter) }); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
api.WaitForHTTPUpdates()
|
|
||||||
}
|
|
||||||
|
|
||||||
if runOnce {
|
if runOnce {
|
||||||
if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage {
|
if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage {
|
||||||
log.Info("Running a one time update.")
|
log.Info("Running a one time update.")
|
||||||
|
@ -143,10 +136,21 @@ func Run(c *cobra.Command, names []string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup); err != nil {
|
if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup, scope); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if httpAPI {
|
||||||
|
apiToken, _ := c.PersistentFlags().GetString("http-api-token")
|
||||||
|
|
||||||
|
if err := api.SetupHTTPUpdates(apiToken, func() { runUpdatesWithNotifications(filter) }); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
api.WaitForHTTPUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
if err := runUpgradesOnSchedule(c, filter); err != nil {
|
if err := runUpgradesOnSchedule(c, filter); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -228,6 +228,16 @@ Environment Variable: WATCHTOWER_HTTP_API_TOKEN
|
||||||
Default: -
|
Default: -
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Filter by scope
|
||||||
|
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.github.io/watchtower/running-multiple-instances).
|
||||||
|
|
||||||
|
```
|
||||||
|
Argument: --scope
|
||||||
|
Environment Variable: WATCHTOWER_SCOPE
|
||||||
|
Type: String
|
||||||
|
Default: -
|
||||||
|
```
|
||||||
|
|
||||||
## Scheduling
|
## Scheduling
|
||||||
[Cron expression](https://pkg.go.dev/github.com/robfig/cron@v1.2.0?tab=doc#hdr-CRON_Expression_Format) in 6 fields (rather than the traditional 5) which defines when and how often to check for new images. Either `--interval` or the schedule expression
|
[Cron expression](https://pkg.go.dev/github.com/robfig/cron@v1.2.0?tab=doc#hdr-CRON_Expression_Format) in 6 fields (rather than the traditional 5) which defines when and how often to check for new images. Either `--interval` or the schedule expression
|
||||||
can be defined, but not both. An example: `--schedule "0 0 4 * * *"`
|
can be defined, but not both. An example: `--schedule "0 0 4 * * *"`
|
||||||
|
|
|
@ -23,3 +23,9 @@ Or, it can be specified as part of the `docker run` command line:
|
||||||
```bash
|
```bash
|
||||||
docker run -d --label=com.centurylinklabs.watchtower.enable=true someimage
|
docker run -d --label=com.centurylinklabs.watchtower.enable=true someimage
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you wish to create a monitoring scope, you will need to [run multiple instances and set a scope for each of them](https://containrrr.github.io/watchtower/running-multiple-instances).
|
||||||
|
|
||||||
|
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;
|
27
docs/running-multiple-instances.md
Normal file
27
docs/running-multiple-instances.md
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```json
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
app-monitored-by-watchtower:
|
||||||
|
image: myapps/monitored-by-watchtower
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.scope=myscope"
|
||||||
|
|
||||||
|
watchtower:
|
||||||
|
image: containrrr/watchtower
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
command: --interval 30 --scope myscope
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.scope=myscope"
|
||||||
|
```
|
|
@ -46,7 +46,7 @@ var _ = Describe("the actions package", func() {
|
||||||
When("given an empty array", func() {
|
When("given an empty array", func() {
|
||||||
It("should not do anything", func() {
|
It("should not do anything", func() {
|
||||||
client.TestData.Containers = []container.Container{}
|
client.TestData.Containers = []container.Container{}
|
||||||
err := actions.CheckForMultipleWatchtowerInstances(client, false)
|
err := actions.CheckForMultipleWatchtowerInstances(client, false, "")
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -59,7 +59,7 @@ var _ = Describe("the actions package", func() {
|
||||||
"watchtower",
|
"watchtower",
|
||||||
time.Now()),
|
time.Now()),
|
||||||
}
|
}
|
||||||
err := actions.CheckForMultipleWatchtowerInstances(client, false)
|
err := actions.CheckForMultipleWatchtowerInstances(client, false, "")
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -90,7 +90,7 @@ var _ = Describe("the actions package", func() {
|
||||||
})
|
})
|
||||||
|
|
||||||
It("should stop all but the latest one", func() {
|
It("should stop all but the latest one", func() {
|
||||||
err := actions.CheckForMultipleWatchtowerInstances(client, false)
|
err := actions.CheckForMultipleWatchtowerInstances(client, false, "")
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -120,12 +120,12 @@ var _ = Describe("the actions package", func() {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
It("should try to delete the image if the cleanup flag is true", func() {
|
It("should try to delete the image if the cleanup flag is true", func() {
|
||||||
err := actions.CheckForMultipleWatchtowerInstances(client, true)
|
err := actions.CheckForMultipleWatchtowerInstances(client, true, "")
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImage()).To(BeTrue())
|
Expect(client.TestData.TriedToRemoveImage()).To(BeTrue())
|
||||||
})
|
})
|
||||||
It("should not try to delete the image if the cleanup flag is false", func() {
|
It("should not try to delete the image if the cleanup flag is false", func() {
|
||||||
err := actions.CheckForMultipleWatchtowerInstances(client, false)
|
err := actions.CheckForMultipleWatchtowerInstances(client, false, "")
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImage()).To(BeFalse())
|
Expect(client.TestData.TriedToRemoveImage()).To(BeFalse())
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,10 +19,11 @@ import (
|
||||||
|
|
||||||
// CheckForMultipleWatchtowerInstances will ensure that there are not multiple instances of the
|
// CheckForMultipleWatchtowerInstances will ensure that there are not multiple instances of the
|
||||||
// watchtower running simultaneously. If multiple watchtower containers are detected, this function
|
// watchtower running simultaneously. If multiple watchtower containers are detected, this function
|
||||||
// will stop and remove all but the most recently started container.
|
// will stop and remove all but the most recently started container. This behaviour can be bypassed
|
||||||
func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool) error {
|
// if a scope UID is defined.
|
||||||
|
func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool, scope string) error {
|
||||||
awaitDockerClient()
|
awaitDockerClient()
|
||||||
containers, err := client.ListContainers(filters.WatchtowerContainersFilter)
|
containers, err := client.ListContainers(filters.FilterByScope(scope, filters.WatchtowerContainersFilter))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|
|
@ -134,6 +134,12 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
|
||||||
"",
|
"",
|
||||||
viper.GetString("WATCHTOWER_HTTP_API_TOKEN"),
|
viper.GetString("WATCHTOWER_HTTP_API_TOKEN"),
|
||||||
"Sets an authentication token to HTTP API requests.")
|
"Sets an authentication token to HTTP API requests.")
|
||||||
|
|
||||||
|
flags.StringP(
|
||||||
|
"scope",
|
||||||
|
"",
|
||||||
|
viper.GetString("WATCHTOWER_SCOPE"),
|
||||||
|
"Defines a monitoring scope for the Watchtower instance.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterNotificationFlags that are used by watchtower to send notifications
|
// RegisterNotificationFlags that are used by watchtower to send notifications
|
||||||
|
|
|
@ -20,5 +20,6 @@ nav:
|
||||||
- 'Secure connections': 'secure-connections.md'
|
- 'Secure connections': 'secure-connections.md'
|
||||||
- 'Stop signals': 'stop-signals.md'
|
- 'Stop signals': 'stop-signals.md'
|
||||||
- 'Lifecycle hooks': 'lifecycle-hooks.md'
|
- 'Lifecycle hooks': 'lifecycle-hooks.md'
|
||||||
|
- 'Running multiple instances': 'running-multiple-instances.md'
|
||||||
plugins:
|
plugins:
|
||||||
- search
|
- search
|
||||||
|
|
|
@ -90,6 +90,17 @@ func (c Container) Enabled() (bool, bool) {
|
||||||
return parsedBool, true
|
return parsedBool, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scope returns the value of the scope UID label and if the label
|
||||||
|
// was set.
|
||||||
|
func (c Container) Scope() (string, bool) {
|
||||||
|
rawString, ok := c.getLabelValue(scope)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawString, true
|
||||||
|
}
|
||||||
|
|
||||||
// Links returns a list containing the names of all the containers to which
|
// Links returns a list containing the names of all the containers to which
|
||||||
// this container is linked.
|
// this container is linked.
|
||||||
func (c Container) Links() []string {
|
func (c Container) Links() []string {
|
||||||
|
|
|
@ -6,6 +6,7 @@ const (
|
||||||
enableLabel = "com.centurylinklabs.watchtower.enable"
|
enableLabel = "com.centurylinklabs.watchtower.enable"
|
||||||
dependsOnLabel = "com.centurylinklabs.watchtower.depends-on"
|
dependsOnLabel = "com.centurylinklabs.watchtower.depends-on"
|
||||||
zodiacLabel = "com.centurylinklabs.zodiac.original-image"
|
zodiacLabel = "com.centurylinklabs.zodiac.original-image"
|
||||||
|
scope = "com.centurylinklabs.watchtower.scope"
|
||||||
preCheckLabel = "com.centurylinklabs.watchtower.lifecycle.pre-check"
|
preCheckLabel = "com.centurylinklabs.watchtower.lifecycle.pre-check"
|
||||||
postCheckLabel = "com.centurylinklabs.watchtower.lifecycle.post-check"
|
postCheckLabel = "com.centurylinklabs.watchtower.lifecycle.post-check"
|
||||||
preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update"
|
preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update"
|
||||||
|
|
|
@ -55,3 +55,27 @@ func (_m *FilterableContainer) Name() string {
|
||||||
|
|
||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scope provides a mock function with given fields:
|
||||||
|
func (_m *FilterableContainer) Scope() (string, bool) {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
var r0 string
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(0).(func() string); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 bool
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func() bool); ok {
|
||||||
|
r1 = rf()
|
||||||
|
} else {
|
||||||
|
r1 = ret.Get(1).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,8 +51,24 @@ 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 {
|
||||||
|
return baseFilter(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// BuildFilter creates the needed filter of containers
|
// BuildFilter creates the needed filter of containers
|
||||||
func BuildFilter(names []string, enableLabel bool) t.Filter {
|
func BuildFilter(names []string, enableLabel bool, scope string) t.Filter {
|
||||||
filter := NoFilter
|
filter := NoFilter
|
||||||
filter = FilterByNames(names, filter)
|
filter = FilterByNames(names, filter)
|
||||||
if enableLabel {
|
if enableLabel {
|
||||||
|
@ -60,6 +76,11 @@ func BuildFilter(names []string, enableLabel bool) t.Filter {
|
||||||
// if the label is specifically set.
|
// if the label is specifically set.
|
||||||
filter = FilterByEnableLabel(filter)
|
filter = FilterByEnableLabel(filter)
|
||||||
}
|
}
|
||||||
|
if scope != "" {
|
||||||
|
// If a scope has been defined, containers should only be considered
|
||||||
|
// if the scope is specifically set.
|
||||||
|
filter = FilterByScope(scope, filter)
|
||||||
|
}
|
||||||
filter = FilterByDisabledLabel(filter)
|
filter = FilterByDisabledLabel(filter)
|
||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,29 @@ func TestFilterByEnableLabel(t *testing.T) {
|
||||||
container.AssertExpectations(t)
|
container.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFilterByScope(t *testing.T) {
|
||||||
|
var scope string
|
||||||
|
scope = "testscope"
|
||||||
|
|
||||||
|
filter := FilterByScope(scope, NoFilter)
|
||||||
|
assert.NotNil(t, filter)
|
||||||
|
|
||||||
|
container := new(mocks.FilterableContainer)
|
||||||
|
container.On("Scope").Return("testscope", true)
|
||||||
|
assert.True(t, filter(container))
|
||||||
|
container.AssertExpectations(t)
|
||||||
|
|
||||||
|
container = new(mocks.FilterableContainer)
|
||||||
|
container.On("Scope").Return("nottestscope", true)
|
||||||
|
assert.False(t, filter(container))
|
||||||
|
container.AssertExpectations(t)
|
||||||
|
|
||||||
|
container = new(mocks.FilterableContainer)
|
||||||
|
container.On("Scope").Return("", false)
|
||||||
|
assert.False(t, filter(container))
|
||||||
|
container.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFilterByDisabledLabel(t *testing.T) {
|
func TestFilterByDisabledLabel(t *testing.T) {
|
||||||
filter := FilterByDisabledLabel(NoFilter)
|
filter := FilterByDisabledLabel(NoFilter)
|
||||||
assert.NotNil(t, filter)
|
assert.NotNil(t, filter)
|
||||||
|
@ -91,7 +114,7 @@ func TestBuildFilter(t *testing.T) {
|
||||||
var names []string
|
var names []string
|
||||||
names = append(names, "test")
|
names = append(names, "test")
|
||||||
|
|
||||||
filter := BuildFilter(names, false)
|
filter := BuildFilter(names, false, "")
|
||||||
|
|
||||||
container := new(mocks.FilterableContainer)
|
container := new(mocks.FilterableContainer)
|
||||||
container.On("Name").Return("Invalid")
|
container.On("Name").Return("Invalid")
|
||||||
|
@ -127,7 +150,7 @@ func TestBuildFilterEnableLabel(t *testing.T) {
|
||||||
var names []string
|
var names []string
|
||||||
names = append(names, "test")
|
names = append(names, "test")
|
||||||
|
|
||||||
filter := BuildFilter(names, true)
|
filter := BuildFilter(names, true, "")
|
||||||
|
|
||||||
container := new(mocks.FilterableContainer)
|
container := new(mocks.FilterableContainer)
|
||||||
container.On("Enabled").Return(false, false)
|
container.On("Enabled").Return(false, false)
|
||||||
|
|
|
@ -6,4 +6,5 @@ type FilterableContainer interface {
|
||||||
Name() string
|
Name() string
|
||||||
IsWatchtower() bool
|
IsWatchtower() bool
|
||||||
Enabled() (bool, bool)
|
Enabled() (bool, bool)
|
||||||
|
Scope() (string, bool)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue