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 <simme@arcticbit.se>
pull/509/head^2
Victor Moura 5 years ago committed by GitHub
parent 557f4abcb4
commit 0217e116c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,6 +9,7 @@ import (
"github.com/containrrr/watchtower/internal/actions" "github.com/containrrr/watchtower/internal/actions"
"github.com/containrrr/watchtower/internal/flags" "github.com/containrrr/watchtower/internal/flags"
"github.com/containrrr/watchtower/pkg/api"
"github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/container"
"github.com/containrrr/watchtower/pkg/filters" "github.com/containrrr/watchtower/pkg/filters"
"github.com/containrrr/watchtower/pkg/notifications" "github.com/containrrr/watchtower/pkg/notifications"
@ -111,6 +112,18 @@ func PreRun(cmd *cobra.Command, args []string) {
func Run(c *cobra.Command, names []string) { func Run(c *cobra.Command, names []string) {
filter := filters.BuildFilter(names, enableLabel) filter := filters.BuildFilter(names, enableLabel)
runOnce, _ := c.PersistentFlags().GetBool("run-once") 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 runOnce {
if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage { if noStartupMessage, _ := c.PersistentFlags().GetBool("no-startup-message"); !noStartupMessage {

@ -14,5 +14,7 @@ COPY --from=alpine \
/usr/share/zoneinfo \ /usr/share/zoneinfo \
/usr/share/zoneinfo /usr/share/zoneinfo
EXPOSE 8080
COPY watchtower / COPY watchtower /
ENTRYPOINT ["/watchtower"] ENTRYPOINT ["/watchtower"]

@ -195,6 +195,26 @@ Environment Variable: WATCHTOWER_RUN_ONCE
Default: false 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 ## 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 * * *"`

@ -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
```

@ -113,6 +113,18 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
"", "",
viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"), viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"),
"Enable the execution of commands triggered by pre- and post-update 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 // RegisterNotificationFlags that are used by watchtower to send notifications

@ -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
}
Loading…
Cancel
Save