Make it possible to use watchtower to update exited or created containers as well (#289)

* feature/112: add additional tests that verify include-stopped

* feature/112: implement include-stopped

* feature/112: update readme and cli help

* feature/112: fix linting issues

* remove superfluous logging
pull/328/head v0.3.5
Simon Aronsson 6 years ago committed by GitHub
parent 1631c8cc2e
commit e584f8bfcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -157,6 +157,7 @@ docker run --rm containrrr/watchtower --help
- `--tlsverify` Use TLS when connecting to the Docker socket and verify the server's certificate. - `--tlsverify` Use TLS when connecting to the Docker socket and verify the server's certificate.
- `--debug` Enable debug mode. When this option is specified you'll see more verbose logging in the watchtower log file. - `--debug` Enable debug mode. When this option is specified you'll see more verbose logging in the watchtower log file.
- `--monitor-only` Will only monitor for new images, not update the containers. - `--monitor-only` Will only monitor for new images, not update the containers.
- `--include-stopped` Will also include created and exited containers.
- `--help` Show documentation about the supported flags. - `--help` Show documentation about the supported flags.
See below for options used to configure notifications. See below for options used to configure notifications.

@ -157,6 +157,12 @@ func SetupCliFlags(app *cli.App) {
cli.BoolFlag{ cli.BoolFlag{
Name: "run-once", Name: "run-once",
Usage: "Run once now and exit", 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",
}, },
} }
} }

