Merge branch 'master' into all-contributors/add-lukapeschke

This commit is contained in:
Simon Aronsson 2019-07-22 12:28:59 +02:00 committed by GitHub
commit d66500357b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 325 additions and 257 deletions

View file

@ -303,6 +303,15 @@
"doc"
]
}
{
"login": "zoispag",
"name": "Zois Pagoulatos",
"avatar_url": "https://avatars0.githubusercontent.com/u/21138205?v=4",
"profile": "https://github.com/zoispag",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"projectName": "watchtower",

View file

@ -101,6 +101,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://markwoodbridge.com"><img src="https://avatars2.githubusercontent.com/u/1101318?v=4" width="100px;" alt="Mark Woodbridge"/><br /><sub><b>Mark Woodbridge</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=mrw34" title="Code">💻</a></td>
<td align="center"><a href="http://www.arcticbit.se"><img src="https://avatars0.githubusercontent.com/u/1596025?v=4" width="100px;" alt="Simon Aronsson"/><br /><sub><b>Simon Aronsson</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=simskij" title="Code">💻</a> <a href="#maintenance-simskij" title="Maintenance">🚧</a> <a href="#review-simskij" title="Reviewed Pull Requests">👀</a></td>
<td align="center"><a href="https://github.com/Ansem93"><img src="https://avatars3.githubusercontent.com/u/6626218?v=4" width="100px;" alt="Ansem93"/><br /><sub><b>Ansem93</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=Ansem93" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/zoispag"><img src="https://avatars0.githubusercontent.com/u/21138205?v=4" width="100px;" alt="Zois Pagoulatos"/><br /><sub><b>Zois Pagoulatos</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=zoispag" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/lukapeschke"><img src="https://avatars1.githubusercontent.com/u/17085536?v=4" width="100px;" alt="Luka Peschke"/><br /><sub><b>Luka Peschke</b></sub></a><br /><a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Code">💻</a> <a href="https://github.com/containrrr/watchtower/commits?author=lukapeschke" title="Documentation">📖</a></td>
</tr>
</table>

View file

@ -1,129 +0,0 @@
package actions
import (
"math/rand"
"time"
"github.com/containrrr/watchtower/container"
log "github.com/sirupsen/logrus"
)
var (
letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
)
// UpdateParams contains all different options available to alter the behavior of the Update func
type UpdateParams struct {
Filter container.Filter
Cleanup bool
NoRestart bool
Timeout time.Duration
MonitorOnly bool
}
// 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
// any of the images, the associated containers are stopped and restarted with
// the new image.
func Update(client container.Client, params UpdateParams) error {
log.Debug("Checking containers for updated images")
containers, err := client.ListContainers(params.Filter)
if err != nil {
return err
}
for i, container := range containers {
stale, err := client.IsContainerStale(container)
if err != nil {
log.Infof("Unable to update container %s. Proceeding to next.", containers[i].Name())
log.Debug(err)
stale = false
}
containers[i].Stale = stale
}
containers, err = container.SortByDependencies(containers)
if err != nil {
return err
}
checkDependencies(containers)
if params.MonitorOnly {
return nil
}
// Stop stale containers in reverse order
for i := len(containers) - 1; i >= 0; i-- {
container := containers[i]
if container.IsWatchtower() {
log.Debugf("This is the watchtower container %s", containers[i].Name())
continue
}
if container.Stale {
if err := client.StopContainer(container, params.Timeout); err != nil {
log.Error(err)
}
}
}
// Restart stale containers in sorted order
for _, container := range containers {
if container.Stale {
// Since we can't shutdown a watchtower container immediately, we need to
// start the new one while the old one is still running. This prevents us
// from re-using the same container name so we first rename the current
// instance so that the new one can adopt the old name.
if container.IsWatchtower() {
if err := client.RenameContainer(container, randName()); err != nil {
log.Error(err)
continue
}
}
if !params.NoRestart {
if err := client.StartContainer(container); err != nil {
log.Error(err)
}
}
if params.Cleanup {
client.RemoveImage(container)
}
}
}
return nil
}
func checkDependencies(containers []container.Container) {
for i, parent := range containers {
if parent.Stale {
continue
}
LinkLoop:
for _, linkName := range parent.Links() {
for _, child := range containers {
if child.Name() == linkName && child.Stale {
containers[i].Stale = true
break LinkLoop
}
}
}
}
}
// Generates a random, 32-character, Docker-compatible container name.
func randName() string {
b := make([]rune, 32)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

View file

@ -1,18 +1,20 @@
package cmd
import (
"github.com/containrrr/watchtower/actions"
"github.com/containrrr/watchtower/container"
"github.com/containrrr/watchtower/internal/flags"
"github.com/containrrr/watchtower/notifications"
"github.com/robfig/cron"
log "github.com/sirupsen/logrus"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/containrrr/watchtower/internal/actions"
"github.com/containrrr/watchtower/internal/flags"
"github.com/containrrr/watchtower/pkg/container"
"github.com/containrrr/watchtower/pkg/notifications"
t "github.com/containrrr/watchtower/pkg/types"
"github.com/robfig/cron"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -32,9 +34,9 @@ var (
)
var rootCmd = &cobra.Command{
Use: "watchtower",
Short: "Automatically updates running Docker containers",
Long: `
Use: "watchtower",
Short: "Automatically updates running Docker containers",
Long: `
Watchtower automatically updates running Docker containers whenever a new image is released.
More information available at https://github.com/containrrr/watchtower/.
`,
@ -60,7 +62,7 @@ func Execute() {
func PreRun(cmd *cobra.Command, args []string) {
f := cmd.PersistentFlags()
if enabled, _ := f.GetBool("debug"); enabled == true {
if enabled, _ := f.GetBool("debug"); enabled {
log.SetLevel(log.DebugLevel)
}
@ -92,9 +94,11 @@ func PreRun(cmd *cobra.Command, args []string) {
noPull, _ := f.GetBool("no-pull")
includeStopped, _ := f.GetBool("include-stopped")
removeVolumes, _ := f.GetBool("remove-volumes")
client = container.NewClient(
!noPull,
includeStopped,
removeVolumes,
)
notifier = notifications.NewNotifier(cmd)
@ -116,11 +120,14 @@ func Run(c *cobra.Command, names []string) {
log.Fatal(err)
}
runUpgradesOnSchedule(filter)
if err := runUpgradesOnSchedule(filter); err != nil {
log.Error(err)
}
os.Exit(1)
}
func runUpgradesOnSchedule(filter container.Filter) error {
func runUpgradesOnSchedule(filter t.Filter) error {
tryLockSem := make(chan bool, 1)
tryLockSem <- true
@ -161,7 +168,7 @@ func runUpgradesOnSchedule(filter container.Filter) error {
return nil
}
func runUpdatesWithNotifications(filter container.Filter) {
func runUpdatesWithNotifications(filter t.Filter) {
notifier.StartNotification()
updateParams := actions.UpdateParams{
Filter: filter,
@ -176,5 +183,3 @@ func runUpdatesWithNotifications(filter container.Filter) {
}
notifier.SendNotification()
}

View file

@ -47,6 +47,16 @@ Environment Variable: WATCHTOWER_CLEANUP
Default: false
```
## Remove attached volumes
Removes attached volumes after updating. When this flag is specified, watchtower will remove all attached volumes from the container before restarting container with a new image. Use this option to force new volumes to be populated as containers are updated.
```
Argument: --remove-volumes
Environment Variable: WATCHTOWER_REMOVE_VOLUMES
Type: Boolean
Default: false
```
## Debug
Enable debug mode with verbose logging.

2
go.sum
View file

@ -68,6 +68,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gorilla/mux v1.7.0 h1:tOSd0UKHQd6urX6ApfOn4XdBMY6Sh1MfxV3kmaazO+U=
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
@ -258,5 +259,6 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -2,14 +2,15 @@ package actions_test
import (
"errors"
"github.com/containrrr/watchtower/internal/actions"
"testing"
"time"
"github.com/containrrr/watchtower/actions"
"github.com/containrrr/watchtower/container"
"github.com/containrrr/watchtower/container/mocks"
"github.com/containrrr/watchtower/pkg/container"
"github.com/containrrr/watchtower/pkg/container/mocks"
"github.com/docker/docker/api/types"
t "github.com/containrrr/watchtower/pkg/types"
cli "github.com/docker/docker/client"
. "github.com/onsi/ginkgo"
@ -32,9 +33,10 @@ var _ = Describe("the actions package", func() {
})
BeforeEach(func() {
client = mockClient{
api: dockerClient,
pullImages: false,
TestData: &TestData{},
api: dockerClient,
pullImages: false,
removeVolumes: false,
TestData: &TestData{},
}
})
@ -62,8 +64,9 @@ var _ = Describe("the actions package", func() {
When("given multiple containers", func() {
BeforeEach(func() {
client = mockClient{
api: dockerClient,
pullImages: false,
api: dockerClient,
pullImages: false,
removeVolumes: false,
TestData: &TestData{
NameOfContainerToKeep: "test-container-02",
Containers: []container.Container{
@ -89,8 +92,9 @@ var _ = Describe("the actions package", func() {
When("deciding whether to cleanup images", func() {
BeforeEach(func() {
client = mockClient{
api: dockerClient,
pullImages: false,
api: dockerClient,
pullImages: false,
removeVolumes: false,
TestData: &TestData{
Containers: []container.Container{
createMockContainer(
@ -134,9 +138,10 @@ func createMockContainer(id string, name string, image string, created time.Time
}
type mockClient struct {
TestData *TestData
api cli.CommonAPIClient
pullImages bool
TestData *TestData
api cli.CommonAPIClient
pullImages bool
removeVolumes bool
}
type TestData struct {
@ -145,7 +150,7 @@ type TestData struct {
Containers []container.Container
}
func (client mockClient) ListContainers(f container.Filter) ([]container.Container, error) {
func (client mockClient) ListContainers(f t.Filter) ([]container.Container, error) {
return client.TestData.Containers, nil
}

View file

@ -11,7 +11,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/containrrr/watchtower/container"
"github.com/containrrr/watchtower/pkg/container"
)
// CheckForMultipleWatchtowerInstances will ensure that there are not multiple instances of the
@ -50,7 +50,7 @@ func cleanupExcessWatchtowers(containers []container.Container, client container
continue
}
if cleanup == true {
if cleanup {
if err := client.RemoveImage(c); err != nil {
// logging the original here as we're just returning a count
logrus.Error(err)

121
internal/actions/update.go Normal file
View file

@ -0,0 +1,121 @@
package actions
import (
"github.com/containrrr/watchtower/internal/util"
"github.com/containrrr/watchtower/pkg/container"
log "github.com/sirupsen/logrus"
)
// 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
// any of the images, the associated containers are stopped and restarted with
// the new image.
func Update(client container.Client, params UpdateParams) error {
log.Debug("Checking containers for updated images")
containers, err := client.ListContainers(params.Filter)
if err != nil {
return err
}
for i, container := range containers {
stale, err := client.IsContainerStale(container)
if err != nil {
log.Infof("Unable to update container %s. Proceeding to next.", containers[i].Name())
log.Debug(err)
stale = false
}
containers[i].Stale = stale
}
containers, err = container.SortByDependencies(containers)
if err != nil {
return err
}
checkDependencies(containers)
if params.MonitorOnly {
return nil
}
stopContainersInReversedOrder(containers, client, params)
restartContainersInSortedOrder(containers, client, params)
return nil
}
func stopContainersInReversedOrder(containers []container.Container, client container.Client, params UpdateParams) {
for i := len(containers) - 1; i >= 0; i-- {
stopStaleContainer(containers[i], client, params)
}
}
func stopStaleContainer(container container.Container, client container.Client, params UpdateParams) {
if container.IsWatchtower() {
log.Debugf("This is the watchtower container %s", container.Name())
return
}
if !container.Stale {
return
}
err := client.StopContainer(container, params.Timeout)
if err != nil {
log.Error(err)
}
}
func restartContainersInSortedOrder(containers []container.Container, client container.Client, params UpdateParams) {
for _, container := range containers {
if !container.Stale {
continue
}
restartStaleContainer(container, client, params)
}
}
func restartStaleContainer(container container.Container, client container.Client, params UpdateParams) {
// Since we can't shutdown a watchtower container immediately, we need to
// start the new one while the old one is still running. This prevents us
// from re-using the same container name so we first rename the current
// instance so that the new one can adopt the old name.
if container.IsWatchtower() {
if err := client.RenameContainer(container, util.RandName()); err != nil {
log.Error(err)
return
}
}
if !params.NoRestart {
if err := client.StartContainer(container); err != nil {
log.Error(err)
}
}
if params.Cleanup {
if err := client.RemoveImage(container); err != nil {
log.Error(err)
}
}
}
func checkDependencies(containers []container.Container) {
for i, parent := range containers {
if parent.Stale {
continue
}
LinkLoop:
for _, linkName := range parent.Links() {
for _, child := range containers {
if child.Name() == linkName && child.Stale {
containers[i].Stale = true
break LinkLoop
}
}
}
}
}

View file

@ -0,0 +1,15 @@
package actions
import (
t "github.com/containrrr/watchtower/pkg/types"
"time"
)
// UpdateParams contains all different options available to alter the behavior of the Update func
type UpdateParams struct {
Filter t.Filter
Cleanup bool
NoRestart bool
Timeout time.Duration
MonitorOnly bool
}

View file

@ -1,11 +1,12 @@
package flags
import (
"os"
"time"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"os"
"time"
)
// RegisterDockerFlags that are used directly by the docker api client
@ -52,6 +53,12 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
viper.GetBool("WATCHTOWER_CLEANUP"),
"remove previously used images after updating")
flags.BoolP(
"remove-volumes",
"",
viper.GetBool("WATCHTOWER_REMOVE_VOLUMES"),
"remove attached volumes before updating")
flags.BoolP(
"label-enable",
"e",
@ -64,7 +71,6 @@ func RegisterSystemFlags(rootCmd *cobra.Command) {
viper.GetBool("WATCHTOWER_DEBUG"),
"enable debug mode with verbose logging")
flags.BoolP(
"monitor-only",
"m",
@ -253,7 +259,6 @@ func ReadFlags(cmd *cobra.Command) (bool, bool, bool, time.Duration) {
return cleanup, noRestart, monitorOnly, timeout
}
func setEnvOptStr(env string, opt string) error {
if opt == "" || opt == os.Getenv(env) {
return nil
@ -266,9 +271,8 @@ func setEnvOptStr(env string, opt string) error {
}
func setEnvOptBool(env string, opt bool) error {
if opt == true {
if opt {
return setEnvOptStr(env, "1")
}
return nil
}

View file

@ -0,0 +1,15 @@
package util
import "math/rand"
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
// RandName Generates a random, 32-character, Docker-compatible container name.
func RandName() string {
b := make([]rune, 32)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

View file

@ -1,6 +1,7 @@
package container
package util
func sliceEqual(s1, s2 []string) bool {
// SliceEqual compares two slices and checks whether they have equal content
func SliceEqual(s1, s2 []string) bool {
if len(s1) != len(s2) {
return false
}
@ -14,7 +15,8 @@ func sliceEqual(s1, s2 []string) bool {
return true
}
func sliceSubtract(a1, a2 []string) []string {
// SliceSubtract subtracts the content of slice a2 from slice a1
func SliceSubtract(a1, a2 []string) []string {
a := []string{}
for _, e1 := range a1 {
@ -35,7 +37,8 @@ func sliceSubtract(a1, a2 []string) []string {
return a
}
func stringMapSubtract(m1, m2 map[string]string) map[string]string {
// StringMapSubtract subtracts the content of structmap m2 from structmap m1
func StringMapSubtract(m1, m2 map[string]string) map[string]string {
m := map[string]string{}
for k1, v1 := range m1 {
@ -51,7 +54,8 @@ func stringMapSubtract(m1, m2 map[string]string) map[string]string {
return m
}
func structMapSubtract(m1, m2 map[string]struct{}) map[string]struct{} {
// StructMapSubtract subtracts the content of structmap m2 from structmap m1
func StructMapSubtract(m1, m2 map[string]struct{}) map[string]struct{} {
m := map[string]struct{}{}
for k1, v1 := range m1 {

View file

@ -1,17 +1,15 @@
package container
package util
import (
"testing"
"github.com/stretchr/testify/assert"
"testing"
)
func TestSliceEqual_True(t *testing.T) {
s1 := []string{"a", "b", "c"}
s2 := []string{"a", "b", "c"}
result := sliceEqual(s1, s2)
result := SliceEqual(s1, s2)
assert.True(t, result)
}
@ -20,7 +18,7 @@ func TestSliceEqual_DifferentLengths(t *testing.T) {
s1 := []string{"a", "b", "c"}
s2 := []string{"a", "b", "c", "d"}
result := sliceEqual(s1, s2)
result := SliceEqual(s1, s2)
assert.False(t, result)
}
@ -29,7 +27,7 @@ func TestSliceEqual_DifferentContents(t *testing.T) {
s1 := []string{"a", "b", "c"}
s2 := []string{"a", "b", "d"}
result := sliceEqual(s1, s2)
result := SliceEqual(s1, s2)
assert.False(t, result)
}
@ -38,7 +36,7 @@ func TestSliceSubtract(t *testing.T) {
a1 := []string{"a", "b", "c"}
a2 := []string{"a", "c"}
result := sliceSubtract(a1, a2)
result := SliceSubtract(a1, a2)
assert.Equal(t, []string{"b"}, result)
assert.Equal(t, []string{"a", "b", "c"}, a1)
assert.Equal(t, []string{"a", "c"}, a2)
@ -48,7 +46,7 @@ func TestStringMapSubtract(t *testing.T) {
m1 := map[string]string{"a": "a", "b": "b", "c": "sea"}
m2 := map[string]string{"a": "a", "c": "c"}
result := stringMapSubtract(m1, m2)
result := StringMapSubtract(m1, m2)
assert.Equal(t, map[string]string{"b": "b", "c": "sea"}, result)
assert.Equal(t, map[string]string{"a": "a", "b": "b", "c": "sea"}, m1)
assert.Equal(t, map[string]string{"a": "a", "c": "c"}, m2)
@ -59,7 +57,7 @@ func TestStructMapSubtract(t *testing.T) {
m1 := map[string]struct{}{"a": x, "b": x, "c": x}
m2 := map[string]struct{}{"a": x, "c": x}
result := structMapSubtract(m1, m2)
result := StructMapSubtract(m1, m2)
assert.Equal(t, map[string]struct{}{"b": x}, result)
assert.Equal(t, map[string]struct{}{"a": x, "b": x, "c": x}, m1)
assert.Equal(t, map[string]struct{}{"a": x, "c": x}, m2)

11
main.go
View file

@ -1,21 +1,14 @@
package main // import "github.com/containrrr/watchtower"
package main
import (
"github.com/containrrr/watchtower/cmd"
log "github.com/sirupsen/logrus"
)
// DockerAPIMinVersion is the version of the docker API, which is minimally required by
// watchtower. Currently we require at least API 1.24 and therefore Docker 1.12 or later.
var version = "master"
var commit = "unknown"
var date = "unknown"
func init() {
log.SetLevel(log.InfoLevel)
}
func main() {
cmd.Execute()
cmd.Execute()
}

View file

@ -2,12 +2,13 @@ package container
import (
"fmt"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"io/ioutil"
"time"
t "github.com/containrrr/watchtower/pkg/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
dockerclient "github.com/docker/docker/client"
log "github.com/sirupsen/logrus"
@ -21,7 +22,7 @@ const (
// A Client is the interface through which watchtower interacts with the
// Docker API.
type Client interface {
ListContainers(Filter) ([]Container, error)
ListContainers(t.Filter) ([]Container, error)
StopContainer(Container, time.Duration) error
StartContainer(Container) error
RenameContainer(Container, string) error
@ -35,7 +36,7 @@ type Client interface {
// * DOCKER_HOST the docker-engine host to send api requests to
// * DOCKER_TLS_VERIFY whether to verify tls certificates
// * DOCKER_API_VERSION the minimum docker api version to work with
func NewClient(pullImages bool, includeStopped bool) Client {
func NewClient(pullImages bool, includeStopped bool, removeVolumes bool) Client {
cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
if err != nil {
@ -45,6 +46,7 @@ func NewClient(pullImages bool, includeStopped bool) Client {
return dockerClient{
api: cli,
pullImages: pullImages,
removeVolumes: removeVolumes,
includeStopped: includeStopped,
}
}
@ -52,10 +54,11 @@ func NewClient(pullImages bool, includeStopped bool) Client {
type dockerClient struct {
api dockerclient.CommonAPIClient
pullImages bool
removeVolumes bool
includeStopped bool
}
func (client dockerClient) ListContainers(fn Filter) ([]Container, error) {
func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) {
cs := []Container{}
bg := context.Background()
@ -71,7 +74,7 @@ func (client dockerClient) ListContainers(fn Filter) ([]Container, error) {
types.ContainerListOptions{
Filters: filter,
})
if err != nil {
return nil, err
}
@ -123,21 +126,21 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
}
}
// Wait for container to exit, but proceed anyway after the timeout elapses
client.waitForStop(c, timeout)
// TODO: This should probably be checked.
_ = client.waitForStopOrTimeout(c, timeout)
if c.containerInfo.HostConfig.AutoRemove {
log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", c.ID())
} else {
log.Debugf("Removing container %s", c.ID())
if err := client.api.ContainerRemove(bg, c.ID(), types.ContainerRemoveOptions{Force: true, RemoveVolumes: false}); err != nil {
if err := client.api.ContainerRemove(bg, c.ID(), types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.removeVolumes}); err != nil {
return err
}
}
// Wait for container to be removed. In this case an error is a good thing
if err := client.waitForStop(c, timeout); err == nil {
if err := client.waitForStopOrTimeout(c, timeout); err == nil {
return fmt.Errorf("Container %s (%s) could not be removed", c.Name(), c.ID())
}
@ -242,7 +245,9 @@ func (client dockerClient) IsContainerStale(c Container) (bool, error) {
defer response.Close()
// the pull request will be aborted prematurely unless the response is read
_, err = ioutil.ReadAll(response)
if _, err = ioutil.ReadAll(response); err != nil {
log.Error(err)
}
}
newImageInfo, _, err := client.api.ImageInspectWithRaw(bg, imageName)
@ -266,7 +271,7 @@ func (client dockerClient) RemoveImage(c Container) error {
return err
}
func (client dockerClient) waitForStop(c Container, waitTime time.Duration) error {
func (client dockerClient) waitForStopOrTimeout(c Container, waitTime time.Duration) error {
bg := context.Background()
timeout := time.After(waitTime)

View file

@ -2,6 +2,7 @@ package container
import (
"fmt"
"github.com/containrrr/watchtower/internal/util"
"strconv"
"strings"
@ -146,19 +147,19 @@ func (c Container) runtimeConfig() *dockercontainer.Config {
config.User = ""
}
if sliceEqual(config.Cmd, imageConfig.Cmd) {
if util.SliceEqual(config.Cmd, imageConfig.Cmd) {
config.Cmd = nil
}
if sliceEqual(config.Entrypoint, imageConfig.Entrypoint) {
if util.SliceEqual(config.Entrypoint, imageConfig.Entrypoint) {
config.Entrypoint = nil
}
config.Env = sliceSubtract(config.Env, imageConfig.Env)
config.Env = util.SliceSubtract(config.Env, imageConfig.Env)
config.Labels = stringMapSubtract(config.Labels, imageConfig.Labels)
config.Labels = util.StringMapSubtract(config.Labels, imageConfig.Labels)
config.Volumes = structMapSubtract(config.Volumes, imageConfig.Volumes)
config.Volumes = util.StructMapSubtract(config.Volumes, imageConfig.Volumes)
// subtract ports exposed in image from container
for k := range config.ExposedPorts {

View file

@ -1,7 +1,7 @@
package container
import (
"github.com/containrrr/watchtower/container/mocks"
"github.com/containrrr/watchtower/pkg/container/mocks"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
cli "github.com/docker/docker/client"
@ -23,8 +23,7 @@ var _ = Describe("the container", func() {
server := mocks.NewMockAPIServer()
docker, _ = cli.NewClientWithOpts(
cli.WithHost(server.URL),
cli.WithHTTPClient(server.Client(),
))
cli.WithHTTPClient(server.Client()))
client = dockerClient{
api: docker,
pullImages: false,

View file

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

View file

@ -3,8 +3,8 @@ package container
import (
"testing"
"github.com/containrrr/watchtower/pkg/container/mocks"
"github.com/stretchr/testify/assert"
"github.com/containrrr/watchtower/container/mocks"
)
func TestWatchtowerContainersFilter(t *testing.T) {

View file

@ -48,6 +48,10 @@ func EncodedEnvAuth(ref string) (string, error) {
// The docker config must be mounted on the container
func EncodedConfigAuth(ref string) (string, error) {
server, err := ParseServerAddress(ref)
if err != nil {
log.Errorf("Unable to parse the image ref %s", err)
return "", err
}
configDir := os.Getenv("DOCKER_CONFIG")
if configDir == "" {
configDir = "/"
@ -58,7 +62,8 @@ func EncodedConfigAuth(ref string) (string, error) {
return "", err
}
credStore := CredentialsStore(*configFile)
auth, err := credStore.Get(server) // returns (types.AuthConfig{}) if server not in credStore
auth, _ := credStore.Get(server) // returns (types.AuthConfig{}) if server not in credStore
if auth == (types.AuthConfig{}) {
log.Debugf("No credentials for %s in %s", server, configFile.Filename)
return "", nil

View file

@ -1,15 +1,11 @@
package container
import (
"github.com/stretchr/testify/assert"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestEncodedEnvAuth_ShouldReturnAnErrorIfRepoEnvsAreUnset(t *testing.T) {
os.Unsetenv("REPO_USER")
os.Unsetenv("REPO_PASS")

View file

@ -8,16 +8,16 @@ import (
"os"
"time"
"strconv"
t "github.com/containrrr/watchtower/pkg/types"
log "github.com/sirupsen/logrus"
"strconv"
)
const (
emailType = "email"
)
// Implements typeNotifier, logrus.Hook
// Implements Notifier, logrus.Hook
// The default logrus email integration would have several issues:
// - It would send one email per log output
// - It would only send errors
@ -31,12 +31,12 @@ type emailTypeNotifier struct {
logLevels []log.Level
}
func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) typeNotifier {
func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
flags := c.PersistentFlags()
from, _ := flags.GetString("notification-email-from")
to, _ := flags.GetString("notification-email-to")
server, _ := flags.GetString("notification-email-server")
to, _ := flags.GetString("notification-email-to")
server, _ := flags.GetString("notification-email-server")
user, _ := flags.GetString("notification-email-server-user")
password, _ := flags.GetString("notification-email-server-password")
port, _ := flags.GetInt("notification-email-server-port")
@ -70,7 +70,7 @@ func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte {
}
t := time.Now()
header := make(map[string]string)
header["From"] = e.From
header["To"] = e.To

View file

@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra"
"net/http"
t "github.com/containrrr/watchtower/pkg/types"
log "github.com/sirupsen/logrus"
"io/ioutil"
)
@ -21,7 +22,7 @@ type msTeamsTypeNotifier struct {
data bool
}
func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) typeNotifier {
func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
flags := cmd.PersistentFlags()

View file

@ -1,19 +1,15 @@
package notifications
import (
ty "github.com/containrrr/watchtower/pkg/types"
"github.com/johntdyer/slackrus"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
type typeNotifier interface {
StartNotification()
SendNotification()
}
// Notifier can send log output as notification to admins, with optional batching.
type Notifier struct {
types []typeNotifier
types []ty.Notifier
}
// NewNotifier creates and returns a new Notifier, using global configuration.
@ -34,7 +30,7 @@ func NewNotifier(c *cobra.Command) *Notifier {
types, _ := f.GetStringSlice("notifications")
for _, t := range types {
var tn typeNotifier
var tn ty.Notifier
switch t {
case emailType:
tn = newEmailNotifier(c, acceptedLogLevels)

View file

@ -1,6 +1,7 @@
package notifications
import (
t "github.com/containrrr/watchtower/pkg/types"
"github.com/johntdyer/slackrus"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@ -14,14 +15,14 @@ type slackTypeNotifier struct {
slackrus.SlackrusHook
}
func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) typeNotifier {
func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier {
flags := c.PersistentFlags()
hookURL, _ := flags.GetString("notification-slack-hook-url")
hookURL, _ := flags.GetString("notification-slack-hook-url")
userName, _ := flags.GetString("notification-slack-identifier")
channel, _ := flags.GetString("notification-slack-channel")
emoji, _ := flags.GetString("notification-slack-icon-emoji")
iconURL, _ := flags.GetString("notification-slack-icon-url")
channel, _ := flags.GetString("notification-slack-channel")
emoji, _ := flags.GetString("notification-slack-icon-emoji")
iconURL, _ := flags.GetString("notification-slack-icon-url")
n := &slackTypeNotifier{
SlackrusHook: slackrus.SlackrusHook{

5
pkg/types/filter.go Normal file
View file

@ -0,0 +1,5 @@
package types
// 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

View file

@ -0,0 +1,9 @@
package types
// A FilterableContainer is the interface which is used to filter
// containers.
type FilterableContainer interface {
Name() string
IsWatchtower() bool
Enabled() (bool, bool)
}

7
pkg/types/notifier.go Normal file
View file

@ -0,0 +1,7 @@
package types
// Notifier is the interface that all notification services have in common
type Notifier interface {
StartNotification()
SendNotification()
}