From 0217e116c47378f3eea88523af91084867c1950e Mon Sep 17 00:00:00 2001 From: Victor Moura Date: Mon, 20 Apr 2020 11:17:14 -0300 Subject: [PATCH] Watchtower HTTP API based updates (#432) * Added HTTP API trigger to update running images * Adds HTTP API authentication token parameter and handling * Exposes port 8080 in Dockerfile to allow inter-container update triggering via HTTP API * Fixes codacy issue * Adds API usage doc * Fix grammar * Moves api logic to a package of its own * Makes WT exit if token has not been set in HTTP API mode * Adds lock to prevent concurrent updates when in HTTP API mode Co-authored-by: Simon Aronsson --- cmd/root.go | 13 +++++++++ dockerfiles/Dockerfile | 2 ++ docs/arguments.md | 20 ++++++++++++++ docs/http-api-mode.md | 35 +++++++++++++++++++++++ internal/flags/flags.go | 12 ++++++++ pkg/api/api.go | 61 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 143 insertions(+) create mode 100644 docs/http-api-mode.md create mode 100644 pkg/api/api.go diff --git a/cmd/root.go b/cmd/root.go index cf2d5c6..2a87c1d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,6 +9,7 @@ import ( "github.com/containrrr/watchtower/internal/actions" "github.com/containrrr/watchtower/internal/flags" + "github.com/containrrr/watchtower/pkg/api" "github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/filters" "github.com/containrrr/watchtower/pkg/notifications" @@ -111,6 +112,18 @@ func PreRun(cmd *cobra.Command, args []string) { func Run(c *cobra.Command, names []string) { filter := filters.BuildFilter(names, enableLabel) 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 { diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile index f792d32..7e28eb2 100644 --- a/dockerfiles/Dockerfile +++ b/dockerfiles/Dockerfile @@ -14,5 +14,7 @@ COPY --from=alpine \ /usr/share/zoneinfo \ /usr/share/zoneinfo +EXPOSE 8080 + COPY watchtower / ENTRYPOINT ["/watchtower"] diff --git a/docs/arguments.md b/docs/arguments.md index 29ac8ca..1100725 100644 --- a/docs/arguments.md +++ b/docs/arguments.md @@ -193,6 +193,26 @@ Run an update attempt against a container name list one time immediately and exi Environment Variable: WATCHTOWER_RUN_ONCE Type: Boolean Default: false +``` + +## HTTP API Mode +Runs Watchtower in HTTP API mode, only allowing image updates to be triggered by an HTTP request. + +``` + Argument: --http-api +Environment Variable: WATCHTOWER_HTTP_API + Type: Boolean + Default: false +``` + +## HTTP API Token +Sets an authentication token to HTTP API requests. + +``` + Argument: --http-api-token +Environment Variable: WATCHTOWER_HTTP_API_TOKEN + Type: String + Default: - ``` ## Scheduling diff --git a/docs/http-api-mode.md b/docs/http-api-mode.md new file mode 100644 index 0000000..7d14d09 --- /dev/null +++ b/docs/http-api-mode.md @@ -0,0 +1,35 @@ +Watchtower provides an HTTP API mode that enables an HTTP endpoint that can be requested to trigger container updating. The current available endpoint list is: + +- `/v1/update` - triggers an update for all of the containers monitored by this Watchtower instance. + +--- + +To enable this mode, use the flag `--http-api`. 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.enable=true" + + watchtower: + image: containrrr/watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: --debug --http-api + environment: + - WATCHTOWER_HTTP_API_TOKEN=mytoken + labels: + - "com.centurylinklabs.watchtower.enable=false" + ports: + - 8080:8080 +``` + +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 +curl -H "Token: mytoken" localhost:8080/v1/update +``` \ No newline at end of file diff --git a/internal/flags/flags.go b/internal/flags/flags.go index d8c7dff..74c7bb4 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -113,6 +113,18 @@ func RegisterSystemFlags(rootCmd *cobra.Command) { "", viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"), "Enable the execution of commands triggered by pre- and post-update lifecycle hooks") + + flags.BoolP( + "http-api", + "", + viper.GetBool("WATCHTOWER_HTTP_API"), + "Runs Watchtower in HTTP API mode, so that image updates must to be triggered by a request") + + flags.StringP( + "http-api-token", + "", + viper.GetString("WATCHTOWER_HTTP_API_TOKEN"), + "Sets an authentication token to HTTP API requests.") } // RegisterNotificationFlags that are used by watchtower to send notifications diff --git a/pkg/api/api.go b/pkg/api/api.go new file mode 100644 index 0000000..f0e4b4e --- /dev/null +++ b/pkg/api/api.go @@ -0,0 +1,61 @@ +package api + +import ( + "os" + "net/http" + "errors" + log "github.com/sirupsen/logrus" + "io" +) + +var ( + lock chan bool +) + +func init() { + lock = make(chan bool, 1) + lock <- true +} + +func SetupHTTPUpdates(apiToken string, updateFunction func()) error { + if apiToken == "" { + return errors.New("API token is empty or has not been set. Not starting API.") + } + + log.Println("Watchtower HTTP API started.") + + http.HandleFunc("/v1/update", func(w http.ResponseWriter, r *http.Request){ + log.Info("Updates triggered by HTTP API request.") + + _, err := io.Copy(os.Stdout, r.Body) + if err != nil { + log.Println(err) + return + } + + if r.Header.Get("Token") != apiToken { + log.Println("Invalid token. Not updating.") + return + } + + log.Println("Valid token found. Attempting to update.") + + select { + case chanValue := <- lock: + defer func() { lock <- chanValue }() + updateFunction() + default: + log.Debug("Skipped. Another update already running.") + } + + + }) + + return nil +} + +func WaitForHTTPUpdates() error { + log.Fatal(http.ListenAndServe(":8080", nil)) + os.Exit(0) + return nil +} \ No newline at end of file