Merge pull request #173 from v2tec/filters

Filters refactoring and always exclude disabled containers
pull/174/head
stffabi 7 years ago committed by GitHub
commit 88a7a084a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -129,9 +129,21 @@ docker run -d --label=com.centurylinklabs.watchtower.stop-signal=SIGHUP someimag
## Selectively Watching Containers ## Selectively Watching Containers
By default, watchtower will watch all containers. By default, watchtower will watch all containers. However, sometimes only some containers should be updated.
However, sometimes only some containers should be updated.
If you need to selectively watch 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 exclude some containers, set the *com.centurylinklabs.watchtower.enable* label to `false`.
```docker
LABEL com.centurylinklabs.watchtower.enable="false"
```
Or, it can be specified as part of the `docker run` command line:
```bash
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.
```docker ```docker
LABEL com.centurylinklabs.watchtower.enable="true" LABEL com.centurylinklabs.watchtower.enable="true"

@ -6,14 +6,12 @@ import (
"github.com/v2tec/watchtower/container" "github.com/v2tec/watchtower/container"
) )
func watchtowerContainersFilter(c container.Container) bool { return c.IsWatchtower() }
// CheckPrereqs will ensure that there are not multiple instances of the // CheckPrereqs will ensure that there are not multiple instances of the
// watchtower running simultaneously. If multiple watchtower containers are // watchtower running simultaneously. If multiple watchtower containers are
// detected, this function will stop and remove all but the most recently // detected, this function will stop and remove all but the most recently
// started container. // started container.
func CheckPrereqs(client container.Client, cleanup bool) error { func CheckPrereqs(client container.Client, cleanup bool) error {
containers, err := client.ListContainers(watchtowerContainersFilter) containers, err := client.ListContainers(container.WatchtowerContainersFilter)
if err != nil { if err != nil {
return err return err
} }

@ -12,31 +12,14 @@ var (
letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
) )
func allContainersFilter(container.Container) bool { return true }
func containerFilter(names []string) container.Filter {
if len(names) == 0 {
return allContainersFilter
}
return func(c container.Container) bool {
for _, name := range names {
if (name == c.Name()) || (name == c.Name()[1:]) {
return true
}
}
return false
}
}
// Update looks at the running Docker containers to see if any of the images // 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 // 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 // any of the images, the associated containers are stopped and restarted with
// the new image. // the new image.
func Update(client container.Client, names []string, cleanup bool, noRestart bool, timeout time.Duration) error { func Update(client container.Client, filter container.Filter, cleanup bool, noRestart bool, timeout time.Duration) error {
log.Debug("Checking containers for updated images") log.Debug("Checking containers for updated images")
containers, err := client.ListContainers(containerFilter(names)) containers, err := client.ListContainers(filter)
if err != nil { if err != nil {
return err return err
} }

@ -5,10 +5,10 @@ import (
"io/ioutil" "io/ioutil"
"time" "time"
log "github.com/sirupsen/logrus"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
dockerclient "github.com/docker/docker/client" dockerclient "github.com/docker/docker/client"
log "github.com/sirupsen/logrus"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
@ -16,10 +16,6 @@ const (
defaultStopSignal = "SIGTERM" defaultStopSignal = "SIGTERM"
) )
// 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(Container) bool
// A Client is the interface through which watchtower interacts with the // A Client is the interface through which watchtower interacts with the
// Docker API. // Docker API.
type Client interface { type Client interface {
@ -37,20 +33,19 @@ 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, enableLabel bool) Client { func NewClient(pullImages bool) Client {
cli, err := dockerclient.NewEnvClient() cli, err := dockerclient.NewEnvClient()
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, enableLabel: enableLabel} return dockerClient{api: cli, pullImages: pullImages}
} }
type dockerClient struct { type dockerClient struct {
api *dockerclient.Client api *dockerclient.Client
pullImages bool pullImages bool
enableLabel bool
} }
func (client dockerClient) ListContainers(fn Filter) ([]Container, error) { func (client dockerClient) ListContainers(fn Filter) ([]Container, error) {
@ -62,6 +57,7 @@ func (client dockerClient) ListContainers(fn Filter) ([]Container, error) {
runningContainers, err := client.api.ContainerList( runningContainers, err := client.api.ContainerList(
bg, bg,
types.ContainerListOptions{}) types.ContainerListOptions{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -79,15 +75,6 @@ func (client dockerClient) ListContainers(fn Filter) ([]Container, error) {
c := Container{containerInfo: &containerInfo, imageInfo: &imageInfo} c := Container{containerInfo: &containerInfo, imageInfo: &imageInfo}
if client.enableLabel {
// If label filtering is enabled, containers should only be enabled
// if the label is specifically set to true.
enabledLabel, ok := c.Enabled()
if !ok || !enabledLabel {
continue
}
}
if fn(c) { if fn(c) {
cs = append(cs, c) cs = append(cs, c)
} }
@ -229,10 +216,9 @@ func (client dockerClient) IsContainerStale(c Container) (bool, error) {
if newImageInfo.ID != oldImageInfo.ID { if newImageInfo.ID != oldImageInfo.ID {
log.Infof("Found new %s image (%s)", imageName, newImageInfo.ID) log.Infof("Found new %s image (%s)", imageName, newImageInfo.ID)
return true, nil return true, nil
} else {
log.Debugf("No new images found for %s", c.Name())
} }
log.Debugf("No new images found for %s", c.Name())
return false, nil return false, nil
} }

@ -155,7 +155,7 @@ func (c Container) runtimeConfig() *dockercontainer.Config {
config.Volumes = structMapSubtract(config.Volumes, imageConfig.Volumes) config.Volumes = structMapSubtract(config.Volumes, imageConfig.Volumes)
// subtract ports exposed in image from container // subtract ports exposed in image from container
for k, _ := range config.ExposedPorts { for k := range config.ExposedPorts {
if _, ok := imageConfig.ExposedPorts[k]; ok { if _, ok := imageConfig.ExposedPorts[k]; ok {
delete(config.ExposedPorts, k) delete(config.ExposedPorts, k)
} }

@ -0,0 +1,75 @@
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)
}
// WatchtowerContainersFilter filters only watchtower containers
func WatchtowerContainersFilter(c FilterableContainer) bool { return c.IsWatchtower() }
// Filter no containers and returns all
func noFilter(FilterableContainer) bool { return true }
// Filters containers which don't have a specified name
func filterByNames(names []string, baseFilter Filter) Filter {
if len(names) == 0 {
return baseFilter
}
return func(c FilterableContainer) bool {
for _, name := range names {
if (name == c.Name()) || (name == c.Name()[1:]) {
return baseFilter(c)
}
}
return false
}
}
// Filters out containers that don't have the 'enableLabel'
func filterByEnableLabel(baseFilter Filter) Filter {
return func(c FilterableContainer) bool {
// If label filtering is enabled, containers should only be considered
// if the label is specifically set.
_, ok := c.Enabled()
if !ok {
return false
}
return baseFilter(c)
}
}
// Filters out containers that have a 'enableLabel' and is set to disable.
func filterByDisabledLabel(baseFilter Filter) Filter {
return func(c FilterableContainer) bool {
enabledLabel, ok := c.Enabled()
if ok && !enabledLabel {
// If the label has been set and it demands a disable
return false
}
return baseFilter(c)
}
}
// BuildFilter creates the needed filter of containers
func BuildFilter(names []string, enableLabel bool) Filter {
filter := noFilter
filter = filterByNames(names, filter)
if enableLabel {
// If label filtering is enabled, containers should only be considered
// if the label is specifically set.
filter = filterByEnableLabel(filter)
}
filter = filterByDisabledLabel(filter)
return filter
}

@ -0,0 +1,153 @@
package container
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/v2tec/watchtower/container/mocks"
)
func TestWatchtowerContainersFilter(t *testing.T) {
container := new(mocks.FilterableContainer)
container.On("IsWatchtower").Return(true)
assert.True(t, WatchtowerContainersFilter(container))
container.AssertExpectations(t)
}
func TestNoFilter(t *testing.T) {
container := new(mocks.FilterableContainer)
assert.True(t, noFilter(container))
container.AssertExpectations(t)
}
func TestFilterByNames(t *testing.T) {
var names []string
filter := filterByNames(names, nil)
assert.Nil(t, filter)
names = append(names, "test")
filter = filterByNames(names, noFilter)
assert.NotNil(t, filter)
container := new(mocks.FilterableContainer)
container.On("Name").Return("test")
assert.True(t, filter(container))
container.AssertExpectations(t)
container = new(mocks.FilterableContainer)
container.On("Name").Return("NoTest")
assert.False(t, filter(container))
container.AssertExpectations(t)
}
func TestFilterByEnableLabel(t *testing.T) {
filter := filterByEnableLabel(noFilter)
assert.NotNil(t, filter)
container := new(mocks.FilterableContainer)
container.On("Enabled").Return(true, true)
assert.True(t, filter(container))
container.AssertExpectations(t)
container = new(mocks.FilterableContainer)
container.On("Enabled").Return(false, true)
assert.True(t, filter(container))
container.AssertExpectations(t)
container = new(mocks.FilterableContainer)
container.On("Enabled").Return(false, false)
assert.False(t, filter(container))
container.AssertExpectations(t)
}
func TestFilterByDisabledLabel(t *testing.T) {
filter := filterByDisabledLabel(noFilter)
assert.NotNil(t, filter)
container := new(mocks.FilterableContainer)
container.On("Enabled").Return(true, true)
assert.True(t, filter(container))
container.AssertExpectations(t)
container = new(mocks.FilterableContainer)
container.On("Enabled").Return(false, true)
assert.False(t, filter(container))
container.AssertExpectations(t)
container = new(mocks.FilterableContainer)
container.On("Enabled").Return(false, false)
assert.True(t, filter(container))
container.AssertExpectations(t)
}
func TestBuildFilter(t *testing.T) {
var names []string
names = append(names, "test")
filter := BuildFilter(names, false)
container := new(mocks.FilterableContainer)
container.On("Name").Return("Invalid")
container.On("Enabled").Return(false, false)
assert.False(t, filter(container))
container.AssertExpectations(t)
container = new(mocks.FilterableContainer)
container.On("Name").Return("test")
container.On("Enabled").Return(false, false)
assert.True(t, filter(container))
container.AssertExpectations(t)
container = new(mocks.FilterableContainer)
container.On("Name").Return("Invalid")
container.On("Enabled").Return(true, true)
assert.False(t, filter(container))
container.AssertExpectations(t)
container = new(mocks.FilterableContainer)
container.On("Name").Return("test")
container.On("Enabled").Return(true, true)
assert.True(t, filter(container))
container.AssertExpectations(t)
container = new(mocks.FilterableContainer)
container.On("Enabled").Return(false, true)
assert.False(t, filter(container))
container.AssertExpectations(t)
}
func TestBuildFilterEnableLabel(t *testing.T) {
var names []string
names = append(names, "test")
filter := BuildFilter(names, true)
container := new(mocks.FilterableContainer)
container.On("Enabled").Return(false, false)
assert.False(t, filter(container))
container.AssertExpectations(t)
container = new(mocks.FilterableContainer)
container.On("Name").Return("Invalid")
container.On("Enabled").Twice().Return(true, true)
assert.False(t, filter(container))
container.AssertExpectations(t)
container = new(mocks.FilterableContainer)
container.On("Name").Return("test")
container.On("Enabled").Twice().Return(true, true)
assert.True(t, filter(container))
container.AssertExpectations(t)
container = new(mocks.FilterableContainer)
container.On("Enabled").Return(false, true)
assert.False(t, filter(container))
container.AssertExpectations(t)
}

@ -1,42 +0,0 @@
package mockclient
import (
"time"
"github.com/stretchr/testify/mock"
"github.com/v2tec/watchtower/container"
)
type MockClient struct {
mock.Mock
}
func (m *MockClient) ListContainers(cf container.Filter) ([]container.Container, error) {
args := m.Called(cf)
return args.Get(0).([]container.Container), args.Error(1)
}
func (m *MockClient) StopContainer(c container.Container, timeout time.Duration) error {
args := m.Called(c, timeout)
return args.Error(0)
}
func (m *MockClient) StartContainer(c container.Container) error {
args := m.Called(c)
return args.Error(0)
}
func (m *MockClient) RenameContainer(c container.Container, name string) error {
args := m.Called(c, name)
return args.Error(0)
}
func (m *MockClient) IsContainerStale(c container.Container) (bool, error) {
args := m.Called(c)
return args.Bool(0), args.Error(1)
}
func (m *MockClient) RemoveImage(c container.Container) error {
args := m.Called(c)
return args.Error(0)
}

@ -1,17 +0,0 @@
package mockclient
import (
"reflect"
"testing"
"github.com/v2tec/watchtower/container"
)
func TestMockInterface(t *testing.T) {
iface := reflect.TypeOf((*container.Client)(nil)).Elem()
mock := &MockClient{}
if !reflect.TypeOf(mock).Implements(iface) {
t.Fatalf("Mock does not implement the Client interface")
}
}

@ -0,0 +1,57 @@
package mocks
import mock "github.com/stretchr/testify/mock"
// FilterableContainer is an autogenerated mock type for the FilterableContainer type
type FilterableContainer struct {
mock.Mock
}
// Enabled provides a mock function with given fields:
func (_m *FilterableContainer) Enabled() (bool, bool) {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
var r1 bool
if rf, ok := ret.Get(1).(func() bool); ok {
r1 = rf()
} else {
r1 = ret.Get(1).(bool)
}
return r0, r1
}
// IsWatchtower provides a mock function with given fields:
func (_m *FilterableContainer) IsWatchtower() bool {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// Name provides a mock function with given fields:
func (_m *FilterableContainer) Name() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}

@ -5,20 +5,18 @@ import (
"os" "os"
"strings" "strings"
log "github.com/sirupsen/logrus"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/reference" "github.com/docker/docker/api/types/reference"
"github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command"
"github.com/docker/docker/cliconfig" "github.com/docker/docker/cliconfig"
"github.com/docker/docker/cliconfig/configfile" "github.com/docker/docker/cliconfig/configfile"
"github.com/docker/docker/cliconfig/credentials" "github.com/docker/docker/cliconfig/credentials"
log "github.com/sirupsen/logrus"
) )
/** // EncodedAuth returns an encoded auth config for the given registry
* Return an encoded auth config for the given registry // loaded from environment variables or docker config
* loaded from environment variables or docker config // as available in that order
* as available in that order
*/
func EncodedAuth(ref string) (string, error) { func EncodedAuth(ref string) (string, error) {
auth, err := EncodedEnvAuth(ref) auth, err := EncodedEnvAuth(ref)
if err != nil { if err != nil {
@ -27,11 +25,9 @@ func EncodedAuth(ref string) (string, error) {
return auth, err return auth, err
} }
/* // EncodedEnvAuth returns an encoded auth config for the given registry
* Return an encoded auth config for the given registry // loaded from environment variables
* loaded from environment variables // Returns an error if authentication environment variables have not been set
* Returns an error if authentication environment variables have not been set
*/
func EncodedEnvAuth(ref string) (string, error) { func EncodedEnvAuth(ref string) (string, error) {
username := os.Getenv("REPO_USER") username := os.Getenv("REPO_USER")
password := os.Getenv("REPO_PASS") password := os.Getenv("REPO_PASS")
@ -42,17 +38,14 @@ func EncodedEnvAuth(ref string) (string, error) {
} }
log.Debugf("Loaded auth credentials %s for %s", auth, ref) log.Debugf("Loaded auth credentials %s for %s", auth, ref)
return EncodeAuth(auth) return EncodeAuth(auth)
} else {
return "", errors.New("Registry auth environment variables (REPO_USER, REPO_PASS) not set")
} }
return "", errors.New("Registry auth environment variables (REPO_USER, REPO_PASS) not set")
} }
/* // EncodedConfigAuth returns an encoded auth config for the given registry
* Return an encoded auth config for the given registry // loaded from the docker config
* loaded from the docker config // Returns an empty string if credentials cannot be found for the referenced server
* Returns an empty string if credentials cannot be found for the referenced server // The docker config must be mounted on the container
* The docker config must be mounted on the container
*/
func EncodedConfigAuth(ref string) (string, error) { func EncodedConfigAuth(ref string) (string, error) {
server, err := ParseServerAddress(ref) server, err := ParseServerAddress(ref)
configDir := os.Getenv("DOCKER_CONFIG") configDir := os.Getenv("DOCKER_CONFIG")
@ -92,18 +85,14 @@ func CredentialsStore(configFile configfile.ConfigFile) credentials.Store {
return credentials.NewFileStore(&configFile) return credentials.NewFileStore(&configFile)
} }
/* // EncodeAuth Base64 encode an AuthConfig struct for transmission over HTTP
* Base64 encode an AuthConfig struct for transmission over HTTP
*/
func EncodeAuth(auth types.AuthConfig) (string, error) { func EncodeAuth(auth types.AuthConfig) (string, error) {
return command.EncodeAuthToBase64(auth) return command.EncodeAuthToBase64(auth)
} }
/** // DefaultAuthHandler will be invoked if an AuthConfig is rejected
* This function will be invoked if an AuthConfig is rejected // It could be used to return a new value for the "X-Registry-Auth" authentication header,
* It could be used to return a new value for the "X-Registry-Auth" authentication header, // but there's no point trying again with the same value as used in AuthConfig
* but there's no point trying again with the same value as used in AuthConfig
*/
func DefaultAuthHandler() (string, error) { func DefaultAuthHandler() (string, error) {
log.Debug("Authentication request was rejected. Trying again without authentication") log.Debug("Authentication request was rejected. Trying again without authentication")
return "", nil return "", nil

@ -29,6 +29,7 @@ var (
scheduleSpec string scheduleSpec string
cleanup bool cleanup bool
noRestart bool noRestart bool
enableLabel bool
notifier *notifications.Notifier notifier *notifications.Notifier
timeout time.Duration timeout time.Duration
) )
@ -189,6 +190,7 @@ func before(c *cli.Context) error {
if timeout < 0 { if timeout < 0 {
log.Fatal("Please specify a positive value for timeout value.") log.Fatal("Please specify a positive value for timeout value.")
} }
enableLabel = c.GlobalBool("label-enable")
// configure environment vars for client // configure environment vars for client
err := envConfig(c) err := envConfig(c)
@ -196,7 +198,7 @@ func before(c *cli.Context) error {
return err return err
} }
client = container.NewClient(!c.GlobalBool("no-pull"), c.GlobalBool("label-enable")) client = container.NewClient(!c.GlobalBool("no-pull"))
notifier = notifications.NewNotifier(c) notifier = notifications.NewNotifier(c)
return nil return nil
@ -209,6 +211,8 @@ func start(c *cli.Context) error {
log.Fatal(err) log.Fatal(err)
} }
filter := container.BuildFilter(names, enableLabel)
tryLockSem := make(chan bool, 1) tryLockSem := make(chan bool, 1)
tryLockSem <- true tryLockSem <- true
@ -220,7 +224,7 @@ func start(c *cli.Context) error {
case v := <-tryLockSem: case v := <-tryLockSem:
defer func() { tryLockSem <- v }() defer func() { tryLockSem <- v }()
notifier.StartNotification() notifier.StartNotification()
if err := actions.Update(client, names, cleanup, noRestart, timeout); err != nil { if err := actions.Update(client, filter, cleanup, noRestart, timeout); err != nil {
log.Println(err) log.Println(err)
} }
notifier.SendNotification() notifier.SendNotification()

Loading…
Cancel
Save