From 135467dcf6ffc4f44f792d02a70cad90312bb31a Mon Sep 17 00:00:00 2001 From: Niklas Wigertz Danielsson Date: Sun, 15 Sep 2019 16:58:46 +0200 Subject: [PATCH] add pre/post update check lifecycle hooks (#373) * add pre/post update check lifecycle hooks * update docs for lifecycle hooks * Fix phrasing --- docs/lifecycle-hooks.md | 38 +++++++++++++++++----------- internal/actions/update.go | 52 ++++++++++++++++++++++++++++++++++++++ pkg/container/metadata.go | 12 +++++++++ 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/docs/lifecycle-hooks.md b/docs/lifecycle-hooks.md index 97d254e..071726c 100644 --- a/docs/lifecycle-hooks.md +++ b/docs/lifecycle-hooks.md @@ -1,45 +1,53 @@ - ## Executing commands before and after updating -> **DO NOTE**: Both commands are shell commands executed with `sh`, and therefore require the +> **DO NOTE**: These are shell commands executed with `sh`, and therefore require the > container to provide the `sh` executable. -It is possible to execute a *pre-update* command and a *post-update* command -**inside** every container updated by watchtower. The *pre-update* command is -executed before stopping the container, and the *post-update* command is -executed after restarting the container. +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, with -`com.centurylinklabs.watchtower.lifecycle.pre-update-command` for the *pre-update* -command and `com.centurylinklabs.watchtower.lifecycle.post-update` for the -*post-update* command. +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: +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 +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. \ No newline at end of file +log statement containing the exit code will be reported. diff --git a/internal/actions/update.go b/internal/actions/update.go index 5763017..02013d6 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -13,6 +13,8 @@ import ( 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 @@ -36,12 +38,14 @@ func Update(client container.Client, params UpdateParams) error { checkDependencies(containers) if params.MonitorOnly { + executePostCheck(client, params) return nil } stopContainersInReversedOrder(containers, client, params) restartContainersInSortedOrder(containers, client, params) + executePostCheck(client, params) return nil } @@ -123,11 +127,58 @@ func checkDependencies(containers []container.Container) { } } +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.") @@ -146,6 +197,7 @@ func executePostUpdateCommand(client container.Client, newContainerID string) { command := newContainer.GetLifecyclePostUpdateCommand() if len(command) == 0 { log.Debug("No post-update command supplied. Skipping") + return } log.Info("Executing post-update command.") diff --git a/pkg/container/metadata.go b/pkg/container/metadata.go index 3ab9ec2..0e04350 100644 --- a/pkg/container/metadata.go +++ b/pkg/container/metadata.go @@ -5,10 +5,22 @@ const ( 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)