@ -2,6 +2,8 @@ package container
import ( import (
"fmt" "fmt"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"io/ioutil" "io/ioutil"
"time" "time"
@ -33,36 +35,48 @@ type Client interface {
// * DOCKER_HOST the docker-engine host to send api requests to // * DOCKER_HOST the docker-engine host to send api requests to
// * DOCKER_TLS_VERIFY whether to verify tls certificates // * DOCKER_TLS_VERIFY whether to verify tls certificates
// * DOCKER_API_VERSION the minimum docker api version to work with // * DOCKER_API_VERSION the minimum docker api version to work with
func NewClient(pullImages bool) Client { func NewClient(pullImages bool, includeStopped bool) Client {
cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv) cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
if err != nil { if err != nil {
log.Fatalf("Error instantiating Docker client: %s", err) log.Fatalf("Error instantiating Docker client: %s", err)
} }
return dockerClient{api: cli, pullImages: pullImages} return dockerClient{
api: cli,
pullImages: pullImages,
includeStopped: includeStopped,
}
} }
type dockerClient struct { type dockerClient struct {
api dockerclient.CommonAPIClient api dockerclient.CommonAPIClient
pullImages bool pullImages bool
includeStopped bool
} }
func (client dockerClient) ListContainers(fn Filter) ([]Container, error) { func (client dockerClient) ListContainers(fn Filter) ([]Container, error) {
cs := []Container{} cs := []Container{}
bg := context.Background() bg := context.Background()
if client.includeStopped {
log.Debug("Retrieving containers including stopped and exited")
} else {
log.Debug("Retrieving running containers") log.Debug("Retrieving running containers")
}
runningContainers, err := client.api.ContainerList( filter := client.createListFilter()
containers, err := client.api.ContainerList(
bg, bg,
types.ContainerListOptions{}) types.ContainerListOptions{
Filters: filter,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, runningContainer := range runningContainers { for _, runningContainer := range containers {
containerInfo, err := client.api.ContainerInspect(bg, runningContainer.ID) containerInfo, err := client.api.ContainerInspect(bg, runningContainer.ID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -83,6 +97,18 @@ func (client dockerClient) ListContainers(fn Filter) ([]Container, error) {
return cs, nil return cs, nil
} }
func (client dockerClient) createListFilter() filters.Args {
filterArgs := filters.NewArgs()
filterArgs.Add("status", "running")
if client.includeStopped {
filterArgs.Add("status", "created")
filterArgs.Add("status", "exited")
}
return filterArgs
}
func (client dockerClient) StopContainer(c Container, timeout time.Duration) error { func (client dockerClient) StopContainer(c Container, timeout time.Duration) error {
bg := context.Background() bg := context.Background()
signal := c.StopSignal() signal := c.StopSignal()
@ -90,11 +116,12 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
signal = defaultStopSignal signal = defaultStopSignal
} }
if c.IsRunning() {
log.Infof("Stopping %s (%s) with %s", c.Name(), c.ID(), signal) log.Infof("Stopping %s (%s) with %s", c.Name(), c.ID(), signal)
if err := client.api.ContainerKill(bg, c.ID(), signal); err != nil { if err := client.api.ContainerKill(bg, c.ID(), signal); err != nil {
return err return err
} }
}
// Wait for container to exit, but proceed anyway after the timeout elapses // Wait for container to exit, but proceed anyway after the timeout elapses
client.waitForStop(c, timeout) client.waitForStop(c, timeout)
@ -160,15 +187,23 @@ func (client dockerClient) StartContainer(c Container) error {
} }
log.Debugf("Starting container %s (%s)", name, creation.ID) return client.startContainerIfPreviouslyRunning(bg, c, creation)
err = client.api.ContainerStart(bg, creation.ID, types.ContainerStartOptions{})
if err != nil {
return err
} }
func (client dockerClient) startContainerIfPreviouslyRunning(bg context.Context, c Container, creation container.ContainerCreateCreatedBody) error {
name := c.Name()
if !c.IsRunning() {
return nil return nil
}
log.Debugf("Starting container %s (%s)", name, creation.ID)
err := client.api.ContainerStart(bg, creation.ID, types.ContainerStartOptions{})
if err != nil {
return err
}
return nil
} }
func (client dockerClient) RenameContainer(c Container, newName string) error { func (client dockerClient) RenameContainer(c Container, newName string) error {

@ -38,6 +38,13 @@ func (c Container) ID() string {
return c.containerInfo.ID return c.containerInfo.ID
} }
// IsRunning returns a boolean flag indicating whether or not the current
// container is running. The status is determined by the value of the
// container's "State.Running" property.
func (c Container) IsRunning() bool {
return c.containerInfo.State.Running
}
// Name returns the Docker container name. // Name returns the Docker container name.
func (c Container) Name() string { func (c Container) Name() string {
return c.containerInfo.Name return c.containerInfo.Name

@ -17,15 +17,16 @@ func TestContainer(t *testing.T) {
var _ = Describe("the container", func() { var _ = Describe("the container", func() {
Describe("the client", func() { Describe("the client", func() {
var docker *cli.Client
var client Client var client Client
BeforeSuite(func() { BeforeSuite(func() {
server := mocks.NewMockAPIServer() server := mocks.NewMockAPIServer()
c, _ := cli.NewClientWithOpts( docker, _ = cli.NewClientWithOpts(
cli.WithHost(server.URL), cli.WithHost(server.URL),
cli.WithHTTPClient(server.Client(), cli.WithHTTPClient(server.Client(),
)) ))
client = dockerClient{ client = dockerClient{
api: c, api: docker,
pullImages: false, pullImages: false,
} }
}) })
@ -55,6 +56,18 @@ var _ = Describe("the container", func() {
Expect(containers[0].ImageName()).To(Equal("containrrr/watchtower:latest")) Expect(containers[0].ImageName()).To(Equal("containrrr/watchtower:latest"))
}) })
}) })
When(`listing containers with the "include stopped" option`, func() {
It("should return both stopped and running containers", func() {
client = dockerClient{
api: docker,
pullImages: false,
includeStopped: true,
}
containers, err := client.ListContainers(noFilter)
Expect(err).NotTo(HaveOccurred())
Expect(len(containers) > 0).To(BeTrue())
})
})
}) })
When("asked for metadata", func() { When("asked for metadata", func() {
var c *Container var c *Container

@ -18,7 +18,11 @@ func NewMockAPIServer() *httptest.Server {
logrus.Debug("Mock server has received a HTTP call on ", r.URL) logrus.Debug("Mock server has received a HTTP call on ", r.URL)
var response = "" var response = ""
if isRequestFor("containers/json?limit=0", r) { if isRequestFor("filters=%7B%22status%22%3A%7B%22running%22%3Atrue%7D%7D&limit=0", r) {
response = getMockJSONFromDisk("./mocks/data/containers.json")
} else if isRequestFor("filters=%7B%22status%22%3A%7B%22created%22%3Atrue%2C%22exited%22%3Atrue%2C%22running%22%3Atrue%7D%7D&limit=0", r) {
response = getMockJSONFromDisk("./mocks/data/containers.json")
} else if isRequestFor("containers/json?limit=0", r) {
response = getMockJSONFromDisk("./mocks/data/containers.json") response = getMockJSONFromDisk("./mocks/data/containers.json")
} else if isRequestFor("ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65", r) { } else if isRequestFor("ae8964ba86c7cd7522cf84e09781343d88e0e3543281c747d88b27e246578b65", r) {
response = getMockJSONFromDisk("./mocks/data/container_stopped.json") response = getMockJSONFromDisk("./mocks/data/container_stopped.json")
@ -48,4 +52,3 @@ func getMockJSONFromDisk(relPath string) string {
} }
return string(buf) return string(buf)
} }

@ -12,7 +12,7 @@
"Labels": { "Labels": {
"com.centurylinklabs.watchtower": "true" "com.centurylinklabs.watchtower": "true"
}, },
"State": "exited", "State": "running",
"Status": "Exited (1) 6 days ago", "Status": "Exited (1) 6 days ago",
"HostConfig": { "HostConfig": {
"NetworkMode": "default" "NetworkMode": "default"

@ -89,7 +89,10 @@ func before(c *cli.Context) error {
return err return err
} }
client = container.NewClient(!c.GlobalBool("no-pull")) client = container.NewClient(
!c.GlobalBool("no-pull"),
c.GlobalBool("include-stopped"),
)
notifier = notifications.NewNotifier(c) notifier = notifications.NewNotifier(c)
return nil return nil

Loading…
Cancel
Save