Merge branch 'master' into all-contributors/add-zoispag
commit
5927c6c2a1
@ -1 +1,2 @@
|
||||
custom: https://www.amazon.com/hz/wishlist/ls/F94JJV822VX6
|
||||
github: simskij
|
||||
|
@ -1,11 +0,0 @@
|
||||
newIssueWelcomeComment: >
|
||||
Hi there!
|
||||
|
||||
Thanks a bunch for opening your first issue! :pray:
|
||||
As you're new to this repo, we'd like to suggest that you read our [code of conduct](https://github.com/containrrr/watchtower/blob/master/CODE_OF_CONDUCT.md)
|
||||
|
||||
newPRWelcomeComment: >
|
||||
Thanks for opening this pull request! Please check out our [contributing guidelines](https://github.com/containrrr/watchtower/blob/master/CONTRIBUTING.md) as well as our [code of conduct](https://github.com/containrrr/watchtower/blob/master/CODE_OF_CONDUCT.md).
|
||||
|
||||
firstPRMergeComment: >
|
||||
Congrats on merging your first pull request! We are all very proud of you! :sparkles:
|
@ -0,0 +1,18 @@
|
||||
name: Greetings
|
||||
|
||||
on: [pull_request, issues]
|
||||
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: >
|
||||
Hi there! 👋🏼
|
||||
As you're new to this repo, we'd like to suggest that you read our [code of conduct](https://github.com/containrrr/watchtower/blob/master/CODE_OF_CONDUCT.md)
|
||||
as well as our [contribution guidelines](https://github.com/containrrr/watchtower/blob/master/CONTRIBUTING.md).
|
||||
Thanks a bunch for opening your first issue! 🙏
|
||||
pr-message: >
|
||||
Congratulations on opening your first pull request! We'll get back to you as soon as possible. In the meantime, please make sure you've updated the documentation to reflect your changes and have added test automation as needed. Thanks! 🙏🏼
|
@ -1,129 +0,0 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/container"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
)
|
||||
|
||||
// UpdateParams contains all different options available to alter the behavior of the Update func
|
||||
type UpdateParams struct {
|
||||
Filter container.Filter
|
||||
Cleanup bool
|
||||
NoRestart bool
|
||||
Timeout time.Duration
|
||||
MonitorOnly bool
|
||||
}
|
||||
|
||||
// Update looks at the running Docker containers to see if any of the images
|
||||
// used to start those containers have been updated. If a change is detected in
|
||||
// any of the images, the associated containers are stopped and restarted with
|
||||
// the new image.
|
||||
func Update(client container.Client, params UpdateParams) error {
|
||||
log.Debug("Checking containers for updated images")
|
||||
|
||||
containers, err := client.ListContainers(params.Filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, container := range containers {
|
||||
stale, err := client.IsContainerStale(container)
|
||||
if err != nil {
|
||||
log.Infof("Unable to update container %s. Proceeding to next.", containers[i].Name())
|
||||
log.Debug(err)
|
||||
stale = false
|
||||
}
|
||||
containers[i].Stale = stale
|
||||
}
|
||||
|
||||
containers, err = container.SortByDependencies(containers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkDependencies(containers)
|
||||
|
||||
if params.MonitorOnly {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stale containers in reverse order
|
||||
for i := len(containers) - 1; i >= 0; i-- {
|
||||
container := containers[i]
|
||||
|
||||
if container.IsWatchtower() {
|
||||
log.Debugf("This is the watchtower container %s", containers[i].Name())
|
||||
continue
|
||||
}
|
||||
|
||||
if container.Stale {
|
||||
if err := client.StopContainer(container, params.Timeout); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restart stale containers in sorted order
|
||||
for _, container := range containers {
|
||||
if container.Stale {
|
||||
// Since we can't shutdown a watchtower container immediately, we need to
|
||||
// start the new one while the old one is still running. This prevents us
|
||||
// from re-using the same container name so we first rename the current
|
||||
// instance so that the new one can adopt the old name.
|
||||
if container.IsWatchtower() {
|
||||
if err := client.RenameContainer(container, randName()); err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !params.NoRestart {
|
||||
if err := client.StartContainer(container); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
if params.Cleanup {
|
||||
client.RemoveImage(container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkDependencies(containers []container.Container) {
|
||||
|
||||
for i, parent := range containers {
|
||||
if parent.Stale {
|
||||
continue
|
||||
}
|
||||
|
||||
LinkLoop:
|
||||
for _, linkName := range parent.Links() {
|
||||
for _, child := range containers {
|
||||
if child.Name() == linkName && child.Stale {
|
||||
containers[i].Stale = true
|
||||
break LinkLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generates a random, 32-character, Docker-compatible container name.
|
||||
func randName() string {
|
||||
b := make([]rune, 32)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
@ -1,168 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// SetupCliFlags registers flags on the supplied urfave app
|
||||
func SetupCliFlags(app *cli.App) {
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "host, H",
|
||||
Usage: "daemon socket to connect to",
|
||||
Value: "unix:///var/run/docker.sock",
|
||||
EnvVar: "DOCKER_HOST",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "interval, i",
|
||||
Usage: "poll interval (in seconds)",
|
||||
Value: 300,
|
||||
EnvVar: "WATCHTOWER_POLL_INTERVAL",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "schedule, s",
|
||||
Usage: "the cron expression which defines when to update",
|
||||
EnvVar: "WATCHTOWER_SCHEDULE",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-pull",
|
||||
Usage: "do not pull new images",
|
||||
EnvVar: "WATCHTOWER_NO_PULL",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-restart",
|
||||
Usage: "do not restart containers",
|
||||
EnvVar: "WATCHTOWER_NO_RESTART",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "cleanup",
|
||||
Usage: "remove old images after updating",
|
||||
EnvVar: "WATCHTOWER_CLEANUP",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "tlsverify",
|
||||
Usage: "use TLS and verify the remote",
|
||||
EnvVar: "DOCKER_TLS_VERIFY",
|
||||
},
|
||||
cli.DurationFlag{
|
||||
Name: "stop-timeout",
|
||||
Usage: "timeout before container is forcefully stopped",
|
||||
Value: time.Second * 10,
|
||||
EnvVar: "WATCHTOWER_TIMEOUT",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "label-enable",
|
||||
Usage: "watch containers where the com.centurylinklabs.watchtower.enable label is true",
|
||||
EnvVar: "WATCHTOWER_LABEL_ENABLE",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "enable debug mode with verbose logging",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "notifications",
|
||||
Value: &cli.StringSlice{},
|
||||
Usage: "notification types to send (valid: email, slack, msteams)",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATIONS",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "notifications-level",
|
||||
Usage: "The log level used for sending notifications. Possible values: \"panic\", \"fatal\", \"error\", \"warn\", \"info\" or \"debug\"",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATIONS_LEVEL",
|
||||
Value: "info",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "notification-email-from",
|
||||
Usage: "Address to send notification e-mails from",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_FROM",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "notification-email-to",
|
||||
Usage: "Address to send notification e-mails to",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_TO",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "notification-email-server",
|
||||
Usage: "SMTP server to send notification e-mails through",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "notification-email-server-port",
|
||||
Usage: "SMTP server port to send notification e-mails through",
|
||||
Value: 25,
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "notification-email-server-tls-skip-verify",
|
||||
Usage: "Controls whether watchtower verifies the SMTP server's certificate chain and host name. " +
|
||||
"If set, TLS accepts any certificate " +
|
||||
"presented by the server and any host name in that certificate. " +
|
||||
"In this mode, TLS is susceptible to man-in-the-middle attacks. " +
|
||||
"This should be used only for testing.",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "notification-email-server-user",
|
||||
Usage: "SMTP server user for sending notifications",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "notification-email-server-password",
|
||||
Usage: "SMTP server password for sending notifications",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "notification-slack-hook-url",
|
||||
Usage: "The Slack Hook URL to send notifications to",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "notification-slack-identifier",
|
||||
Usage: "A string which will be used to identify the messages coming from this watchtower instance. Default if omitted is \"watchtower\"",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER",
|
||||
Value: "watchtower",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "notification-slack-channel",
|
||||
Usage: "A string which overrides the webhook's default channel. Example: #my-custom-channel",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_SLACK_CHANNEL",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "notification-slack-icon-emoji",
|
||||
Usage: "An emoji code string to use in place of the default icon",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "notification-slack-icon-url",
|
||||
Usage: "An icon image URL string to use in place of the default icon",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_SLACK_ICON_URL",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "notification-msteams-hook",
|
||||
Usage: "The MSTeams WebHook URL to send notifications to",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "notification-msteams-data",
|
||||
Usage: "The MSTeams notifier will try to extract log entry fields as MSTeams message facts",
|
||||
EnvVar: "WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "monitor-only",
|
||||
Usage: "Will only monitor for new images, not update the containers",
|
||||
EnvVar: "WATCHTOWER_MONITOR_ONLY",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "run-once",
|
||||
Usage: "Run once now and exit",
|
||||
EnvVar: "WATCHTOWER_RUN_ONCE",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "include-stopped",
|
||||
Usage: "Will also include created and exited containers",
|
||||
EnvVar: "WATCHTOWER_INCLUDE_STOPPED",
|
||||
},
|
||||
}
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/watchtower/internal/actions"
|
||||
"github.com/containrrr/watchtower/internal/flags"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
"github.com/containrrr/watchtower/pkg/notifications"
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/robfig/cron"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
client container.Client
|
||||
scheduleSpec string
|
||||
cleanup bool
|
||||
noRestart bool
|
||||
monitorOnly bool
|
||||
enableLabel bool
|
||||
notifier *notifications.Notifier
|
||||
timeout time.Duration
|
||||
lifecycleHooks bool
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "watchtower",
|
||||
Short: "Automatically updates running Docker containers",
|
||||
Long: `
|
||||
Watchtower automatically updates running Docker containers whenever a new image is released.
|
||||
More information available at https://github.com/containrrr/watchtower/.
|
||||
`,
|
||||
Run: Run,
|
||||
PreRun: PreRun,
|
||||
}
|
||||
|
||||
func init() {
|
||||
flags.SetDefaults()
|
||||
flags.RegisterDockerFlags(rootCmd)
|
||||
flags.RegisterSystemFlags(rootCmd)
|
||||
flags.RegisterNotificationFlags(rootCmd)
|
||||
}
|
||||
|
||||
// Execute the root func and exit in case of errors
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// PreRun is a lifecycle hook that runs before the command is executed.
|
||||
func PreRun(cmd *cobra.Command, args []string) {
|
||||
f := cmd.PersistentFlags()
|
||||
|
||||
if enabled, _ := f.GetBool("debug"); enabled {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
pollingSet := f.Changed("interval")
|
||||
schedule, _ := f.GetString("schedule")
|
||||
cronLen := len(schedule)
|
||||
|
||||
if pollingSet && cronLen > 0 {
|
||||
log.Fatal("Only schedule or interval can be defined, not both.")
|
||||
} else if cronLen > 0 {
|
||||
scheduleSpec, _ = f.GetString("schedule")
|
||||
} else {
|
||||
interval, _ := f.GetInt("interval")
|
||||
scheduleSpec = "@every " + strconv.Itoa(interval) + "s"
|
||||
}
|
||||
|
||||
cleanup, noRestart, monitorOnly, timeout = flags.ReadFlags(cmd)
|
||||
|
||||
if timeout < 0 {
|
||||
log.Fatal("Please specify a positive value for timeout value.")
|
||||
}
|
||||
|
||||
enableLabel, _ = f.GetBool("label-enable")
|
||||
lifecycleHooks, _ = f.GetBool("enable-lifecycle-hooks")
|
||||
|
||||
// configure environment vars for client
|
||||
err := flags.EnvConfig(cmd)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
noPull, _ := f.GetBool("no-pull")
|
||||
includeStopped, _ := f.GetBool("include-stopped")
|
||||
reviveStopped, _ := f.GetBool("revive-stopped")
|
||||
removeVolumes, _ := f.GetBool("remove-volumes")
|
||||
|
||||
client = container.NewClient(
|
||||
!noPull,
|
||||
includeStopped,
|
||||
reviveStopped,
|
||||
removeVolumes,
|
||||
)
|
||||
|
||||
notifier = notifications.NewNotifier(cmd)
|
||||
}
|
||||
|
||||
// Run is the main execution flow of the command
|
||||
func Run(c *cobra.Command, names []string) {
|
||||
filter := container.BuildFilter(names, enableLabel)
|
||||
runOnce, _ := c.PersistentFlags().GetBool("run-once")
|
||||
|
||||
if runOnce {
|
||||
log.Info("Running a one time update.")
|
||||
runUpdatesWithNotifications(filter)
|
||||
os.Exit(0)
|
||||
return
|
||||
}
|
||||
|
||||
if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := runUpgradesOnSchedule(filter); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func runUpgradesOnSchedule(filter t.Filter) error {
|
||||
tryLockSem := make(chan bool, 1)
|
||||
tryLockSem <- true
|
||||
|
||||
cron := cron.New()
|
||||
err := cron.AddFunc(
|
||||
scheduleSpec,
|
||||
func() {
|
||||
select {
|
||||
case v := <-tryLockSem:
|
||||
defer func() { tryLockSem <- v }()
|
||||
runUpdatesWithNotifications(filter)
|
||||
default:
|
||||
log.Debug("Skipped another update already running.")
|
||||
}
|
||||
|
||||
nextRuns := cron.Entries()
|
||||
if len(nextRuns) > 0 {
|
||||
log.Debug("Scheduled next run: " + nextRuns[0].Next.String())
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debug("Starting Watchtower and scheduling first run: " + cron.Entries()[0].Schedule.Next(time.Now()).String())
|
||||
cron.Start()
|
||||
|
||||
// Graceful shut-down on SIGINT/SIGTERM
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
signal.Notify(interrupt, syscall.SIGTERM)
|
||||
|
||||
<-interrupt
|
||||
cron.Stop()
|
||||
log.Info("Waiting for running update to be finished...")
|
||||
<-tryLockSem
|
||||
return nil
|
||||
}
|
||||
|
||||
func runUpdatesWithNotifications(filter t.Filter) {
|
||||
notifier.StartNotification()
|
||||
updateParams := actions.UpdateParams{
|
||||
Filter: filter,
|
||||
Cleanup: cleanup,
|
||||
NoRestart: noRestart,
|
||||
Timeout: timeout,
|
||||
MonitorOnly: monitorOnly,
|
||||
LifecycleHooks: lifecycleHooks,
|
||||
}
|
||||
err := actions.Update(client, updateParams)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
notifier.SendNotification()
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
Some private docker registries (the most prominent probably being AWS ECR) use non-standard ways of authentication.
|
||||
To be able to use this together with watchtower, we need to use a credential helper.
|
||||
|
||||
To keep the image size small we've decided to not include any helpers in the watchtower image, instead we'll put the
|
||||
helper in a separate container and mount it using volumes.
|
||||
|
||||
### Example
|
||||
Example implementation for use with [amazon-ecr-credential-helper](https://github.com/awslabs/amazon-ecr-credential-helper):
|
||||
|
||||
```Dockerfile
|
||||
FROM golang:latest
|
||||
|
||||
ENV CGO_ENABLED 0
|
||||
ENV REPO github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login
|
||||
|
||||
RUN go get -u $REPO
|
||||
|
||||
RUN rm /go/bin/docker-credential-ecr-login
|
||||
|
||||
RUN go build \
|
||||
-o /go/bin/docker-credential-ecr-login \
|
||||
/go/src/$REPO
|
||||
|
||||
WORKDIR /go/bin/
|
||||
```
|
||||
|
||||
and the docker-compose definition:
|
||||
```yaml
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
watchtower:
|
||||
image: index.docker.io/containrrr/watchtower:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- <PATH_TO_HOME_DIR>/.docker/config.json:/config.json
|
||||
- helper:/go/bin
|
||||
environment:
|
||||
- HOME=/
|
||||
- PATH=$PATH:/go/bin
|
||||
- AWS_REGION=<AWS_REGION>
|
||||
- AWS_ACCESS_KEY_ID=<AWS_ACCESS_KEY>
|
||||
- AWS_SECRET_ACCESS_KEY=<AWS_SECRET_ACCESS_KEY>
|
||||
volumes:
|
||||
helper: {}
|
||||
```
|
||||
|
||||
and for `<PATH_TO_HOME_DIR>/.docker/config.json`:
|
||||
```json
|
||||
{
|
||||
"HttpHeaders" : {
|
||||
"User-Agent" : "Docker-Client/19.03.1 (XXXXXX)"
|
||||
},
|
||||
"credsStore" : "osxkeychain",
|
||||
"auths" : {
|
||||
"xyzxyzxyz.dkr.ecr.eu-north-1.amazonaws.com" : {},
|
||||
"https://index.docker.io/v1/": {}
|
||||
},
|
||||
"credHelpers": {
|
||||
"xyzxyzxyz.dkr.ecr.eu-north-1.amazonaws.com" : "ecr-login",
|
||||
"index.docker.io": "osxkeychain"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*Note:* `osxkeychain` can be changed to your prefered credentials helper.
|
@ -0,0 +1,53 @@
|
||||
## Executing commands before and after updating
|
||||
|
||||
> **DO NOTE**: These are shell commands executed with `sh`, and therefore require the
|
||||
> container to provide the `sh` executable.
|
||||
|
||||
It is possible to execute _pre/post\-check_ and _pre/post\-update_ commands
|
||||
**inside** every container updated by watchtower.
|
||||
|
||||
- The _pre-check_ command is executed for each container prior to every update cycle.
|
||||
- The _pre-update_ command is executed before stopping the container when an update is about to start.
|
||||
- The _post-update_ command is executed after restarting the updated container
|
||||
- The _post-check_ command is executed for each container post every update cycle.
|
||||
|
||||
This feature is disabled by default. To enable it, you need to set the option
|
||||
`--enable-lifecycle-hooks` on the command line, or set the environment variable
|
||||
`WATCHTOWER_LIFECYCLE_HOOKS` to `true`.
|
||||
|
||||
### Specifying update commands
|
||||
|
||||
The commands are specified using docker container labels, the following are currently available:
|
||||
|
||||
| Type | Docker Container Label |
|
||||
| ----------- | ------------------------------------------------------ |
|
||||
| Pre Check | `com.centurylinklabs.watchtower.lifecycle.pre-check` |
|
||||
| Pre Update | `com.centurylinklabs.watchtower.lifecycle.pre-update` |
|
||||
| Post Update | `com.centurylinklabs.watchtower.lifecycle.post-update` |
|
||||
| Post Check | `com.centurylinklabs.watchtower.lifecycle.post-check` |
|
||||
|
||||
These labels can be declared as instructions in a Dockerfile (with some example .sh files):
|
||||
|
||||
```docker
|
||||
LABEL com.centurylinklabs.watchtower.lifecycle.pre-check="/sync.sh"
|
||||
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="/dump-data.sh"
|
||||
LABEL com.centurylinklabs.watchtower.lifecycle.post-update="/restore-data.sh"
|
||||
LABEL com.centurylinklabs.watchtower.lifecycle.post-check="/send-heartbeat.sh"
|
||||
```
|
||||
|
||||
Or be specified as part of the `docker run` command line:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--label=com.centurylinklabs.watchtower.lifecycle.pre-check="/sync.sh" \
|
||||
--label=com.centurylinklabs.watchtower.lifecycle.pre-update="/dump-data.sh" \
|
||||
--label=com.centurylinklabs.watchtower.lifecycle.post-update="/restore-data.sh" \
|
||||
someimage
|
||||
--label=com.centurylinklabs.watchtower.lifecycle.post-check="/send-heartbeat.sh" \
|
||||
```
|
||||
|
||||
### Execution failure
|
||||
|
||||
The failure of a command to execute, identified by an exit code different than
|
||||
0, will not prevent watchtower from updating the container. Only an error
|
||||
log statement containing the exit code will be reported.
|
@ -1,3 +1,3 @@
|
||||
Watchtower will detect if there are links between any of the running containers and ensure that things are stopped/started in a way that won't break any of the links. If an update is detected for one of the dependencies in a group of linked containers, watchtower will stop and start all of the containers in the correct order so that the application comes back up correctly.
|
||||
Watchtower will detect if there are links between any of the running containers and ensures that things are stopped/started in a way that won't break any of the links. If an update is detected for one of the dependencies in a group of linked containers, watchtower will stop and start all of the containers in the correct order so that the application comes back up correctly.
|
||||
|
||||
For example, imagine you were running a _mysql_ container and a _wordpress_ container which had been linked to the _mysql_ container. If watchtower were to detect that the _mysql_ container required an update, it would first shut down the linked _wordpress_ container followed by the _mysql_ container. When restarting the containers it would handle _mysql_ first and then _wordpress_ to ensure that the link continued to work.
|
@ -0,0 +1,207 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"github.com/containrrr/watchtower/internal/util"
|
||||
"github.com/containrrr/watchtower/pkg/container"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Update looks at the running Docker containers to see if any of the images
|
||||
// used to start those containers have been updated. If a change is detected in
|
||||
// any of the images, the associated containers are stopped and restarted with
|
||||
// the new image.
|
||||
func Update(client container.Client, params UpdateParams) error {
|
||||
log.Debug("Checking containers for updated images")
|
||||
|
||||
executePreCheck(client, params)
|
||||
|
||||
containers, err := client.ListContainers(params.Filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, container := range containers {
|
||||
stale, err := client.IsContainerStale(container)
|
||||
if err != nil {
|
||||
log.Infof("Unable to update container %s. Proceeding to next.", containers[i].Name())
|
||||
log.Debug(err)
|
||||
stale = false
|
||||
}
|
||||
containers[i].Stale = stale
|
||||
}
|
||||
|
||||
containers, err = container.SortByDependencies(containers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
checkDependencies(containers)
|
||||
|
||||
if params.MonitorOnly {
|
||||
executePostCheck(client, params)
|
||||
return nil
|
||||
}
|
||||
|
||||
stopContainersInReversedOrder(containers, client, params)
|
||||
restartContainersInSortedOrder(containers, client, params)
|
||||
|
||||
executePostCheck(client, params)
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopContainersInReversedOrder(containers []container.Container, client container.Client, params UpdateParams) {
|
||||
for i := len(containers) - 1; i >= 0; i-- {
|
||||
stopStaleContainer(containers[i], client, params)
|
||||
}
|
||||
}
|
||||
|
||||
func stopStaleContainer(container container.Container, client container.Client, params UpdateParams) {
|
||||
if container.IsWatchtower() {
|
||||
log.Debugf("This is the watchtower container %s", container.Name())
|
||||
return
|
||||
}
|
||||
|
||||
if !container.Stale {
|
||||
return
|
||||
}
|
||||
|
||||
executePreUpdateCommand(client, container)
|
||||
|
||||
if err := client.StopContainer(container, params.Timeout); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func restartContainersInSortedOrder(containers []container.Container, client container.Client, params UpdateParams) {
|
||||
for _, container := range containers {
|
||||
if !container.Stale {
|
||||
continue
|
||||
}
|
||||
restartStaleContainer(container, client, params)
|
||||
}
|
||||
}
|
||||
|
||||
func restartStaleContainer(container container.Container, client container.Client, params UpdateParams) {
|
||||
// Since we can't shutdown a watchtower container immediately, we need to
|
||||
// start the new one while the old one is still running. This prevents us
|
||||
// from re-using the same container name so we first rename the current
|
||||
// instance so that the new one can adopt the old name.
|
||||
if container.IsWatchtower() {
|
||||
if err := client.RenameContainer(container, util.RandName()); err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !params.NoRestart {
|
||||
if newContainerID, err := client.StartContainer(container); err != nil {
|
||||
log.Error(err)
|
||||
} else if container.Stale && params.LifecycleHooks {
|
||||
executePostUpdateCommand(client, newContainerID)
|
||||
}
|
||||
}
|
||||
|
||||
if params.Cleanup {
|
||||
if err := client.RemoveImage(container); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkDependencies(containers []container.Container) {
|
||||
|
||||
for i, parent := range containers {
|
||||
if parent.ToRestart() {
|
||||
continue
|
||||
}
|
||||
|
||||
LinkLoop:
|
||||
for _, linkName := range parent.Links() {
|
||||
for _, child := range containers {
|
||||
if child.Name() == linkName && child.ToRestart() {
|
||||
containers[i].Linked = true
|
||||
break LinkLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func executePreCheck(client container.Client, params UpdateParams) {
|
||||
containers, err := client.ListContainers(params.Filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, container := range containers {
|
||||
executePreCheckCommand(client, container)
|
||||
}
|
||||
}
|
||||
|
||||
func executePostCheck(client container.Client, params UpdateParams) {
|
||||
containers, err := client.ListContainers(params.Filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, container := range containers {
|
||||
executePostCheckCommand(client, container)
|
||||
}
|
||||
}
|
||||
|
||||
func executePreCheckCommand(client container.Client, container container.Container) {
|
||||
command := container.GetLifecyclePreCheckCommand()
|
||||
if len(command) == 0 {
|
||||
log.Debug("No pre-check command supplied. Skipping")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Executing pre-check command.")
|
||||
if err := client.ExecuteCommand(container.ID(), command); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func executePostCheckCommand(client container.Client, container container.Container) {
|
||||
command := container.GetLifecyclePostCheckCommand()
|
||||
if len(command) == 0 {
|
||||
log.Debug("No post-check command supplied. Skipping")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Executing post-check command.")
|
||||
if err := client.ExecuteCommand(container.ID(), command); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func executePreUpdateCommand(client container.Client, container container.Container) {
|
||||
|
||||
command := container.GetLifecyclePreUpdateCommand()
|
||||
if len(command) == 0 {
|
||||
log.Debug("No pre-update command supplied. Skipping")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Executing pre-update command.")
|
||||
if err := client.ExecuteCommand(container.ID(), command); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func executePostUpdateCommand(client container.Client, newContainerID string) {
|
||||
newContainer, err := client.GetContainer(newContainerID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
command := newContainer.GetLifecyclePostUpdateCommand()
|
||||
if len(command) == 0 {
|
||||
log.Debug("No post-update command supplied. Skipping")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Executing post-update command.")
|
||||
if err := client.ExecuteCommand(newContainerID, command); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UpdateParams contains all different options available to alter the behavior of the Update func
|
||||
type UpdateParams struct {
|
||||
Filter t.Filter
|
||||
Cleanup bool
|
||||
NoRestart bool
|
||||
Timeout time.Duration
|
||||
MonitorOnly bool
|
||||
LifecycleHooks bool
|
||||
}
|
@ -0,0 +1,324 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// DockerAPIMinVersion is the minimum version of the docker api required to
|
||||
// use watchtower
|
||||
const DockerAPIMinVersion string = "1.24"
|
||||
|
||||
// RegisterDockerFlags that are used directly by the docker api client
|
||||
func RegisterDockerFlags(rootCmd *cobra.Command) {
|
||||
flags := rootCmd.PersistentFlags()
|
||||
flags.StringP("host", "H", viper.GetString("DOCKER_HOST"), "daemon socket to connect to")
|
||||
flags.BoolP("tlsverify", "v", viper.GetBool("DOCKER_TLS_VERIFY"), "use TLS and verify the remote")
|
||||
flags.StringP("api-version", "a", viper.GetString("DOCKER_API_VERSION"), "api version to use by docker client")
|
||||
}
|
||||
|
||||
// RegisterSystemFlags that are used by watchtower to modify the program flow
|
||||
func RegisterSystemFlags(rootCmd *cobra.Command) {
|
||||
flags := rootCmd.PersistentFlags()
|
||||
flags.IntP(
|
||||
"interval",
|
||||
"i",
|
||||
viper.GetInt("WATCHTOWER_POLL_INTERVAL"),
|
||||
"poll interval (in seconds)")
|
||||
|
||||
flags.StringP("schedule",
|
||||
"s",
|
||||
viper.GetString("WATCHTOWER_SCHEDULE"),
|
||||
"the cron expression which defines when to update")
|
||||
|
||||
flags.DurationP("stop-timeout",
|
||||
"t",
|
||||
viper.GetDuration("WATCHTOWER_TIMEOUT"),
|
||||
"timeout before a container is forcefully stopped")
|
||||
|
||||
flags.BoolP(
|
||||
"no-pull",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NO_PULL"),
|
||||
"do not pull any new images")
|
||||
|
||||
flags.BoolP(
|
||||
"no-restart",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NO_RESTART"),
|
||||
"do not restart any containers")
|
||||
|
||||
flags.BoolP(
|
||||
"cleanup",
|
||||
"c",
|
||||
viper.GetBool("WATCHTOWER_CLEANUP"),
|
||||
"remove previously used images after updating")
|
||||
|
||||
flags.BoolP(
|
||||
"remove-volumes",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"),
|
||||
"remove attached volumes before updating")
|
||||
|
||||
flags.BoolP(
|
||||
"label-enable",
|
||||
"e",
|
||||
viper.GetBool("WATCHTOWER_LABEL_ENABLE"),
|
||||
"watch containers where the com.centurylinklabs.watchtower.enable label is true")
|
||||
|
||||
flags.BoolP(
|
||||
"debug",
|
||||
"d",
|
||||
viper.GetBool("WATCHTOWER_DEBUG"),
|
||||
"enable debug mode with verbose logging")
|
||||
|
||||
flags.BoolP(
|
||||
"monitor-only",
|
||||
"m",
|
||||
viper.GetBool("WATCHTOWER_MONITOR_ONLY"),
|
||||
"Will only monitor for new images, not update the containers")
|
||||
|
||||
flags.BoolP(
|
||||
"run-once",
|
||||
"R",
|
||||
viper.GetBool("WATCHTOWER_RUN_ONCE"),
|
||||
"Run once now and exit")
|
||||
|
||||
flags.BoolP(
|
||||
"include-stopped",
|
||||
"S",
|
||||
viper.GetBool("WATCHTOWER_INCLUDE_STOPPED"),
|
||||
"Will also include created and exited containers")
|
||||
|
||||
flags.BoolP(
|
||||
"revive-stopped",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_REVIVE_STOPPED"),
|
||||
"Will also start stopped containers that were updated, if include-stopped is active")
|
||||
|
||||
flags.BoolP(
|
||||
"enable-lifecycle-hooks",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_LIFECYCLE_HOOKS"),
|
||||
"Enable the execution of commands triggered by pre- and post-update lifecycle hooks")
|
||||
}
|
||||
|
||||
// RegisterNotificationFlags that are used by watchtower to send notifications
|
||||
func RegisterNotificationFlags(rootCmd *cobra.Command) {
|
||||
flags := rootCmd.PersistentFlags()
|
||||
|
||||
flags.StringSliceP(
|
||||
"notifications",
|
||||
"n",
|
||||
viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"),
|
||||
" notification types to send (valid: email, slack, msteams, gotify)")
|
||||
|
||||
flags.StringP(
|
||||
"notifications-level",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"),
|
||||
"The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-from",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_FROM"),
|
||||
"Address to send notification emails from")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-to",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_TO"),
|
||||
"Address to send notification emails to")
|
||||
|
||||
flags.IntP(
|
||||
"notification-email-delay",
|
||||
"",
|
||||
viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_DELAY"),
|
||||
"Delay before sending notifications, expressed in seconds")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-server",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER"),
|
||||
"SMTP server to send notification emails through")
|
||||
|
||||
flags.IntP(
|
||||
"notification-email-server-port",
|
||||
"",
|
||||
viper.GetInt("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT"),
|
||||
"SMTP server port to send notification emails through")
|
||||
|
||||
flags.BoolP(
|
||||
"notification-email-server-tls-skip-verify",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_TLS_SKIP_VERIFY"),
|
||||
`
|
||||
Controls whether watchtower verifies the SMTP server's certificate chain and host name.
|
||||
Should only be used for testing.
|
||||
`)
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-server-user",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER"),
|
||||
"SMTP server user for sending notifications")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-server-password",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD"),
|
||||
"SMTP server password for sending notifications")
|
||||
|
||||
flags.StringP(
|
||||
"notification-email-subjecttag",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG"),
|
||||
"Subject prefix tag for notifications via mail")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-hook-url",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL"),
|
||||
"The Slack Hook URL to send notifications to")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-identifier",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER"),
|
||||
"A string which will be used to identify the messages coming from this watchtower instance")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-channel",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_CHANNEL"),
|
||||
"A string which overrides the webhook's default channel. Example: #my-custom-channel")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-icon-emoji",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_EMOJI"),
|
||||
"An emoji code string to use in place of the default icon")
|
||||
|
||||
flags.StringP(
|
||||
"notification-slack-icon-url",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_SLACK_ICON_URL"),
|
||||
"An icon image URL string to use in place of the default icon")
|
||||
|
||||
flags.StringP(
|
||||
"notification-msteams-hook",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL"),
|
||||
"The MSTeams WebHook URL to send notifications to")
|
||||
|
||||
flags.BoolP(
|
||||
"notification-msteams-data",
|
||||
"",
|
||||
viper.GetBool("WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA"),
|
||||
"The MSTeams notifier will try to extract log entry fields as MSTeams message facts")
|
||||
|
||||
flags.StringP(
|
||||
"notification-gotify-url",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_URL"),
|
||||
"The Gotify URL to send notifications to")
|
||||
flags.StringP(
|
||||
"notification-gotify-token",
|
||||
"",
|
||||
viper.GetString("WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN"),
|
||||
"The Gotify Application required to query the Gotify API")
|
||||
}
|
||||
|
||||
// SetDefaults provides default values for environment variables
|
||||
func SetDefaults() {
|
||||
viper.AutomaticEnv()
|
||||
viper.SetDefault("DOCKER_HOST", "unix:///var/run/docker.sock")
|
||||
viper.SetDefault("DOCKER_API_VERSION", DockerAPIMinVersion)
|
||||
viper.SetDefault("WATCHTOWER_POLL_INTERVAL", 300)
|
||||
viper.SetDefault("WATCHTOWER_TIMEOUT", time.Second*10)
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATIONS", []string{})
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATIONS_LEVEL", "info")
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT", 25)
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG", "")
|
||||
viper.SetDefault("WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER", "watchtower")
|
||||
}
|
||||
|
||||
// EnvConfig translates the command-line options into environment variables
|
||||
// that will initialize the api client
|
||||
func EnvConfig(cmd *cobra.Command) error {
|
||||
var err error
|
||||
var host string
|
||||
var tls bool
|
||||
var version string
|
||||
|
||||
flags := cmd.PersistentFlags()
|
||||
|
||||
if host, err = flags.GetString("host"); err != nil {
|
||||
return err
|
||||
}
|
||||
if tls, err = flags.GetBool("tlsverify"); err != nil {
|
||||
return err
|
||||
}
|
||||
if version, err = flags.GetString("api-version"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = setEnvOptStr("DOCKER_HOST", host); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = setEnvOptBool("DOCKER_TLS_VERIFY", tls); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = setEnvOptStr("DOCKER_API_VERSION", version); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadFlags reads common flags used in the main program flow of watchtower
|
||||
func ReadFlags(cmd *cobra.Command) (bool, bool, bool, time.Duration) {
|
||||
flags := cmd.PersistentFlags()
|
||||
|
||||
var err error
|
||||
var cleanup bool
|
||||
var noRestart bool
|
||||
var monitorOnly bool
|
||||
var timeout time.Duration
|
||||
|
||||
if cleanup, err = flags.GetBool("cleanup"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if noRestart, err = flags.GetBool("no-restart"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if monitorOnly, err = flags.GetBool("monitor-only"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if timeout, err = flags.GetDuration("stop-timeout"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return cleanup, noRestart, monitorOnly, timeout
|
||||
}
|
||||
|
||||
func setEnvOptStr(env string, opt string) error {
|
||||
if opt == "" || opt == os.Getenv(env) {
|
||||
return nil
|
||||
}
|
||||
err := os.Setenv(env, opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setEnvOptBool(env string, opt bool) error {
|
||||
if opt {
|
||||
return setEnvOptStr(env, "1")
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package flags
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEnvConfig_Defaults(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
|
||||
err := EnvConfig(cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "unix:///var/run/docker.sock", os.Getenv("DOCKER_HOST"))
|
||||
assert.Equal(t, "", os.Getenv("DOCKER_TLS_VERIFY"))
|
||||
assert.Equal(t, DockerAPIMinVersion, os.Getenv("DOCKER_API_VERSION"))
|
||||
}
|
||||
|
||||
func TestEnvConfig_Custom(t *testing.T) {
|
||||
cmd := new(cobra.Command)
|
||||
SetDefaults()
|
||||
RegisterDockerFlags(cmd)
|
||||
|
||||
err := cmd.ParseFlags([]string{"--host", "some-custom-docker-host", "--tlsverify", "--api-version", "1.99"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = EnvConfig(cmd)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "some-custom-docker-host", os.Getenv("DOCKER_HOST"))
|
||||
assert.Equal(t, "1", os.Getenv("DOCKER_TLS_VERIFY"))
|
||||
assert.Equal(t, "1.99", os.Getenv("DOCKER_API_VERSION"))
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package util
|
||||
|
||||
import "math/rand"
|
||||
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
// RandName Generates a random, 32-character, Docker-compatible container name.
|
||||
func RandName() string {
|
||||
b := make([]rune, 32)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"github.com/johntdyer/slackrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
slackType = "slack"
|
||||
)
|
||||
|
||||
type slackTypeNotifier struct {
|
||||
slackrus.SlackrusHook
|
||||
}
|
||||
|
||||
func newSlackNotifier(c *cli.Context, acceptedLogLevels []log.Level) typeNotifier {
|
||||
n := &slackTypeNotifier{
|
||||
SlackrusHook: slackrus.SlackrusHook{
|
||||
HookURL: c.GlobalString("notification-slack-hook-url"),
|
||||
Username: c.GlobalString("notification-slack-identifier"),
|
||||
Channel: c.GlobalString("notification-slack-channel"),
|
||||
IconEmoji: c.GlobalString("notification-slack-icon-emoji"),
|
||||
IconURL: c.GlobalString("notification-slack-icon-url"),
|
||||
AcceptedLevels: acceptedLogLevels,
|
||||
},
|
||||
}
|
||||
|
||||
log.AddHook(n)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *slackTypeNotifier) StartNotification() {}
|
||||
|
||||
func (s *slackTypeNotifier) SendNotification() {}
|
@ -0,0 +1,51 @@
|
||||
package container
|
||||
|
||||
const (
|
||||
watchtowerLabel = "com.centurylinklabs.watchtower"
|
||||
signalLabel = "com.centurylinklabs.watchtower.stop-signal"
|
||||
enableLabel = "com.centurylinklabs.watchtower.enable"
|
||||
zodiacLabel = "com.centurylinklabs.zodiac.original-image"
|
||||
preCheckLabel = "com.centurylinklabs.watchtower.lifecycle.pre-check"
|
||||
postCheckLabel = "com.centurylinklabs.watchtower.lifecycle.post-check"
|
||||
preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update"
|
||||
postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update"
|
||||
)
|
||||
|
||||
// GetLifecyclePreCheckCommand returns the pre-check command set in the container metadata or an empty string
|
||||
func (c Container) GetLifecyclePreCheckCommand() string {
|
||||
return c.getLabelValueOrEmpty(preCheckLabel)
|
||||
}
|
||||
|
||||
// GetLifecyclePostCheckCommand returns the post-check command set in the container metadata or an empty string
|
||||
func (c Container) GetLifecyclePostCheckCommand() string {
|
||||
return c.getLabelValueOrEmpty(postCheckLabel)
|
||||
}
|
||||
|
||||
// GetLifecyclePreUpdateCommand returns the pre-update command set in the container metadata or an empty string
|
||||
func (c Container) GetLifecyclePreUpdateCommand() string {
|
||||
return c.getLabelValueOrEmpty(preUpdateLabel)
|
||||
}
|
||||
|
||||
// GetLifecyclePostUpdateCommand returns the post-update command set in the container metadata or an empty string
|
||||
func (c Container) GetLifecyclePostUpdateCommand() string {
|
||||
return c.getLabelValueOrEmpty(postUpdateLabel)
|
||||
}
|
||||
|
||||
// ContainsWatchtowerLabel takes a map of labels and values and tells
|
||||
// the consumer whether it contains a valid watchtower instance label
|
||||
func ContainsWatchtowerLabel(labels map[string]string) bool {
|
||||
val, ok := labels[watchtowerLabel]
|
||||
return ok && val == "true"
|
||||
}
|
||||
|
||||
func (c Container) getLabelValueOrEmpty(label string) string {
|
||||
if val, ok := c.containerInfo.Config.Labels[label]; ok {
|
||||
return val
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c Container) getLabelValue(label string) (string, bool) {
|
||||
val, ok := c.containerInfo.Config.Labels[label]
|
||||
return val, ok
|
||||
}
|
@ -1,15 +1,11 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"os"
|
||||
"testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
func TestEncodedEnvAuth_ShouldReturnAnErrorIfRepoEnvsAreUnset(t *testing.T) {
|
||||
os.Unsetenv("REPO_USER")
|
||||
os.Unsetenv("REPO_PASS")
|
@ -0,0 +1,101 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
gotifyType = "gotify"
|
||||
)
|
||||
|
||||
type gotifyTypeNotifier struct {
|
||||
gotifyURL string
|
||||
gotifyAppToken string
|
||||
logLevels []log.Level
|
||||
}
|
||||
|
||||
func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
|
||||
flags := c.PersistentFlags()
|
||||
|
||||
gotifyURL, _ := flags.GetString("notification-gotify-url")
|
||||
if len(gotifyURL) < 1 {
|
||||
log.Fatal("Required argument --notification-gotify-url(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_URL(env) is empty.")
|
||||
} else if !(strings.HasPrefix(gotifyURL, "http://") || strings.HasPrefix(gotifyURL, "https://")) {
|
||||
log.Fatal("Gotify URL must start with \"http://\" or \"https://\"")
|
||||
} else if strings.HasPrefix(gotifyURL, "http://") {
|
||||
log.Warn("Using an HTTP url for Gotify is insecure")
|
||||
}
|
||||
|
||||
gotifyToken, _ := flags.GetString("notification-gotify-token")
|
||||
if len(gotifyToken) < 1 {
|
||||
log.Fatal("Required argument --notification-gotify-token(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN(env) is empty.")
|
||||
}
|
||||
|
||||
n := &gotifyTypeNotifier{
|
||||
gotifyURL: gotifyURL,
|
||||
gotifyAppToken: gotifyToken,
|
||||
logLevels: acceptedLogLevels,
|
||||
}
|
||||
|
||||
log.AddHook(n)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *gotifyTypeNotifier) StartNotification() {}
|
||||
|
||||
func (n *gotifyTypeNotifier) SendNotification() {}
|
||||
|
||||
func (n *gotifyTypeNotifier) Levels() []log.Level {
|
||||
return n.logLevels
|
||||
}
|
||||
|
||||
func (n *gotifyTypeNotifier) getURL() string {
|
||||
url := n.gotifyURL
|
||||
if !strings.HasSuffix(url, "/") {
|
||||
url += "/"
|
||||
}
|
||||
return url + "message?token=" + n.gotifyAppToken
|
||||
}
|
||||
|
||||
func (n *gotifyTypeNotifier) Fire(entry *log.Entry) error {
|
||||
|
||||
go func() {
|
||||
jsonBody, err := json.Marshal(gotifyMessage{
|
||||
Message: "(" + entry.Level.String() + "): " + entry.Message,
|
||||
Title: "Watchtower",
|
||||
Priority: 0,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Failed to create JSON body for Gotify notification: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonBodyBuffer := bytes.NewBuffer([]byte(jsonBody))
|
||||
resp, err := http.Post(n.getURL(), "application/json", jsonBodyBuffer)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to send Gotify notification: ", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
fmt.Printf("Gotify notification returned %d HTTP status code", resp.StatusCode)
|
||||
}
|
||||
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
type gotifyMessage struct {
|
||||
Message string `json:"message"`
|
||||
Title string `json:"title"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
"github.com/johntdyer/slackrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
slackType = "slack"
|
||||
)
|
||||
|
||||
type slackTypeNotifier struct {
|
||||
slackrus.SlackrusHook
|
||||
}
|
||||
|
||||
func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
|
||||
flags := c.PersistentFlags()
|
||||
|
||||
hookURL, _ := flags.GetString("notification-slack-hook-url")
|
||||
userName, _ := flags.GetString("notification-slack-identifier")
|
||||
channel, _ := flags.GetString("notification-slack-channel")
|
||||
emoji, _ := flags.GetString("notification-slack-icon-emoji")
|
||||
iconURL, _ := flags.GetString("notification-slack-icon-url")
|
||||
|
||||
n := &slackTypeNotifier{
|
||||
SlackrusHook: slackrus.SlackrusHook{
|
||||
HookURL: hookURL,
|
||||
Username: userName,
|
||||
Channel: channel,
|
||||
IconEmoji: emoji,
|
||||
IconURL: iconURL,
|
||||
AcceptedLevels: acceptedLogLevels,
|
||||
},
|
||||
}
|
||||
|
||||
log.AddHook(n)
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *slackTypeNotifier) StartNotification() {}
|
||||
|
||||
func (s *slackTypeNotifier) SendNotification() {}
|
@ -0,0 +1,5 @@
|
||||
package types
|
||||
|
||||
// A Filter is a prototype for a function that can be used to filter the
|
||||
// results from a call to the ListContainers() method on the Client.
|
||||
type Filter func(FilterableContainer) bool
|
@ -0,0 +1,9 @@
|
||||
package types
|
||||
|
||||
// A FilterableContainer is the interface which is used to filter
|
||||
// containers.
|
||||
type FilterableContainer interface {
|
||||
Name() string
|
||||
IsWatchtower() bool
|
||||
Enabled() (bool, bool)
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package types
|
||||
|
||||
// Notifier is the interface that all notification services have in common
|
||||
type Notifier interface {
|
||||
StartNotification()
|
||||
SendNotification()
|
||||
}
|
@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
IMAGE=server
|
||||
CONTAINER=server
|
||||
LINKED_IMAGE=linked
|
||||
LINKED_CONTAINER=linked
|
||||
WATCHTOWER_INTERVAL=2
|
||||
|
||||
function remove_container {
|
||||
docker kill $1 >> /dev/null || true && docker rm -v $1 >> /dev/null || true
|
||||
}
|
||||
|
||||
function cleanup {
|
||||
# Do cleanup on exit or error
|
||||
echo "Final cleanup"
|
||||
sleep 2
|
||||
remove_container $CONTAINER
|
||||
remove_container $LINKED_CONTAINER
|
||||
pkill -9 -f watchtower >> /dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
DEFAULT_WATCHTOWER="$(dirname "${BASH_SOURCE[0]}")/../watchtower"
|
||||
WATCHTOWER=$1
|
||||
WATCHTOWER=${WATCHTOWER:-$DEFAULT_WATCHTOWER}
|
||||
echo "watchtower path is $WATCHTOWER"
|
||||
|
||||
##################################################################################
|
||||
##### PREPARATION ################################################################
|
||||
##################################################################################
|
||||
|
||||
# Create Dockerfile template
|
||||
DOCKERFILE=$(cat << EOF
|
||||
FROM node:alpine
|
||||
|
||||
LABEL com.centurylinklabs.watchtower.lifecycle.pre-update="cat /opt/test/value.txt"
|
||||
LABEL com.centurylinklabs.watchtower.lifecycle.post-update="echo image > /opt/test/value.txt"
|
||||
|
||||
ENV IMAGE_TIMESTAMP=TIMESTAMP
|
||||
|
||||
WORKDIR /opt/test
|
||||
ENTRYPOINT ["/usr/local/bin/node", "/opt/test/server.js"]
|
||||
|
||||
EXPOSE 8888
|
||||
|
||||
RUN mkdir -p /opt/test && echo "default" > /opt/test/value.txt
|
||||
COPY server.js /opt/test/server.js
|
||||
EOF
|
||||
)
|
||||
|
||||
# Create temporary directory to build docker image
|
||||
TMP_DIR="/tmp/watchtower-commands-test"
|
||||
mkdir -p $TMP_DIR
|
||||
|
||||
# Create simple http server
|
||||
cat > $TMP_DIR/server.js << EOF
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
|
||||
http.createServer(function(request, response) {
|
||||
const fileContent = fs.readFileSync("/opt/test/value.txt");
|
||||
response.writeHead(200, {"Content-Type": "text/plain"});
|
||||
response.write(fileContent);
|
||||
response.end();
|
||||
}).listen(8888, () => { console.log('server is listening on 8888'); });
|
||||
EOF
|
||||
|
||||
function builddocker {
|
||||
TIMESTAMP=$(date +%s)
|
||||
echo "Building image $TIMESTAMP"
|
||||
echo "${DOCKERFILE/TIMESTAMP/$TIMESTAMP}" > $TMP_DIR/Dockerfile
|
||||
docker build $TMP_DIR -t $IMAGE >> /dev/null
|
||||
}
|
||||
|
||||
# Start watchtower
|
||||
echo "Starting watchtower"
|
||||
$WATCHTOWER -i $WATCHTOWER_INTERVAL --no-pull --stop-timeout 2s --enable-lifecycle-hooks $CONTAINER $LINKED_CONTAINER &
|
||||
sleep 3
|
||||
|
||||
echo "#################################################################"
|
||||
echo "##### TEST CASE 1: Execute commands from base image"
|
||||
echo "#################################################################"
|
||||
|
||||
# Build base image
|
||||
builddocker
|
||||
|
||||
# Run container
|
||||
docker run -d -p 0.0.0.0:8888:8888 --name $CONTAINER $IMAGE:latest >> /dev/null
|
||||
sleep 1
|
||||
echo "Container $CONTAINER is runnning"
|
||||
|
||||
# Test default value
|
||||
RESP=$(curl -s http://localhost:8888)
|
||||
if [ $RESP != "default" ]; then
|
||||
echo "Default value of container response is invalid" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build updated image to trigger watchtower update
|
||||
builddocker
|
||||
|
||||
WAIT_AMOUNT=$(($WATCHTOWER_INTERVAL * 3))
|
||||
echo "Wait for $WAIT_AMOUNT seconds"
|
||||
sleep $WAIT_AMOUNT
|
||||
|
||||
# Test value after post-update-command
|
||||
RESP=$(curl -s http://localhost:8888)
|
||||
if [[ $RESP != "image" ]]; then
|
||||
echo "Value of container response is invalid. Expected: image. Actual: $RESP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
remove_container $CONTAINER
|
||||
|
||||
echo "#################################################################"
|
||||
echo "##### TEST CASE 2: Execute commands from container and base image"
|
||||
echo "#################################################################"
|
||||
|
||||
# Build base image
|
||||
builddocker
|
||||
|
||||
# Run container
|
||||
docker run -d -p 0.0.0.0:8888:8888 \
|
||||
--label=com.centurylinklabs.watchtower.lifecycle.post-update="echo container > /opt/test/value.txt" \
|
||||
--name $CONTAINER $IMAGE:latest >> /dev/null
|
||||
sleep 1
|
||||
echo "Container $CONTAINER is runnning"
|
||||
|
||||
# Test default value
|
||||
RESP=$(curl -s http://localhost:8888)
|
||||
if [ $RESP != "default" ]; then
|
||||
echo "Default value of container response is invalid" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build updated image to trigger watchtower update
|
||||
builddocker
|
||||
|
||||
WAIT_AMOUNT=$(($WATCHTOWER_INTERVAL * 3))
|
||||
echo "Wait for $WAIT_AMOUNT seconds"
|
||||
sleep $WAIT_AMOUNT
|
||||
|
||||
# Test value after post-update-command
|
||||
RESP=$(curl -s http://localhost:8888)
|
||||
if [[ $RESP != "container" ]]; then
|
||||
echo "Value of container response is invalid. Expected: container. Actual: $RESP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
remove_container $CONTAINER
|
||||
|
||||
echo "#################################################################"
|
||||
echo "##### TEST CASE 3: Execute commands with a linked container"
|
||||
echo "#################################################################"
|
||||
|
||||
# Tag the current image to keep a version for the linked container
|
||||
docker tag $IMAGE:latest $LINKED_IMAGE:latest
|
||||
|
||||
# Build base image
|
||||
builddocker
|
||||
|
||||
# Run container
|
||||
docker run -d -p 0.0.0.0:8888:8888 \
|
||||
--label=com.centurylinklabs.watchtower.lifecycle.post-update="echo container > /opt/test/value.txt" \
|
||||
--name $CONTAINER $IMAGE:latest >> /dev/null
|
||||
docker run -d -p 0.0.0.0:8989:8888 \
|
||||
--label=com.centurylinklabs.watchtower.lifecycle.post-update="echo container > /opt/test/value.txt" \
|
||||
--link $CONTAINER \
|
||||
--name $LINKED_CONTAINER $LINKED_IMAGE:latest >> /dev/null
|
||||
sleep 1
|
||||
echo "Container $CONTAINER and $LINKED_CONTAINER are runnning"
|
||||
|
||||
# Test default value
|
||||
RESP=$(curl -s http://localhost:8888)
|
||||
if [ $RESP != "default" ]; then
|
||||
echo "Default value of container response is invalid" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test default value for linked container
|
||||
RESP=$(curl -s http://localhost:8989)
|
||||
if [ $RESP != "default" ]; then
|
||||
echo "Default value of linked container response is invalid" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build updated image to trigger watchtower update
|
||||
builddocker
|
||||
|
||||
WAIT_AMOUNT=$(($WATCHTOWER_INTERVAL * 3))
|
||||
echo "Wait for $WAIT_AMOUNT seconds"
|
||||
sleep $WAIT_AMOUNT
|
||||
|
||||
# Test value after post-update-command
|
||||
RESP=$(curl -s http://localhost:8888)
|
||||
if [[ $RESP != "container" ]]; then
|
||||
echo "Value of container response is invalid. Expected: container. Actual: $RESP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test that linked container did not execute pre/post-update-command
|
||||
RESP=$(curl -s http://localhost:8989)
|
||||
if [[ $RESP != "default" ]]; then
|
||||
echo "Value of linked container response is invalid. Expected: default. Actual: $RESP"
|
||||
exit 1
|
||||
fi
|
Loading…
Reference in New Issue