Merge branch 'master' into all-contributors/add-zoispag

pull/422/head
Simon Aronsson 5 years ago committed by GitHub
commit 5927c6c2a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -293,6 +293,16 @@
"doc"
]
},
{
"login": "lukapeschke",
"name": "Luka Peschke",
"avatar_url": "https://avatars1.githubusercontent.com/u/17085536?v=4",
"profile": "https://github.com/lukapeschke",
"contributions": [
"code",
"doc"
]
},
{
"login": "zoispag",
"name": "Zois Pagoulatos",
@ -302,6 +312,97 @@
"code",
"review"
]
},
{
"login": "alexandremenif",
"name": "Alexandre Menif",
"avatar_url": "https://avatars0.githubusercontent.com/u/16152103?v=4",
"profile": "https://alexandre.menif.name",
"contributions": [
"code"
]
},
{
"login": "chugunov",
"name": "Andrey",
"avatar_url": "https://avatars1.githubusercontent.com/u/4140479?v=4",
"profile": "https://github.com/chugunov",
"contributions": [
"doc"
]
},
{
"login": "noplanman",
"name": "Armando Lüscher",
"avatar_url": "https://avatars3.githubusercontent.com/u/9423417?v=4",
"profile": "https://noplanman.ch",
"contributions": [
"doc"
]
},
{
"login": "rjbudke",
"name": "Ryan Budke",
"avatar_url": "https://avatars2.githubusercontent.com/u/273485?v=4",
"profile": "https://github.com/rjbudke",
"contributions": [
"doc"
]
},
{
"login": "kaloyan-raev",
"name": "Kaloyan Raev",
"avatar_url": "https://avatars2.githubusercontent.com/u/468091?v=4",
"profile": "http://kaloyan.raev.name",
"contributions": [
"code",
"test"
]
},
{
"login": "sixth",
"name": "sixth",
"avatar_url": "https://avatars3.githubusercontent.com/u/11591445?v=4",
"profile": "https://github.com/sixth",
"contributions": [
"doc"
]
},
{
"login": "foosel",
"name": "Gina Häußge",
"avatar_url": "https://avatars0.githubusercontent.com/u/83657?v=4",
"profile": "https://foosel.net",
"contributions": [
"code"
]
},
{
"login": "8ear",
"name": "Max H.",
"avatar_url": "https://avatars0.githubusercontent.com/u/10329648?v=4",
"profile": "https://github.com/8ear",
"contributions": [
"code"
]
},
{
"login": "pjknkda",
"name": "Jungkook Park",
"avatar_url": "https://avatars0.githubusercontent.com/u/4986524?v=4",
"profile": "https://pjknkda.github.io",
"contributions": [
"doc"
]
},
{
"login": "jnidzwetzki",
"name": "Jan Kristof Nidzwetzki",
"avatar_url": "https://avatars1.githubusercontent.com/u/5753622?v=4",
"profile": "https://achfrag.net",
"contributions": [
"doc"
]
}
],
"contributorsPerLine": 7,

@ -87,8 +87,7 @@ jobs:
at: .
- run: go build ./...
- run: go get github.com/schrej/godacov
- run: go get -u github.com/haya14busa/goverage
- run: goverage -v -coverprofile=coverage.out ./...
- run: go test ./... -coverprofile coverage.out
- run: godacov -t $CODACY_TOKEN -r ./coverage.out -c $CIRCLE_SHA1
build:
executor: go
@ -224,10 +223,16 @@ jobs:
- run:
name: Install prerequisites
command: |
pip install \
sudo pip install \
mkdocs \
mkdocs-material \
md-toc
- add_ssh_keys:
fingerprints:
- "91:75:47:15:b2:8e:85:e5:67:0e:63:7f:22:d2:b4:6e"
- run:
name: Generate and publish
command: mkdocs gh-deploy
command: |
mkdir ~/.ssh && touch ~/.ssh/known_hosts;
ssh-keyscan -H github.com >> ~/.ssh/known_hosts && \
mkdocs gh-deploy

@ -1 +1,2 @@
custom: https://www.amazon.com/hz/wishlist/ls/F94JJV822VX6
github: simskij

11
.github/config.yml vendored

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

6
.github/stale.yml vendored

@ -2,8 +2,10 @@ daysUntilStale: 60
daysUntilClose: 7
exemptMilestones: true
exemptLabels:
- pinned
- security
- "Public Service Announcement"
- "Do not close"
- "Type: Bug"
- "Type: Security"
staleLabel: "Status: Stale"
markComment: >
This issue has been automatically marked as stale because it has not had

@ -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! 🙏🏼

@ -38,6 +38,9 @@
<a href="#contributors">
<img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-30-orange.svg?style=flat-square" />
</a>
<a href="https://hub.docker.com/r/containrrr/watchtower">
<img alt="Pulls from DockerHub" src="https://img.shields.io/docker/pulls/containrrr/watchtower.svg?style=flat-square" />
</a>
</p>
## Quick Start
@ -102,7 +105,22 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://markwoodbridge.com"><img src="https://avatars2.githubusercontent.com/u/1101318?v=4" width="100px;" alt=""/><br /><sub><b>Mark Woodbridge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mrw34" title="Code">💻</a></td>
<td align="center"><a href="http://www.arcticbit.se"><img src="https://avatars0.githubusercontent.com/u/1596025?v=4" width="100px;" alt=""/><br /><sub><b>Simon Aronsson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Code">💻</a> <a href="#maintenance-simskij" title="Maintenance">🚧</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Asimskij" title="Reviewed Pull Requests">👀</a></td>
<td align="center"><a href="https://github.com/Ansem93"><img src="https://avatars3.githubusercontent.com/u/6626218?v=4" width="100px;" alt=""/><br /><sub><b>Ansem93</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Ansem93" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/lukapeschke"><img src="https://avatars1.githubusercontent.com/u/17085536?v=4" width="100px;" alt=""/><br /><sub><b>Luka Peschke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/zoispag"><img src="https://avatars0.githubusercontent.com/u/21138205?v=4" width="100px;" alt=""/><br /><sub><b>Zois Pagoulatos</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zoispag" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/pulls?q=is%3Apr+reviewed-by%3Azoispag" title="Reviewed Pull Requests">👀</a></td>
<td align="center"><a href="https://alexandre.menif.name"><img src="https://avatars0.githubusercontent.com/u/16152103?v=4" width="100px;" alt=""/><br /><sub><b>Alexandre Menif</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=alexandremenif" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/chugunov"><img src="https://avatars1.githubusercontent.com/u/4140479?v=4" width="100px;" alt=""/><br /><sub><b>Andrey</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=chugunov" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://noplanman.ch"><img src="https://avatars3.githubusercontent.com/u/9423417?v=4" width="100px;" alt=""/><br /><sub><b>Armando Lüscher</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=noplanman" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/rjbudke"><img src="https://avatars2.githubusercontent.com/u/273485?v=4" width="100px;" alt=""/><br /><sub><b>Ryan Budke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=rjbudke" title="Documentation">📖</a></td>
<td align="center"><a href="http://kaloyan.raev.name"><img src="https://avatars2.githubusercontent.com/u/468091?v=4" width="100px;" alt=""/><br /><sub><b>Kaloyan Raev</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=kaloyan-raev" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/sixth"><img src="https://avatars3.githubusercontent.com/u/11591445?v=4" width="100px;" alt=""/><br /><sub><b>sixth</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=sixth" title="Documentation">📖</a></td>
<td align="center"><a href="https://foosel.net"><img src="https://avatars0.githubusercontent.com/u/83657?v=4" width="100px;" alt=""/><br /><sub><b>Gina Häußge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=foosel" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/8ear"><img src="https://avatars0.githubusercontent.com/u/10329648?v=4" width="100px;" alt=""/><br /><sub><b>Max H.</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=8ear" title="Code">💻</a></td>
<td align="center"><a href="https://pjknkda.github.io"><img src="https://avatars0.githubusercontent.com/u/4986524?v=4" width="100px;" alt=""/><br /><sub><b>Jungkook Park</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=pjknkda" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://achfrag.net"><img src="https://avatars1.githubusercontent.com/u/5753622?v=4" width="100px;" alt=""/><br /><sub><b>Jan Kristof Nidzwetzki</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=jnidzwetzki" title="Documentation">📖</a></td>
</tr>
</table>

