mirror of
https://github.com/containrrr/watchtower.git
synced 2025-12-17 07:30:13 +01:00
Merge branch 'master' into all-contributors/add-lukapeschke
This commit is contained in:
commit
d66500357b
39 changed files with 325 additions and 257 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
29
cmd/root.go
29
cmd/root.go
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
2
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -34,6 +35,7 @@ var _ = Describe("the actions package", func() {
|
|||
client = mockClient{
|
||||
api: dockerClient,
|
||||
pullImages: false,
|
||||
removeVolumes: false,
|
||||
TestData: &TestData{},
|
||||
}
|
||||
})
|
||||
|
|
@ -64,6 +66,7 @@ var _ = Describe("the actions package", func() {
|
|||
client = mockClient{
|
||||
api: dockerClient,
|
||||
pullImages: false,
|
||||
removeVolumes: false,
|
||||
TestData: &TestData{
|
||||
NameOfContainerToKeep: "test-container-02",
|
||||
Containers: []container.Container{
|
||||
|
|
@ -91,6 +94,7 @@ var _ = Describe("the actions package", func() {
|
|||
client = mockClient{
|
||||
api: dockerClient,
|
||||
pullImages: false,
|
||||
removeVolumes: false,
|
||||
TestData: &TestData{
|
||||
Containers: []container.Container{
|
||||
createMockContainer(
|
||||
|
|
@ -137,6 +141,7 @@ type mockClient struct {
|
|||
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
|
||||
}
|
||||
|
||||
|
|
@ -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
121
internal/actions/update.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
internal/actions/update_params.go
Normal file
15
internal/actions/update_params.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
15
internal/util/rand_name.go
Normal file
15
internal/util/rand_name.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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)
|
||||
9
main.go
9
main.go
|
|
@ -1,17 +1,10 @@
|
|||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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,
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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,7 +31,7 @@ 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")
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,7 +15,7 @@ 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")
|
||||
5
pkg/types/filter.go
Normal file
5
pkg/types/filter.go
Normal 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
|
||||
9
pkg/types/filterable_container.go
Normal file
9
pkg/types/filterable_container.go
Normal 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
7
pkg/types/notifier.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package types
|
||||
|
||||
// Notifier is the interface that all notification services have in common
|
||||
type Notifier interface {
|
||||
StartNotification()
|
||||
SendNotification()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue