mirror of
https://github.com/containrrr/watchtower.git
synced 2025-09-22 05:40:50 +02:00
Feat/lifecycle hooks (#351)
* feat(update): add lifecycle hooks to the update action * fix(ci): add bash tests for lifecycle-hooks to the ci workflow * fix(ci): move integration tests to an isolated step * fix(ci): fix malformed all-contributors json * fix(ci): disable automatic bash test until we figure out a reasonable way to run it in circleci
This commit is contained in:
parent
874180a518
commit
bfae38dbf8
12 changed files with 499 additions and 73 deletions
|
@ -1,8 +1,10 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
t "github.com/containrrr/watchtower/pkg/types"
|
||||
|
@ -15,18 +17,18 @@ import (
|
|||
"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(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
|
||||
}
|
||||
|
||||
|
@ -80,18 +82,12 @@ func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) {
|
|||
}
|
||||
|
||||
for _, runningContainer := range containers {
|
||||
containerInfo, err := client.api.ContainerInspect(bg, runningContainer.ID)
|
||||
|
||||
c, err := client.GetContainer(runningContainer.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := Container{containerInfo: &containerInfo, imageInfo: &imageInfo}
|
||||
|
||||
if fn(c) {
|
||||
cs = append(cs, c)
|
||||
}
|
||||
|
@ -112,6 +108,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()
|
||||
|
@ -147,7 +160,7 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
|
|||
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()
|
||||
|
@ -167,40 +180,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() {
|
||||
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 {
|
||||
|
@ -271,6 +284,67 @@ func (client dockerClient) RemoveImage(c Container) error {
|
|||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -10,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 {
|
||||
|
@ -28,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
|
||||
|
@ -62,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
|
||||
}
|
||||
|
@ -77,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
|
||||
}
|
||||
|
@ -105,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
|
||||
|
@ -117,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
|
||||
|
@ -189,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"
|
||||
}
|
||||
|
|
39
pkg/container/metadata.go
Normal file
39
pkg/container/metadata.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package container
|
||||
|
||||
const (
|
||||
watchtowerLabel = "com.centurylinklabs.watchtower"
|
||||
signalLabel = "com.centurylinklabs.watchtower.stop-signal"
|
||||
enableLabel = "com.centurylinklabs.watchtower.enable"
|
||||
zodiacLabel = "com.centurylinklabs.zodiac.original-image"
|
||||
preUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.pre-update"
|
||||
postUpdateLabel = "com.centurylinklabs.watchtower.lifecycle.post-update"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue