diff --git a/cmd/root.go b/cmd/root.go index 707c4fd..e4c24b3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -156,6 +156,7 @@ func Run(c *cobra.Command, names []string) { runOnce, _ := c.PersistentFlags().GetBool("run-once") enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update") enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics") + unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls") apiToken, _ := c.PersistentFlags().GetString("http-api-token") if rollingRestart && monitorOnly { @@ -180,10 +181,14 @@ func Run(c *cobra.Command, names []string) { logNotifyExit(err) } + // The lock is shared between the scheduler and the HTTP API. It only allows one update to run at a time. + updateLock := make(chan bool, 1) + updateLock <- true + httpAPI := api.New(apiToken) if enableUpdateAPI { - updateHandler := update.New(func() { runUpdatesWithNotifications(filter) }) + updateHandler := update.New(func() { runUpdatesWithNotifications(filter) }, updateLock) httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle) } @@ -192,11 +197,11 @@ func Run(c *cobra.Command, names []string) { httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle) } - if err := httpAPI.Start(enableUpdateAPI); err != nil { + if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil { log.Error("failed to start API", err) } - if err := runUpgradesOnSchedule(c, filter, filterDesc); err != nil { + if err := runUpgradesOnSchedule(c, filter, filterDesc, updateLock); err != nil { log.Error(err) } @@ -275,17 +280,19 @@ func writeStartupMessage(c *cobra.Command, sched time.Time, filtering string) { } } -func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string) error { - tryLockSem := make(chan bool, 1) - tryLockSem <- true +func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string, lock chan bool) error { + if lock == nil { + lock = make(chan bool, 1) + lock <- true + } scheduler := cron.New() err := scheduler.AddFunc( scheduleSpec, func() { select { - case v := <-tryLockSem: - defer func() { tryLockSem <- v }() + case v := <-lock: + defer func() { lock <- v }() metric := runUpdatesWithNotifications(filter) metrics.RegisterScan(metric) default: @@ -316,7 +323,7 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string) <-interrupt scheduler.Stop() log.Info("Waiting for running update to be finished...") - <-tryLockSem + <-lock return nil } diff --git a/docs/arguments.md b/docs/arguments.md index 4763940..218680e 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -255,6 +255,16 @@ Environment Variable: WATCHTOWER_HTTP_API_TOKEN Default: - ``` +## HTTP API periodic polls +Keep running periodic updates if the HTTP API mode is enabled, otherwise the HTTP API would prevent periodic polls. + +``` + Argument: --http-api-periodic-polls +Environment Variable: WATCHTOWER_HTTP_API_PERIODIC_POLLS + Type: Boolean + Default: false +``` + ## 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). diff --git a/docs/http-api-mode.md b/docs/http-api-mode.md index 5f5b19a..7af1421 100644 --- a/docs/http-api-mode.md +++ b/docs/http-api-mode.md @@ -28,6 +28,8 @@ services: - 8080:8080 ``` +By default, enabling this mode prevents periodic polls (i.e. what is specified using `--interval` or `--schedule`). To run periodic updates regardless, pass `--http-api-periodic-polls`. + Notice that there is an environment variable named WATCHTOWER_HTTP_API_TOKEN. To prevent external services from accidentally triggering image updates, all of the requests have to contain a "Token" field, valued as the token defined in WATCHTOWER_HTTP_API_TOKEN, in their headers. In this case, there is a port bind to the host machine, allowing to request localhost:8080 to reach Watchtower. The following `curl` command would trigger an image update: ```bash diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 80a5a7c..177073d 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -151,6 +151,11 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { "", viper.GetString("WATCHTOWER_HTTP_API_TOKEN"), "Sets an authentication token to HTTP API requests.") + flags.BoolP( + "http-api-periodic-polls", + "", + viper.GetBool("WATCHTOWER_HTTP_API_PERIODIC_POLLS"), + "Also run periodic updates (specified with --interval and --schedule) if HTTP API is enabled") // https://no-color.org/ flags.BoolP( "no-color", diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index b659a96..e298622 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -79,3 +79,18 @@ func testGetSecretsFromFiles(t *testing.T, flagName string, expected string) { assert.Equal(t, expected, value) } + +func TestHTTPAPIPeriodicPollsFlag(t *testing.T) { + cmd := new(cobra.Command) + SetDefaults() + RegisterDockerFlags(cmd) + RegisterSystemFlags(cmd) + + err := cmd.ParseFlags([]string{"--http-api-periodic-polls"}) + require.NoError(t, err) + + periodicPolls, err := cmd.PersistentFlags().GetBool("http-api-periodic-polls") + require.NoError(t, err) + + assert.Equal(t, true, periodicPolls) +} diff --git a/pkg/api/update/update.go b/pkg/api/update/update.go index 463b082..4721e3e 100644 --- a/pkg/api/update/update.go +++ b/pkg/api/update/update.go @@ -13,9 +13,13 @@ var ( ) // New is a factory function creating a new Handler instance -func New(updateFn func()) *Handler { - lock = make(chan bool, 1) - lock <- true +func New(updateFn func(), updateLock chan bool) *Handler { + if updateLock != nil { + lock = updateLock + } else { + lock = make(chan bool, 1) + lock <- true + } return &Handler{ fn: updateFn,