You've already forked watchtower
							
							
				mirror of
				https://github.com/containrrr/watchtower.git
				synced 2025-10-31 00:17:44 +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:
		
							
								
								
									
										30
									
								
								cmd/root.go
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								cmd/root.go
									
									
									
									
									
								
							| @@ -30,6 +30,7 @@ var ( | ||||
| 	notifier       *notifications.Notifier | ||||
| 	timeout        time.Duration | ||||
| 	lifecycleHooks bool | ||||
| 	scope	   string | ||||
| ) | ||||
|  | ||||
| var rootCmd = &cobra.Command{ | ||||
| @@ -90,6 +91,9 @@ func PreRun(cmd *cobra.Command, args []string) { | ||||
|  | ||||
| 	enableLabel, _ = f.GetBool("label-enable") | ||||
| 	lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks") | ||||
| 	scope, _ = f.GetString("scope") | ||||
|  | ||||
| 	log.Debug(scope) | ||||
|  | ||||
| 	// configure environment vars for client | ||||
| 	err := flags.EnvConfig(cmd) | ||||
| @@ -118,21 +122,10 @@ func PreRun(cmd *cobra.Command, args []string) { | ||||
|  | ||||
| // Run is the main execution flow of the command | ||||
| func Run(c *cobra.Command, names []string) { | ||||
| 	filter := filters.BuildFilter(names, enableLabel) | ||||
| 	filter := filters.BuildFilter(names, enableLabel, scope) | ||||
| 	runOnce, _ := c.PersistentFlags().GetBool("run-once") | ||||
| 	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 noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage { | ||||
| 			log.Info("Running a one time update.") | ||||
| @@ -143,10 +136,21 @@ func Run(c *cobra.Command, names []string) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup); err != nil { | ||||
| 	if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup, scope); err != nil { | ||||
| 		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 { | ||||
| 		log.Error(err) | ||||
| 	} | ||||
|   | ||||
| @@ -228,6 +228,16 @@ Environment Variable: WATCHTOWER_HTTP_API_TOKEN | ||||
|              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 | ||||
| [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 * * *"` | ||||
|   | ||||
| @@ -23,3 +23,9 @@ Or, it can be specified as part of the `docker run` command line: | ||||
| ```bash | ||||
| 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() { | ||||
| 			It("should not do anything", func() { | ||||
| 				client.TestData.Containers = []container.Container{} | ||||
| 				err := actions.CheckForMultipleWatchtowerInstances(client, false) | ||||
| 				err := actions.CheckForMultipleWatchtowerInstances(client, false, "") | ||||
| 				Expect(err).NotTo(HaveOccurred()) | ||||
| 			}) | ||||
| 		}) | ||||
| @@ -59,7 +59,7 @@ var _ = Describe("the actions package", func() { | ||||
| 						"watchtower", | ||||
| 						time.Now()), | ||||
| 				} | ||||
| 				err := actions.CheckForMultipleWatchtowerInstances(client, false) | ||||
| 				err := actions.CheckForMultipleWatchtowerInstances(client, false, "") | ||||
| 				Expect(err).NotTo(HaveOccurred()) | ||||
| 			}) | ||||
| 		}) | ||||
| @@ -90,7 +90,7 @@ var _ = Describe("the actions package", func() { | ||||
| 			}) | ||||
|  | ||||
| 			It("should stop all but the latest one", func() { | ||||
| 				err := actions.CheckForMultipleWatchtowerInstances(client, false) | ||||
| 				err := actions.CheckForMultipleWatchtowerInstances(client, false, "") | ||||
| 				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() { | ||||
| 				err := actions.CheckForMultipleWatchtowerInstances(client, true) | ||||
| 				err := actions.CheckForMultipleWatchtowerInstances(client, true, "") | ||||
| 				Expect(err).NotTo(HaveOccurred()) | ||||
| 				Expect(client.TestData.TriedToRemoveImage()).To(BeTrue()) | ||||
| 			}) | ||||
| 			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(client.TestData.TriedToRemoveImage()).To(BeFalse()) | ||||
| 			}) | ||||
|   | ||||
| @@ -19,10 +19,11 @@ import ( | ||||
|  | ||||
| // CheckForMultipleWatchtowerInstances will ensure that there are not multiple instances of the | ||||
| // watchtower running simultaneously. If multiple watchtower containers are detected, this function | ||||
| // will stop and remove all but the most recently started container. | ||||
| func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool) error { | ||||
| // will stop and remove all but the most recently started container. This behaviour can be bypassed | ||||
| // if a scope UID is defined. | ||||
| func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool, scope string) error { | ||||
| 	awaitDockerClient() | ||||
| 	containers, err := client.ListContainers(filters.WatchtowerContainersFilter) | ||||
| 	containers, err := client.ListContainers(filters.FilterByScope(scope, filters.WatchtowerContainersFilter)) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
|   | ||||
| @@ -134,6 +134,12 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { | ||||
| 		"", | ||||
| 		viper.GetString("WATCHTOWER_HTTP_API_TOKEN"), | ||||
| 		"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 | ||||
|   | ||||
| @@ -20,5 +20,6 @@ nav: | ||||
|    - 'Secure connections': 'secure-connections.md' | ||||
|    - 'Stop signals': 'stop-signals.md' | ||||
|    - 'Lifecycle hooks': 'lifecycle-hooks.md' | ||||
|    - 'Running multiple instances': 'running-multiple-instances.md' | ||||
| plugins: | ||||
|     - search | ||||
|   | ||||
| @@ -90,6 +90,17 @@ func (c Container) Enabled() (bool, bool) { | ||||
| 	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 | ||||
| // this container is linked. | ||||
| func (c Container) Links() []string { | ||||
|   | ||||
| @@ -6,6 +6,7 @@ const ( | ||||
| 	enableLabel           = "com.centurylinklabs.watchtower.enable" | ||||
| 	dependsOnLabel        = "com.centurylinklabs.watchtower.depends-on" | ||||
| 	zodiacLabel           = "com.centurylinklabs.zodiac.original-image" | ||||
| 	scope                 = "com.centurylinklabs.watchtower.scope" | ||||
| 	preCheckLabel         = "com.centurylinklabs.watchtower.lifecycle.pre-check" | ||||
| 	postCheckLabel        = "com.centurylinklabs.watchtower.lifecycle.post-check" | ||||
| 	preUpdateLabel        = "com.centurylinklabs.watchtower.lifecycle.pre-update" | ||||
|   | ||||
| @@ -55,3 +55,27 @@ func (_m *FilterableContainer) Name() string { | ||||
|  | ||||
| 	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 | ||||
| func BuildFilter(names []string, enableLabel bool) t.Filter { | ||||
| func BuildFilter(names []string, enableLabel bool, scope string) t.Filter { | ||||
| 	filter := NoFilter | ||||
| 	filter = FilterByNames(names, filter) | ||||
| 	if enableLabel { | ||||
| @@ -60,6 +76,11 @@ func BuildFilter(names []string, enableLabel bool) t.Filter { | ||||
| 		// if the label is specifically set. | ||||
| 		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) | ||||
| 	return filter | ||||
| } | ||||
|   | ||||
| @@ -67,6 +67,29 @@ func TestFilterByEnableLabel(t *testing.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) { | ||||
| 	filter := FilterByDisabledLabel(NoFilter) | ||||
| 	assert.NotNil(t, filter) | ||||
| @@ -91,7 +114,7 @@ func TestBuildFilter(t *testing.T) { | ||||
| 	var names []string | ||||
| 	names = append(names, "test") | ||||
|  | ||||
| 	filter := BuildFilter(names, false) | ||||
| 	filter := BuildFilter(names, false, "") | ||||
|  | ||||
| 	container := new(mocks.FilterableContainer) | ||||
| 	container.On("Name").Return("Invalid") | ||||
| @@ -127,7 +150,7 @@ func TestBuildFilterEnableLabel(t *testing.T) { | ||||
| 	var names []string | ||||
| 	names = append(names, "test") | ||||
|  | ||||
| 	filter := BuildFilter(names, true) | ||||
| 	filter := BuildFilter(names, true, "") | ||||
|  | ||||
| 	container := new(mocks.FilterableContainer) | ||||
| 	container.On("Enabled").Return(false, false) | ||||
|   | ||||
| @@ -6,4 +6,5 @@ type FilterableContainer interface { | ||||
| 	Name() string | ||||
| 	IsWatchtower() bool | ||||
| 	Enabled() (bool, bool) | ||||
| 	Scope() (string, bool) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user