@ -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()
}

@ -12,8 +12,8 @@ $ docker run -d \
```
In the example above, watchtower will only monitor the containers named "nginx" and "redis" for updates -- all of the other
running containers will be ignored. If you do not want watchtower to run as a daemon you can pass a run-once flag and remove
the watchtower container after it's execution.
running containers will be ignored. If you do not want watchtower to run as a daemon you can pass the `--run-once` flag and remove
the watchtower container after its execution.
```bash
$ docker run --rm \
@ -23,7 +23,7 @@ $ docker run --rm \
nginx redis
```
In the example above, watchtower will execute an upgrade attempt on the containers named "nginx" and "redis". Using this mode will enable debugging output showing all actions performed as usage is intended for interactive users. Once the attempt is completed, the container will exit and remove itself due to the "--rm" flag.
In the example above, watchtower will execute an upgrade attempt on the containers named "nginx" and "redis". Using this mode will enable debugging output showing all actions performed, as usage is intended for interactive users. Once the attempt is completed, the container will exit and remove itself due to the `--rm` flag.
When no arguments are specified, watchtower will monitor all running containers.
@ -47,12 +47,22 @@ Environment Variable: WATCHTOWER_CLEANUP
Default: false
```
## Remove attached volumes
Removes attached volumes after updating. When this flag is specified, watchtower will remove all attached volumes from the container before restarting with a new image. Use this option to force new volumes to be populated as containers are updated.
```
Argument: --remove-volumes
Environment Variable: WATCHTOWER_REMOVE_VOLUMES
Type: Boolean
Default: false
```
## Debug
Enable debug mode with verbose logging.
```
Argument: --debug
Environment Variable: N/A
Environment Variable: WATCHTOWER_DEBUG
Type: Boolean
Default: false
```
@ -65,7 +75,17 @@ Docker daemon socket to connect to. Can be pointed at a remote Docker host by sp
Environment Variable: DOCKER_HOST
Type: String
Default: "unix:///var/run/docker.sock"
```
```
## Docker API version
The API version to use by the Docker client for connecting to the Docker daemon. The minimum supported version is 1.24.
```
Argument: --api-version, -a
Environment Variable: DOCKER_API_VERSION
Type: String
Default: "1.24"
```
## Include stopped
Will also include created and exited containers.
@ -77,18 +97,28 @@ Environment Variable: WATCHTOWER_INCLUDE_STOPPED
Default: false
```
## Poll Interval
## Revive stopped
Start any stopped containers that have had their image updated. This argument is only usable with the `--include-stopped` argument.
```
Argument: --revive-stopped
Environment Variable: WATCHTOWER_REVIVE_STOPPED
Type: Boolean
Default: false
```
## Poll interval
Poll interval (in seconds). This value controls how frequently watchtower will poll for new images.
```
Argument: ---interval, -i
Argument: --interval, -i
Environment Variable: WATCHTOWER_POLL_INTERVAL
Type: Integer
Default: 300
```
## Filter by enable label
Watch containers where the `com.centurylinklabs.watchtower.enable` label is set to true.
Update containers that have a `com.centurylinklabs.watchtower.enable` label set to true.
```
Argument: --label-enable
@ -107,6 +137,16 @@ Environment Variable: WATCHTOWER_MONITOR_ONLY
Default: false
```
## Without restarting containers
Do not restart containers after updating. This option can be useful when the start of the containers
is managed by an external system such as systemd.
```
Argument: --no-restart
Environment Variable: WATCHTOWER_NO_RESTART
Type: Boolean
Default: false
```
## Without pulling new images
Do not pull new images. When this flag is specified, watchtower will not attempt to pull
new images from the registry. Instead it will only monitor the local image cache for changes.

@ -12,7 +12,7 @@ Or, it can be specified as part of the `docker run` command line:
docker run -d --label=com.centurylinklabs.watchtower.enable=false someimage
```
If you need to only include only some containers, pass the --label-enable flag on startup and set the _com.centurylinklabs.watchtower.enable_ label with a value of true for the containers you want to watch.
If you need to include only some containers, pass the `--label-enable` flag on startup and set the _com.centurylinklabs.watchtower.enable_ label with a value of `true` for the containers you want to watch.
```docker
LABEL com.centurylinklabs.watchtower.enable="true"

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

@ -1,12 +1,19 @@
# Notifications
Watchtower can send notifications when containers are updated. Notifications are sent via hooks in the logging system, [logrus](http://github.com/sirupsen/logrus).
The types of notifications to send are passed via the comma-separated option `--notifications` (or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values:
The types of notifications to send are set by passing a comma-separated list of values to the `--notifications` option (or corresponding environment variable `WATCHTOWER_NOTIFICATIONS`), which has the following valid values:
- `email` to send notifications via e-mail
- `slack` to send notifications through a Slack webhook
- `msteams` to send notifications via MSTeams webhook
- `gotify` to send notifications via Gotify
> There is currently a [bug](https://github.com/spf13/viper/issues/380) in Viper, which prevents comma-separated slices to be used when using the environment variable. A workaround is available where we instead put quotes around the environment variable value and replace the commas with spaces, as `WATCHTOWER_NOTIFICATIONS="slack msteams"`
> If you're a `docker-compose` user, make sure to specify environment variables' values in your `.yml` file without double quotes (`"`).
>
> This prevents unexpected errors when watchtower starts.
## Settings
@ -25,6 +32,8 @@ To receive notifications by email, the following command-line options, or their
- `--notification-email-server-port` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT`): The port used to connect to the SMTP server to send e-mails through. Defaults to `25`.
- `--notification-email-server-user` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER`): The username to authenticate with the SMTP server with.
- `--notification-email-server-password` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD`): The password to authenticate with the SMTP server with.
- `--notification-email-delay` (env. `WATCHTOWER_NOTIFICATION_EMAIL_DELAY`): Delay before sending notifications expressed in seconds.
- `--notification-email-subjecttag` (env. `WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG`): Prefix to include in the subject tag. Useful when running multiple watchtowers.
Example:
@ -38,6 +47,7 @@ docker run -d \
-e WATCHTOWER_NOTIFICATION_EMAIL_SERVER=smtp.gmail.com \
-e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=fromaddress@gmail.com \
-e WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=app_password \
-e WATCHTOWER_NOTIFICATION_EMAIL_DELAY=2 \
containrrr/watchtower
```
@ -46,7 +56,7 @@ If watchtower is monitoring the same Docker daemon under which the watchtower co
To receive notifications in Slack, add `slack` to the `--notifications` option or the `WATCHTOWER_NOTIFICATIONS` environment variable.
Additionally, you should set the Slack webhook url using the `--notification-slack-hook-url` option or the `WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL` environment variable.
Additionally, you should set the Slack webhook URL using the `--notification-slack-hook-url` option or the `WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL` environment variable.
By default, watchtower will send messages under the name `watchtower`, you can customize this string through the `--notification-slack-identifier` option or the `WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER` environment variable.
@ -75,7 +85,7 @@ docker run -d \
To receive notifications in MSTeams channel, add `msteams` to the `--notifications` option or the `WATCHTOWER_NOTIFICATIONS` environment variable.
Additionally, you should set the MSTeams webhook url using the `--notification-msteams-hook` option or the `WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL` environment variable.
Additionally, you should set the MSTeams webhook URL using the `--notification-msteams-hook` option or the `WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL` environment variable.
MSTeams notifier could send keys/values filled by `log.WithField` or `log.WithFields` as MSTeams message facts. To enable this feature add `--notification-msteams-data` flag or set `WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA=true` environment variable.
@ -90,3 +100,18 @@ docker run -d \
-e WATCHTOWER_NOTIFICATION_MSTEAMS_USE_LOG_DATA=true \
containrrr/watchtower
```
### Gotify
To push a notification to your Gotify instance, register a Gotify app and specify the Gotify URL and app token:
```bash
docker run -d \
--name watchtower \
-v /var/run/docker.sock:/var/run/docker.sock \
-e WATCHTOWER_NOTIFICATIONS=gotify \
-e WATCHTOWER_NOTIFICATION_GOTIFY_URL="https://my.gotify.tld/" \
-e WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN="SuperSecretToken" \
containrrr/watchtower
```

@ -1,6 +1,6 @@
Watchtower is itself packaged as a Docker container so installation is as simple as pulling the `containrrr/watchtower` image. If you are using ARM based architecture, pull the appropriate `containrrr/watchtower:armhf-<tag>` image from the [containrrr Docker Hub](https://hub.docker.com/r/containrrr/watchtower/tags/).
Since the watchtower code needs to interact with the Docker API in order to monitor the running containers, you need to mount _/var/run/docker.sock_ into the container with the -v flag when you run it.
Since the watchtower code needs to interact with the Docker API in order to monitor the running containers, you need to mount _/var/run/docker.sock_ into the container with the `-v` flag when you run it.
Run the `watchtower` container with the following command:
@ -37,7 +37,7 @@ docker run -d \
containrrr/watchtower container_to_watch --debug
```
If you mount the config file as described above, be sure to also prepend the url for the registry when starting up your watched image (you can omit the https://). Here is a complete docker-compose.yml file that starts up a docker container from a private repo at dockerhub and monitors it with watchtower. Note the command argument changing the interval to 30s rather than the default 5 minutes.
If you mount the config file as described above, be sure to also prepend the URL for the registry when starting up your watched image (you can omit the https://). Here is a complete docker-compose.yml file that starts up a docker container from a private repo at Docker Hub and monitors it with watchtower. Note the command argument changing the interval to 30s rather than the default 5 minutes.
```json
version: "3"

@ -6,7 +6,7 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78
github.com/Microsoft/go-winio v0.4.12
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973
github.com/beorn7/perks v1.0.0
github.com/brysgo/gomock_ginkgo v0.0.0-20180512161304-be2c1b0e4111
github.com/containerd/containerd v1.2.6 // indirect
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808
@ -23,7 +23,6 @@ require (
github.com/gogo/protobuf v1.2.1
github.com/golang/mock v1.1.1
github.com/golang/protobuf v1.3.1
github.com/google/go-cmp v0.2.0 // indirect
github.com/gorilla/mux v1.7.0
github.com/hashicorp/go-memdb v1.0.0 // indirect
github.com/hashicorp/go-version v1.1.0
@ -44,24 +43,23 @@ require (
github.com/opencontainers/selinux v1.2.1 // indirect
github.com/pkg/errors v0.8.1
github.com/pmezard/go-difflib v1.0.0
github.com/prometheus/client_golang v0.9.2
github.com/prometheus/client_golang v0.9.3
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90
github.com/prometheus/common v0.2.0
github.com/prometheus/procfs v0.0.0-20190403104016-ea9eea638872
github.com/prometheus/common v0.4.0
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/sirupsen/logrus v1.4.1
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3
github.com/spf13/viper v1.4.0
github.com/stretchr/objx v0.1.1
github.com/stretchr/testify v1.3.0
github.com/theupdateframework/notary v0.6.1
github.com/urfave/cli v1.20.0
github.com/vbatts/tar-split v0.11.1 // indirect
golang.org/x/crypto v0.0.0-20190403202508-8e1b8d32e692
golang.org/x/net v0.0.0-20190403144856-b630fd6fe46b
golang.org/x/net v0.0.0-20190522155817-f3200d17e092
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
google.golang.org/genproto v0.0.0-20190401181712-f467c93bbac2
google.golang.org/grpc v1.19.1
google.golang.org/grpc v1.21.0
gotest.tools v2.2.0+incompatible // indirect
)

@ -4,22 +4,34 @@ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc=
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI=
github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/brysgo/gomock_ginkgo v0.0.0-20180512161304-be2c1b0e4111 h1:gRfsoKtF1tba+hVsNgo7OKG7a35hBK30ouOTHPgqFf8=
github.com/brysgo/gomock_ginkgo v0.0.0-20180512161304-be2c1b0e4111/go.mod h1:H1ipqq0hhUWJgVeQ5dbUe/C8YptJrE/VGDQp9bI+qTo=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/containerd v1.2.6 h1:K38ZSAA9oKSrX3iFNY+4SddZ8hH1TCMCerc8NHfcKBQ=
github.com/containerd/containerd v1.2.6/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882bXEDKfWIf0wa8HRvpnBoPszJJXL+TVbBw4M=
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/cli v0.0.0-20190327152802-57b27434ea29 h1:ciaXDHaWQda0nvevWqcjtXX/buQY3e0lga1vq8Batq0=
github.com/docker/cli v0.0.0-20190327152802-57b27434ea29/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
@ -40,21 +52,30 @@ github.com/docker/swarmkit v1.12.0 h1:vcbNXevt9xOod0miQxkp9WZ70IsOCe8geXkmFnXP2e
github.com/docker/swarmkit v1.12.0/go.mod h1:n3Z4lIEl7g261ptkGDBcYi/3qBMDl9csaAhwi2MPejs=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U=
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-memdb v1.0.0 h1:K1O4N2VPndZiTrdH3lmmf5bemr9Xw81KjVwhReIUjTQ=
github.com/hashicorp/go-memdb v1.0.0/go.mod h1:I6dKdmYhZqU0RJSheVEWgTNWdVQH5QvTgIUQ0t/t32M=
@ -62,6 +83,8 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
@ -70,6 +93,7 @@ github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22 h1:jKUP9TQ0c7X3
github.com/johntdyer/slack-go v0.0.0-20180213144715-95fac1160b22/go.mod h1:u0Jo4f2dNlTJeeOywkM6bLwxq6gC3pZ9rEFHn3AhTdk=
github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07 h1:+kBG/8rjCa6vxJZbUjAiE4MQmBEBYc8nLEb51frnvBY=
github.com/johntdyer/slackrus v0.0.0-20180518184837-f7aae3243a07/go.mod h1:j1kV/8f3jowErEq4XyeypkCdvg5EeHkf0YCKCcq5Ybo=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@ -77,13 +101,21 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/pkcs11 v0.0.0-20190401114359-553cfdd26aaa/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE=
github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -98,6 +130,8 @@ github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59P
github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/selinux v1.2.1 h1:Svlc+L67YcjN4K2bqD8Wlw9jtMlmZ+1FEGn6zsm8am0=
github.com/opencontainers/selinux v1.2.1/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -106,25 +140,44 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190403104016-ea9eea638872 h1:0aNv3xC7DmQoy1/x1sMh18g+fihWW68LL13i8ao9kl4=
github.com/prometheus/procfs v0.0.0-20190403104016-ea9eea638872/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 h1:x7xEyJDP7Hv3LVgvWhzioQqbC/KtuUhTigKlH/8ehhE=
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -133,9 +186,15 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0=
github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/vbatts/tar-split v0.11.1/go.mod h1:LEuURwDEiWjRjwu46yU3KVGuUdVv/dcnpcEPSzR8z6g=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190403202508-8e1b8d32e692 h1:GRhHqDOgeDr6QDTtq9gn2O4iKvm5dsbfqD/TXb0KLX0=
@ -143,14 +202,19 @@ golang.org/x/crypto v0.0.0-20190403202508-8e1b8d32e692/go.mod h1:WFFai1msRO1wXaE
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190403144856-b630fd6fe46b h1:/zjbcJPEGAyu6Is/VBOALsgdi4z9+kz/Vtdm6S+beD0=
golang.org/x/net v0.0.0-20190403144856-b630fd6fe46b/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -160,16 +224,19 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@ -178,12 +245,20 @@ google.golang.org/genproto v0.0.0-20190401181712-f467c93bbac2/go.mod h1:VzzqZJRn
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM=
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

@ -2,14 +2,15 @@ package actions_test
import (
"errors"
"github.com/containrrr/watchtower/internal/actions"
"testing"
"time"
"github.com/containrrr/watchtower/actions"
"github.com/containrrr/watchtower/container"
"github.com/containrrr/watchtower/container/mocks"
"github.com/containrrr/watchtower/pkg/container"
"github.com/containrrr/watchtower/pkg/container/mocks"
"github.com/docker/docker/api/types"
t "github.com/containrrr/watchtower/pkg/types"
cli "github.com/docker/docker/client"
. "github.com/onsi/ginkgo"
@ -32,9 +33,10 @@ var _ = Describe("the actions package", func() {
})
BeforeEach(func() {
client = mockClient{
api: dockerClient,
pullImages: false,
TestData: &TestData{},
api: dockerClient,
pullImages: false,
removeVolumes: false,
TestData: &TestData{},
}
})
@ -62,8 +64,9 @@ var _ = Describe("the actions package", func() {
When("given multiple containers", func() {
BeforeEach(func() {
client = mockClient{
api: dockerClient,
pullImages: false,
api: dockerClient,
pullImages: false,
removeVolumes: false,
TestData: &TestData{
NameOfContainerToKeep: "test-container-02",
Containers: []container.Container{
@ -89,8 +92,9 @@ var _ = Describe("the actions package", func() {
When("deciding whether to cleanup images", func() {
BeforeEach(func() {
client = mockClient{
api: dockerClient,
pullImages: false,
api: dockerClient,
pullImages: false,
removeVolumes: false,
TestData: &TestData{
Containers: []container.Container{
createMockContainer(
@ -134,9 +138,10 @@ func createMockContainer(id string, name string, image string, created time.Time
}
type mockClient struct {
TestData *TestData
api cli.CommonAPIClient
pullImages bool
TestData *TestData
api cli.CommonAPIClient
pullImages bool
removeVolumes bool
}
type TestData struct {
@ -145,7 +150,7 @@ type TestData struct {
Containers []container.Container
}
func (client mockClient) ListContainers(f container.Filter) ([]container.Container, error) {
func (client mockClient) ListContainers(f t.Filter) ([]container.Container, error) {
return client.TestData.Containers, nil
}
@ -155,7 +160,7 @@ func (client mockClient) StopContainer(c container.Container, d time.Duration) e
}
return nil
}
func (client mockClient) StartContainer(c container.Container) error {
func (client mockClient) StartContainer(c container.Container) (string, error) {
panic("Not implemented")
}
@ -168,6 +173,14 @@ func (client mockClient) RemoveImage(c container.Container) error {
return nil
}
func (client mockClient) GetContainer(containerID string) (container.Container, error) {
return container.Container{}, nil
}
func (client mockClient) ExecuteCommand(containerID string, command string) error {
return nil
}
func (client mockClient) IsContainerStale(c container.Container) (bool, error) {
panic("Not implemented")
}

@ -11,7 +11,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/containrrr/watchtower/container"
"github.com/containrrr/watchtower/pkg/container"
)
// CheckForMultipleWatchtowerInstances will ensure that there are not multiple instances of the
@ -50,7 +50,7 @@ func cleanupExcessWatchtowers(containers []container.Container, client container
continue
}
if cleanup == true {
if cleanup {
if err := client.RemoveImage(c); err != nil {
// logging the original here as we're just returning a count
logrus.Error(err)
@ -79,6 +79,6 @@ func createErrorIfAnyHaveOccurred(c int, i int) error {
}
func awaitDockerClient() {
log.Debug("Sleeping for a seconds to ensure the docker api client has been properly initialized.")
log.Debug("Sleeping for a second to ensure the docker api client has been properly initialized.")
time.Sleep(1 * time.Second)
}

@ -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,6 +1,7 @@
package container
package util
func sliceEqual(s1, s2 []string) bool {
// SliceEqual compares two slices and checks whether they have equal content
func SliceEqual(s1, s2 []string) bool {
if len(s1) != len(s2) {
return false
}
@ -14,7 +15,8 @@ func sliceEqual(s1, s2 []string) bool {
return true
}
func sliceSubtract(a1, a2 []string) []string {
// SliceSubtract subtracts the content of slice a2 from slice a1
func SliceSubtract(a1, a2 []string) []string {
a := []string{}
for _, e1 := range a1 {
@ -35,7 +37,8 @@ func sliceSubtract(a1, a2 []string) []string {
return a
}
func stringMapSubtract(m1, m2 map[string]string) map[string]string {
// StringMapSubtract subtracts the content of structmap m2 from structmap m1
func StringMapSubtract(m1, m2 map[string]string) map[string]string {
m := map[string]string{}
for k1, v1 := range m1 {
@ -51,7 +54,8 @@ func stringMapSubtract(m1, m2 map[string]string) map[string]string {
return m
}
func structMapSubtract(m1, m2 map[string]struct{}) map[string]struct{} {
// StructMapSubtract subtracts the content of structmap m2 from structmap m1
func StructMapSubtract(m1, m2 map[string]struct{}) map[string]struct{} {
m := map[string]struct{}{}
for k1, v1 := range m1 {

@ -1,17 +1,15 @@
package container
package util
import (
"testing"
"github.com/stretchr/testify/assert"
"testing"
)
func TestSliceEqual_True(t *testing.T) {
s1 := []string{"a", "b", "c"}
s2 := []string{"a", "b", "c"}
result := sliceEqual(s1, s2)
result := SliceEqual(s1, s2)
assert.True(t, result)
}
@ -20,7 +18,7 @@ func TestSliceEqual_DifferentLengths(t *testing.T) {
s1 := []string{"a", "b", "c"}
s2 := []string{"a", "b", "c", "d"}
result := sliceEqual(s1, s2)
result := SliceEqual(s1, s2)
assert.False(t, result)
}
@ -29,7 +27,7 @@ func TestSliceEqual_DifferentContents(t *testing.T) {
s1 := []string{"a", "b", "c"}
s2 := []string{"a", "b", "d"}
result := sliceEqual(s1, s2)
result := SliceEqual(s1, s2)
assert.False(t, result)
}
@ -38,7 +36,7 @@ func TestSliceSubtract(t *testing.T) {
a1 := []string{"a", "b", "c"}
a2 := []string{"a", "c"}
result := sliceSubtract(a1, a2)
result := SliceSubtract(a1, a2)
assert.Equal(t, []string{"b"}, result)
assert.Equal(t, []string{"a", "b", "c"}, a1)
assert.Equal(t, []string{"a", "c"}, a2)
@ -48,7 +46,7 @@ func TestStringMapSubtract(t *testing.T) {
m1 := map[string]string{"a": "a", "b": "b", "c": "sea"}
m2 := map[string]string{"a": "a", "c": "c"}
result := stringMapSubtract(m1, m2)
result := StringMapSubtract(m1, m2)
assert.Equal(t, map[string]string{"b": "b", "c": "sea"}, result)
assert.Equal(t, map[string]string{"a": "a", "b": "b", "c": "sea"}, m1)
assert.Equal(t, map[string]string{"a": "a", "c": "c"}, m2)
@ -59,7 +57,7 @@ func TestStructMapSubtract(t *testing.T) {
m1 := map[string]struct{}{"a": x, "b": x, "c": x}
m2 := map[string]struct{}{"a": x, "c": x}
result := structMapSubtract(m1, m2)
result := StructMapSubtract(m1, m2)
assert.Equal(t, map[string]struct{}{"b": x}, result)
assert.Equal(t, map[string]struct{}{"a": x, "b": x, "c": x}, m1)
assert.Equal(t, map[string]struct{}{"a": x, "c": x}, m2)

@ -1,39 +1,8 @@
package main // import "github.com/containrrr/watchtower"
package main
import (
"os"
"os/signal"
"syscall"
"time"
"strconv"
"github.com/containrrr/watchtower/actions"
cliApp "github.com/containrrr/watchtower/app"
"github.com/containrrr/watchtower/container"
"github.com/containrrr/watchtower/notifications"
"github.com/robfig/cron"
"github.com/containrrr/watchtower/cmd"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// DockerAPIMinVersion is the version of the docker API, which is minimally required by
// watchtower. Currently we require at least API 1.24 and therefore Docker 1.12 or later.
const DockerAPIMinVersion string = "1.24"
var version = "master"
var commit = "unknown"
var date = "unknown"
var (
client container.Client
scheduleSpec string
cleanup bool
noRestart bool
monitorOnly bool
enableLabel bool
notifier *notifications.Notifier
timeout time.Duration
)
func init() {
@ -41,173 +10,5 @@ func init() {
}
func main() {
app := cli.NewApp()
InitApp(app)
cliApp.SetupCliFlags(app)
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
// InitApp initializes urfave app metadata and sets up entrypoints
func InitApp(app *cli.App) {
app.Name = "watchtower"
app.Version = version + " - " + commit + " - " + date
app.Usage = "Automatically update running Docker containers"
app.Before = before
app.Action = start
}
func before(c *cli.Context) error {
if c.GlobalBool("debug") {
log.SetLevel(log.DebugLevel)
}
pollingSet := c.IsSet("interval")
cronSet := c.IsSet("schedule")
cronLen := len(c.String("schedule"))
if pollingSet && cronSet && cronLen > 0 {
log.Fatal("Only schedule or interval can be defined, not both.")
} else if cronSet && cronLen > 0 {
scheduleSpec = c.String("schedule")
} else {
scheduleSpec = "@every " + strconv.Itoa(c.Int("interval")) + "s"
}
readFlags(c)
if timeout < 0 {
log.Fatal("Please specify a positive value for timeout value.")
}
enableLabel = c.GlobalBool("label-enable")
// configure environment vars for client
err := envConfig(c)
if err != nil {
return err
}
client = container.NewClient(
!c.GlobalBool("no-pull"),
c.GlobalBool("include-stopped"),
)
notifier = notifications.NewNotifier(c)
return nil
}
func start(c *cli.Context) error {
names := c.Args()
filter := container.BuildFilter(names, enableLabel)
if c.GlobalBool("run-once") {
log.Info("Running a one time update.")
runUpdatesWithNotifications(filter)
os.Exit(1)
return nil
}
if err := actions.CheckForMultipleWatchtowerInstances(client, cleanup); err != nil {
log.Fatal(err)
}
runUpgradesOnSchedule(filter)
os.Exit(1)
return nil
}
func runUpgradesOnSchedule(filter container.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 container.Filter) {
notifier.StartNotification()
updateParams := actions.UpdateParams{
Filter: filter,
Cleanup: cleanup,
NoRestart: noRestart,
Timeout: timeout,
MonitorOnly: monitorOnly,
}
err := actions.Update(client, updateParams)
if err != nil {
log.Println(err)
}
notifier.SendNotification()
}
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 == true {
return setEnvOptStr(env, "1")
}
return nil
}
// envConfig translates the command-line options into environment variables
// that will initialize the api client
func envConfig(c *cli.Context) error {
var err error
err = setEnvOptStr("DOCKER_HOST", c.GlobalString("host"))
err = setEnvOptBool("DOCKER_TLS_VERIFY", c.GlobalBool("tlsverify"))
err = setEnvOptStr("DOCKER_API_VERSION", DockerAPIMinVersion)
return err
}
func readFlags(c *cli.Context) {
cleanup = c.GlobalBool("cleanup")
noRestart = c.GlobalBool("no-restart")
monitorOnly = c.GlobalBool("monitor-only")
timeout = c.GlobalDuration("stop-timeout")
cmd.Execute()
}

@ -14,9 +14,11 @@ nav:
- 'Arguments': 'arguments.md'
- 'Notifications': 'notifications.md'
- 'Container selection': 'container-selection.md'
- 'Credential helpers': 'credential-helpers.md'
- 'Linked containers': 'linked-containers.md'
- 'Remote hosts': 'remote-hosts.md'
- 'Secure connections': 'secure-connections.md'
- 'Stop signals': 'stop-signals.md'
- 'Lifecycle hooks': 'lifecycle-hooks.md'
plugins:
- search
- search

@ -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() {}

@ -1,31 +1,34 @@
package container
import (
"bytes"
"fmt"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"io/ioutil"
"strings"
"time"
t "github.com/containrrr/watchtower/pkg/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
dockerclient "github.com/docker/docker/client"
log "github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
const (
defaultStopSignal = "SIGTERM"
)
const defaultStopSignal = "SIGTERM"
// A Client is the interface through which watchtower interacts with the
// Docker API.
type Client interface {
ListContainers(Filter) ([]Container, error)
ListContainers(t.Filter) ([]Container, error)
GetContainer(containerID string) (Container, error)
StopContainer(Container, time.Duration) error
StartContainer(Container) error
StartContainer(Container) (string, error)
RenameContainer(Container, string) error
IsContainerStale(Container) (bool, error)
ExecuteCommand(containerID string, command string) error
RemoveImage(Container) error
}
@ -35,7 +38,7 @@ type Client interface {
// * DOCKER_HOST the docker-engine host to send api requests to
// * DOCKER_TLS_VERIFY whether to verify tls certificates
// * DOCKER_API_VERSION the minimum docker api version to work with
func NewClient(pullImages bool, includeStopped bool) Client {
func NewClient(pullImages bool, includeStopped bool, reviveStopped bool, removeVolumes bool) Client {
cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
if err != nil {
@ -45,17 +48,21 @@ func NewClient(pullImages bool, includeStopped bool) Client {
return dockerClient{
api: cli,
pullImages: pullImages,
removeVolumes: removeVolumes,
includeStopped: includeStopped,
reviveStopped: reviveStopped,
}
}
type dockerClient struct {
api dockerclient.CommonAPIClient
pullImages bool
removeVolumes bool
includeStopped bool
reviveStopped bool
}
func (client dockerClient) ListContainers(fn Filter) ([]Container, error) {
func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) {
cs := []Container{}
bg := context.Background()
@ -71,24 +78,18 @@ func (client dockerClient) ListContainers(fn Filter) ([]Container, error) {
types.ContainerListOptions{
Filters: filter,
})
if err != nil {
return nil, err
}
for _, runningContainer := range containers {
containerInfo, err := client.api.ContainerInspect(bg, runningContainer.ID)
if err != nil {
return nil, err
}
imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image)
c, err := client.GetContainer(runningContainer.ID)
if err != nil {
return nil, err
}
c := Container{containerInfo: &containerInfo, imageInfo: &imageInfo}
if fn(c) {
cs = append(cs, c)
}
@ -109,6 +110,23 @@ func (client dockerClient) createListFilter() filters.Args {
return filterArgs
}
func (client dockerClient) GetContainer(containerID string) (Container, error) {
bg := context.Background()
containerInfo, err := client.api.ContainerInspect(bg, containerID)
if err != nil {
return Container{}, err
}
imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image)
if err != nil {
return Container{}, err
}
container := Container{containerInfo: &containerInfo, imageInfo: &imageInfo}
return container, nil
}
func (client dockerClient) StopContainer(c Container, timeout time.Duration) error {
bg := context.Background()
signal := c.StopSignal()
@ -123,28 +141,28 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
}
}
// Wait for container to exit, but proceed anyway after the timeout elapses
client.waitForStop(c, timeout)
// TODO: This should probably be checked.
_ = client.waitForStopOrTimeout(c, timeout)
if c.containerInfo.HostConfig.AutoRemove {
log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", c.ID())
} else {
log.Debugf("Removing container %s", c.ID())
if err := client.api.ContainerRemove(bg, c.ID(), types.ContainerRemoveOptions{Force: true, RemoveVolumes: false}); err != nil {
if err := client.api.ContainerRemove(bg, c.ID(), types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.removeVolumes}); err != nil {
return err
}
}
// Wait for container to be removed. In this case an error is a good thing
if err := client.waitForStop(c, timeout); err == nil {
if err := client.waitForStopOrTimeout(c, timeout); err == nil {
return fmt.Errorf("Container %s (%s) could not be removed", c.Name(), c.ID())
}
return nil
}
func (client dockerClient) StartContainer(c Container) error {
func (client dockerClient) StartContainer(c Container) (string, error) {
bg := context.Background()
config := c.runtimeConfig()
hostConfig := c.hostConfig()
@ -164,40 +182,40 @@ func (client dockerClient) StartContainer(c Container) error {
name := c.Name()
log.Infof("Creating %s", name)
creation, err := client.api.ContainerCreate(bg, config, hostConfig, simpleNetworkConfig, name)
createdContainer, err := client.api.ContainerCreate(bg, config, hostConfig, simpleNetworkConfig, name)
if err != nil {
return err
return "", err
}
if !(hostConfig.NetworkMode.IsHost()) {
for k := range simpleNetworkConfig.EndpointsConfig {
err = client.api.NetworkDisconnect(bg, k, creation.ID, true)
err = client.api.NetworkDisconnect(bg, k, createdContainer.ID, true)
if err != nil {
return err
return "", err
}
}
for k, v := range networkConfig.EndpointsConfig {
err = client.api.NetworkConnect(bg, k, creation.ID, v)
err = client.api.NetworkConnect(bg, k, createdContainer.ID, v)
if err != nil {
return err
return "", err
}
}
}
return client.startContainerIfPreviouslyRunning(bg, c, creation)
if !c.IsRunning() && !client.reviveStopped {
return createdContainer.ID, nil
}
return createdContainer.ID, client.doStartContainer(bg, c, createdContainer)
}
func (client dockerClient) startContainerIfPreviouslyRunning(bg context.Context, c Container, creation container.ContainerCreateCreatedBody) error {
func (client dockerClient) doStartContainer(bg context.Context, c Container, creation container.ContainerCreateCreatedBody) error {
name := c.Name()
if !c.IsRunning() {
return nil
}
log.Debugf("Starting container %s (%s)", name, creation.ID)
err := client.api.ContainerStart(bg, creation.ID, types.ContainerStartOptions{})
if err != nil {
@ -242,7 +260,9 @@ func (client dockerClient) IsContainerStale(c Container) (bool, error) {
defer response.Close()
// the pull request will be aborted prematurely unless the response is read
_, err = ioutil.ReadAll(response)
if _, err = ioutil.ReadAll(response); err != nil {
log.Error(err)
}
}
newImageInfo, _, err := client.api.ImageInspectWithRaw(bg, imageName)
@ -266,7 +286,68 @@ func (client dockerClient) RemoveImage(c Container) error {
return err
}
func (client dockerClient) waitForStop(c Container, waitTime time.Duration) error {
func (client dockerClient) ExecuteCommand(containerID string, command string) error {
bg := context.Background()
// Create the exec
execConfig := types.ExecConfig{
Tty: true,
Detach: false,
Cmd: []string{"sh", "-c", command},
}
exec, err := client.api.ContainerExecCreate(bg, containerID, execConfig)
if err != nil {
return err
}
response, attachErr := client.api.ContainerExecAttach(bg, exec.ID, types.ExecStartCheck{
Tty: true,
Detach: false,
})
if attachErr != nil {
log.Errorf("Failed to extract command exec logs: %v", attachErr)
}
// Run the exec
execStartCheck := types.ExecStartCheck{Detach: false, Tty: true}
err = client.api.ContainerExecStart(bg, exec.ID, execStartCheck)
if err != nil {
return err
}
var execOutput string
if attachErr == nil {
defer response.Close()
var writer bytes.Buffer
written, err := writer.ReadFrom(response.Reader)
if err != nil {
log.Error(err)
} else if written > 0 {
execOutput = strings.TrimSpace(writer.String())
}
}
// Inspect the exec to get the exit code and print a message if the
// exit code is not success.
execInspect, err := client.api.ContainerExecInspect(bg, exec.ID)
if err != nil {
return err
}
if execInspect.ExitCode > 0 {
log.Errorf("Command exited with code %v.", execInspect.ExitCode)
log.Error(execOutput)
} else {
if len(execOutput) > 0 {
log.Infof("Command output:\n%v", execOutput)
}
}
return nil
}
func (client dockerClient) waitForStopOrTimeout(c Container, waitTime time.Duration) error {
bg := context.Background()
timeout := time.After(waitTime)

@ -2,6 +2,7 @@ package container
import (
"fmt"
"github.com/containrrr/watchtower/internal/util"
"strconv"
"strings"
@ -9,13 +10,6 @@ import (
dockercontainer "github.com/docker/docker/api/types/container"
)
const (
watchtowerLabel = "com.centurylinklabs.watchtower"
signalLabel = "com.centurylinklabs.watchtower.stop-signal"
enableLabel = "com.centurylinklabs.watchtower.enable"
zodiacLabel = "com.centurylinklabs.zodiac.original-image"
)
// NewContainer returns a new Container instance instantiated with the
// specified ContainerInfo and ImageInfo structs.
func NewContainer(containerInfo *types.ContainerJSON, imageInfo *types.ImageInspect) *Container {
@ -27,7 +21,8 @@ func NewContainer(containerInfo *types.ContainerJSON, imageInfo *types.ImageInsp
// Container represents a running Docker container.
type Container struct {
Stale bool
Linked bool
Stale bool
containerInfo *types.ContainerJSON
imageInfo *types.ImageInspect
@ -61,7 +56,7 @@ func (c Container) ImageID() string {
// "latest" tag is assumed.
func (c Container) ImageName() string {
// Compatibility w/ Zodiac deployments
imageName, ok := c.containerInfo.Config.Labels[zodiacLabel]
imageName, ok := c.getLabelValue(zodiacLabel)
if !ok {
imageName = c.containerInfo.Config.Image
}
@ -76,7 +71,7 @@ func (c Container) ImageName() string {
// Enabled returns the value of the container enabled label and if the label
// was set.
func (c Container) Enabled() (bool, bool) {
rawBool, ok := c.containerInfo.Config.Labels[enableLabel]
rawBool, ok := c.getLabelValue(enableLabel)
if !ok {
return false, false
}
@ -104,6 +99,12 @@ func (c Container) Links() []string {
return links
}
// ToRestart return whether the container should be restarted, either because
// is stale or linked to another stale container.
func (c Container) ToRestart() bool {
return c.Stale || c.Linked
}
// IsWatchtower returns a boolean flag indicating whether or not the current
// container is the watchtower container itself. The watchtower container is
// identified by the presence of the "com.centurylinklabs.watchtower" label in
@ -116,11 +117,7 @@ func (c Container) IsWatchtower() bool {
// container's metadata. If the container has not specified a custom stop
// signal, the empty string "" is returned.
func (c Container) StopSignal() string {
if val, ok := c.containerInfo.Config.Labels[signalLabel]; ok {
return val
}
return ""
return c.getLabelValueOrEmpty(signalLabel)
}
// Ideally, we'd just be able to take the ContainerConfig from the old container
@ -146,19 +143,19 @@ func (c Container) runtimeConfig() *dockercontainer.Config {
config.User = ""
}
if sliceEqual(config.Cmd, imageConfig.Cmd) {
if util.SliceEqual(config.Cmd, imageConfig.Cmd) {
config.Cmd = nil
}
if sliceEqual(config.Entrypoint, imageConfig.Entrypoint) {
if util.SliceEqual(config.Entrypoint, imageConfig.Entrypoint) {
config.Entrypoint = nil
}
config.Env = sliceSubtract(config.Env, imageConfig.Env)
config.Env = util.SliceSubtract(config.Env, imageConfig.Env)
config.Labels = stringMapSubtract(config.Labels, imageConfig.Labels)
config.Labels = util.StringMapSubtract(config.Labels, imageConfig.Labels)
config.Volumes = structMapSubtract(config.Volumes, imageConfig.Volumes)
config.Volumes = util.StructMapSubtract(config.Volumes, imageConfig.Volumes)
// subtract ports exposed in image from container
for k := range config.ExposedPorts {
@ -188,10 +185,3 @@ func (c Container) hostConfig() *dockercontainer.HostConfig {
return hostConfig
}
// 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"
}

@ -1,7 +1,7 @@
package container
import (
"github.com/containrrr/watchtower/container/mocks"
"github.com/containrrr/watchtower/pkg/container/mocks"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
cli "github.com/docker/docker/client"
@ -23,8 +23,7 @@ var _ = Describe("the container", func() {
server := mocks.NewMockAPIServer()
docker, _ = cli.NewClientWithOpts(
cli.WithHost(server.URL),
cli.WithHTTPClient(server.Client(),
))
cli.WithHTTPClient(server.Client()))
client = dockerClient{
api: docker,
pullImages: false,

@ -1,30 +1,20 @@
package container
// 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
// A FilterableContainer is the interface which is used to filter
// containers.
type FilterableContainer interface {
Name() string
IsWatchtower() bool
Enabled() (bool, bool)
}
import t "github.com/containrrr/watchtower/pkg/types"
// WatchtowerContainersFilter filters only watchtower containers
func WatchtowerContainersFilter(c FilterableContainer) bool { return c.IsWatchtower() }
func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatchtower() }
// Filter no containers and returns all
func noFilter(FilterableContainer) bool { return true }
func noFilter(t.FilterableContainer) bool { return true }
// Filters containers which don't have a specified name
func filterByNames(names []string, baseFilter Filter) Filter {
func filterByNames(names []string, baseFilter t.Filter) t.Filter {
if len(names) == 0 {
return baseFilter
}
return func(c FilterableContainer) bool {
return func(c t.FilterableContainer) bool {
for _, name := range names {
if (name == c.Name()) || (name == c.Name()[1:]) {
return baseFilter(c)
@ -35,8 +25,8 @@ func filterByNames(names []string, baseFilter Filter) Filter {
}
// Filters out containers that don't have the 'enableLabel'
func filterByEnableLabel(baseFilter Filter) Filter {
return func(c FilterableContainer) bool {
func filterByEnableLabel(baseFilter t.Filter) t.Filter {
return func(c t.FilterableContainer) bool {
// If label filtering is enabled, containers should only be considered
// if the label is specifically set.
_, ok := c.Enabled()
@ -49,8 +39,8 @@ func filterByEnableLabel(baseFilter Filter) Filter {
}
// Filters out containers that have a 'enableLabel' and is set to disable.
func filterByDisabledLabel(baseFilter Filter) Filter {
return func(c FilterableContainer) bool {
func filterByDisabledLabel(baseFilter t.Filter) t.Filter {
return func(c t.FilterableContainer) bool {
enabledLabel, ok := c.Enabled()
if ok && !enabledLabel {
// If the label has been set and it demands a disable
@ -62,7 +52,7 @@ func filterByDisabledLabel(baseFilter Filter) Filter {
}
// BuildFilter creates the needed filter of containers
func BuildFilter(names []string, enableLabel bool) Filter {
func BuildFilter(names []string, enableLabel bool) t.Filter {
filter := noFilter
filter = filterByNames(names, filter)
if enableLabel {

@ -3,8 +3,8 @@ package container
import (
"testing"
"github.com/containrrr/watchtower/pkg/container/mocks"
"github.com/stretchr/testify/assert"
"github.com/containrrr/watchtower/container/mocks"
)
func TestWatchtowerContainersFilter(t *testing.T) {

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

@ -48,6 +48,10 @@ func EncodedEnvAuth(ref string) (string, error) {
// The docker config must be mounted on the container
func EncodedConfigAuth(ref string) (string, error) {
server, err := ParseServerAddress(ref)
if err != nil {
log.Errorf("Unable to parse the image ref %s", err)
return "", err
}
configDir := os.Getenv("DOCKER_CONFIG")
if configDir == "" {
configDir = "/"
@ -58,7 +62,8 @@ func EncodedConfigAuth(ref string) (string, error) {
return "", err
}
credStore := CredentialsStore(*configFile)
auth, err := credStore.Get(server) // returns (types.AuthConfig{}) if server not in credStore
auth, _ := credStore.Get(server) // returns (types.AuthConfig{}) if server not in credStore
if auth == (types.AuthConfig{}) {
log.Debugf("No credentials for %s in %s", server, configFile.Filename)
return "", nil

@ -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")

@ -3,44 +3,59 @@ package notifications
import (
"encoding/base64"
"fmt"
"github.com/spf13/cobra"
"net/smtp"
"os"
"time"
"strconv"
t "github.com/containrrr/watchtower/pkg/types"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli"
"strconv"
)
const (
emailType = "email"
)
// Implements typeNotifier, logrus.Hook
// Implements Notifier, logrus.Hook
// The default logrus email integration would have several issues:
// - It would send one email per log output
// - It would only send errors
// We work around that by holding on to log entries until the update cycle is done.
type emailTypeNotifier struct {
From, To string
Server, User, Password string
Port int
tlsSkipVerify bool
entries []*log.Entry
logLevels []log.Level
From, To string
Server, User, Password, SubjectTag string
Port int
tlsSkipVerify bool
entries []*log.Entry
logLevels []log.Level
delay time.Duration
}
func newEmailNotifier(c *cli.Context, acceptedLogLevels []log.Level) typeNotifier {
func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
flags := c.PersistentFlags()
from, _ := flags.GetString("notification-email-from")
to, _ := flags.GetString("notification-email-to")
server, _ := flags.GetString("notification-email-server")
user, _ := flags.GetString("notification-email-server-user")
password, _ := flags.GetString("notification-email-server-password")
port, _ := flags.GetInt("notification-email-server-port")
tlsSkipVerify, _ := flags.GetBool("notification-email-server-tls-skip-verify")
delay, _ := flags.GetInt("notification-email-delay")
subjecttag, _ := flags.GetString("notification-email-subjecttag")
n := &emailTypeNotifier{
From: c.GlobalString("notification-email-from"),
To: c.GlobalString("notification-email-to"),
Server: c.GlobalString("notification-email-server"),
User: c.GlobalString("notification-email-server-user"),
Password: c.GlobalString("notification-email-server-password"),
Port: c.GlobalInt("notification-email-server-port"),
tlsSkipVerify: c.GlobalBool("notification-email-server-tls-skip-verify"),
From: from,
To: to,
Server: server,
User: user,
Password: password,
Port: port,
tlsSkipVerify: tlsSkipVerify,
logLevels: acceptedLogLevels,
delay: time.Duration(delay) * time.Second,
SubjectTag: subjecttag,
}
log.AddHook(n)
@ -49,7 +64,13 @@ func newEmailNotifier(c *cli.Context, acceptedLogLevels []log.Level) typeNotifie
}
func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte {
emailSubject := "Watchtower updates"
var emailSubject string
if e.SubjectTag == "" {
emailSubject = "Watchtower updates"
} else {
emailSubject = e.SubjectTag + " Watchtower updates"
}
if hostname, err := os.Hostname(); err == nil {
emailSubject += " on " + hostname
}
@ -60,7 +81,7 @@ func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte {
}
t := time.Now()
header := make(map[string]string)
header["From"] = e.From
header["To"] = e.To
@ -107,9 +128,15 @@ func (e *emailTypeNotifier) StartNotification() {
}
func (e *emailTypeNotifier) SendNotification() {
if e.entries != nil && len(e.entries) != 0 {
e.sendEntries(e.entries)
if e.entries == nil || len(e.entries) <= 0 {
return
}
if e.delay > 0 {
time.Sleep(e.delay)
}
e.sendEntries(e.entries)
e.entries = nil
}

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

@ -4,10 +4,11 @@ import (
"bytes"
"encoding/json"
"fmt"
"github.com/spf13/cobra"
"net/http"
t "github.com/containrrr/watchtower/pkg/types"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli"
"io/ioutil"
)
@ -21,17 +22,20 @@ type msTeamsTypeNotifier struct {
data bool
}
func newMsTeamsNotifier(c *cli.Context, acceptedLogLevels []log.Level) typeNotifier {
func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
webHookURL := c.GlobalString("notification-msteams-hook")
flags := cmd.PersistentFlags()
webHookURL, _ := flags.GetString("notification-msteams-hook")
if len(webHookURL) <= 0 {
log.Fatal("Required argument --notification-msteams-hook(cli) or WATCHTOWER_NOTIFICATION_MSTEAMS_HOOK_URL(env) is empty.")
}
withData, _ := flags.GetBool("notification-msteams-data")
n := &msTeamsTypeNotifier{
levels: acceptedLogLevels,
webHookURL: webHookURL,
data: c.GlobalBool("notification-msteams-data"),
data: withData,
}
log.AddHook(n)

@ -1,26 +1,25 @@
package notifications
import (
ty "github.com/containrrr/watchtower/pkg/types"
"github.com/johntdyer/slackrus"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/spf13/cobra"
)
type typeNotifier interface {
StartNotification()
SendNotification()
}
// Notifier can send log output as notification to admins, with optional batching.
type Notifier struct {
types []typeNotifier
types []ty.Notifier
}
// NewNotifier creates and returns a new Notifier, using global configuration.
func NewNotifier(c *cli.Context) *Notifier {
func NewNotifier(c *cobra.Command) *Notifier {
n := &Notifier{}
logLevel, err := log.ParseLevel(c.GlobalString("notifications-level"))
f := c.PersistentFlags()
level, _ := f.GetString("notifications-level")
logLevel, err := log.ParseLevel(level)
if err != nil {
log.Fatalf("Notifications invalid log level: %s", err.Error())
}
@ -28,9 +27,10 @@ func NewNotifier(c *cli.Context) *Notifier {
acceptedLogLevels := slackrus.LevelThreshold(logLevel)
// Parse types and create notifiers.
types := c.GlobalStringSlice("notifications")
types, _ := f.GetStringSlice("notifications")
for _, t := range types {
var tn typeNotifier
var tn ty.Notifier
switch t {
case emailType:
tn = newEmailNotifier(c, acceptedLogLevels)
@ -38,6 +38,8 @@ func NewNotifier(c *cli.Context) *Notifier {
tn = newSlackNotifier(c, acceptedLogLevels)
case msTeamsType:
tn = newMsTeamsNotifier(c, acceptedLogLevels)
case gotifyType:
tn = newGotifyNotifier(c, acceptedLogLevels)
default:
log.Fatalf("Unknown notification type %q", t)
}

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