mirror of
https://github.com/containrrr/watchtower.git
synced 2025-09-22 05:40:50 +02:00
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
This commit is contained in:
parent
1631c8cc2e
commit
e584f8bfcf
8 changed files with 101 additions and 33 deletions
|
@ -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{})
|
}
|
||||||
|
|
||||||
|
func (client dockerClient) startContainerIfPreviouslyRunning(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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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"
|
||||||
|
|
5
main.go
5
main.go
|
@ -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…
Add table
Add a link
Reference in a new issue