mirror of
https://github.com/containrrr/watchtower.git
synced 2025-12-14 06:06:38 +01:00
refactor: extract code from the container package
This commit is contained in:
parent
4130b110c6
commit
d1abce889a
15 changed files with 253 additions and 185 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/containrrr/watchtower/pkg/filters"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -108,7 +109,7 @@ func PreRun(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
// Run is the main execution flow of the command
|
// Run is the main execution flow of the command
|
||||||
func Run(c *cobra.Command, names []string) {
|
func Run(c *cobra.Command, names []string) {
|
||||||
filter := container.BuildFilter(names, enableLabel)
|
filter := filters.BuildFilter(names, enableLabel)
|
||||||
runOnce, _ := c.PersistentFlags().GetBool("run-once")
|
runOnce, _ := c.PersistentFlags().GetBool("run-once")
|
||||||
|
|
||||||
if runOnce {
|
if runOnce {
|
||||||
|
|
@ -172,7 +173,7 @@ func runUpgradesOnSchedule(filter t.Filter) error {
|
||||||
|
|
||||||
func runUpdatesWithNotifications(filter t.Filter) {
|
func runUpdatesWithNotifications(filter t.Filter) {
|
||||||
notifier.StartNotification()
|
notifier.StartNotification()
|
||||||
updateParams := actions.UpdateParams{
|
updateParams := t.UpdateParams{
|
||||||
Filter: filter,
|
Filter: filter,
|
||||||
Cleanup: cleanup,
|
Cleanup: cleanup,
|
||||||
NoRestart: noRestart,
|
NoRestart: noRestart,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package actions
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/containrrr/watchtower/pkg/filters"
|
||||||
|
"github.com/containrrr/watchtower/pkg/sorter"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -19,7 +21,7 @@ import (
|
||||||
// will stop and remove all but the most recently started container.
|
// will stop and remove all but the most recently started container.
|
||||||
func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool) error {
|
func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool) error {
|
||||||
awaitDockerClient()
|
awaitDockerClient()
|
||||||
containers, err := client.ListContainers(container.WatchtowerContainersFilter)
|
containers, err := client.ListContainers(filters.WatchtowerContainersFilter)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|
@ -39,7 +41,7 @@ func cleanupExcessWatchtowers(containers []container.Container, client container
|
||||||
var cleanupErrors int
|
var cleanupErrors int
|
||||||
var stopErrors int
|
var stopErrors int
|
||||||
|
|
||||||
sort.Sort(container.ByCreated(containers))
|
sort.Sort(sorter.ByCreated(containers))
|
||||||
allContainersExceptLast := containers[0 : len(containers)-1]
|
allContainersExceptLast := containers[0 : len(containers)-1]
|
||||||
|
|
||||||
for _, c := range allContainersExceptLast {
|
for _, c := range allContainersExceptLast {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ package actions
|
||||||
import (
|
import (
|
||||||
"github.com/containrrr/watchtower/internal/util"
|
"github.com/containrrr/watchtower/internal/util"
|
||||||
"github.com/containrrr/watchtower/pkg/container"
|
"github.com/containrrr/watchtower/pkg/container"
|
||||||
|
"github.com/containrrr/watchtower/pkg/lifecycle"
|
||||||
|
"github.com/containrrr/watchtower/pkg/sorter"
|
||||||
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -10,10 +13,12 @@ import (
|
||||||
// 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, params UpdateParams) error {
|
func Update(client container.Client, params types.UpdateParams) error {
|
||||||
log.Debug("Checking containers for updated images")
|
log.Debug("Checking containers for updated images")
|
||||||
|
|
||||||
executePreCheck(client, params)
|
if params.LifecycleHooks {
|
||||||
|
lifecycle.ExecutePreChecks(client, params)
|
||||||
|
}
|
||||||
|
|
||||||
containers, err := client.ListContainers(params.Filter)
|
containers, err := client.ListContainers(params.Filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -30,7 +35,7 @@ func Update(client container.Client, params UpdateParams) error {
|
||||||
containers[i].Stale = stale
|
containers[i].Stale = stale
|
||||||
}
|
}
|
||||||
|
|
||||||
containers, err = container.SortByDependencies(containers)
|
containers, err = sorter.SortByDependencies(containers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -38,24 +43,28 @@ func Update(client container.Client, params UpdateParams) error {
|
||||||
checkDependencies(containers)
|
checkDependencies(containers)
|
||||||
|
|
||||||
if params.MonitorOnly {
|
if params.MonitorOnly {
|
||||||
executePostCheck(client, params)
|
if params.LifecycleHooks {
|
||||||
|
lifecycle.ExecutePostChecks(client, params)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
stopContainersInReversedOrder(containers, client, params)
|
stopContainersInReversedOrder(containers, client, params)
|
||||||
restartContainersInSortedOrder(containers, client, params)
|
restartContainersInSortedOrder(containers, client, params)
|
||||||
|
|
||||||
executePostCheck(client, params)
|
if params.LifecycleHooks {
|
||||||
|
lifecycle.ExecutePostChecks(client, params)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopContainersInReversedOrder(containers []container.Container, client container.Client, params UpdateParams) {
|
func stopContainersInReversedOrder(containers []container.Container, client container.Client, params types.UpdateParams) {
|
||||||
for i := len(containers) - 1; i >= 0; i-- {
|
for i := len(containers) - 1; i >= 0; i-- {
|
||||||
stopStaleContainer(containers[i], client, params)
|
stopStaleContainer(containers[i], client, params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopStaleContainer(container container.Container, client container.Client, params UpdateParams) {
|
func stopStaleContainer(container container.Container, client container.Client, params types.UpdateParams) {
|
||||||
if container.IsWatchtower() {
|
if container.IsWatchtower() {
|
||||||
log.Debugf("This is the watchtower container %s", container.Name())
|
log.Debugf("This is the watchtower container %s", container.Name())
|
||||||
return
|
return
|
||||||
|
|
@ -64,15 +73,17 @@ func stopStaleContainer(container container.Container, client container.Client,
|
||||||
if !container.Stale {
|
if !container.Stale {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if params.LifecycleHooks {
|
||||||
|
lifecycle.ExecutePreUpdateCommand(client, container)
|
||||||
|
|
||||||
executePreUpdateCommand(client, container)
|
}
|
||||||
|
|
||||||
if err := client.StopContainer(container, params.Timeout); err != nil {
|
if err := client.StopContainer(container, params.Timeout); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restartContainersInSortedOrder(containers []container.Container, client container.Client, params UpdateParams) {
|
func restartContainersInSortedOrder(containers []container.Container, client container.Client, params types.UpdateParams) {
|
||||||
imageIDs := make(map[string]bool)
|
imageIDs := make(map[string]bool)
|
||||||
|
|
||||||
for _, container := range containers {
|
for _, container := range containers {
|
||||||
|
|
@ -91,7 +102,7 @@ func restartContainersInSortedOrder(containers []container.Container, client con
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restartStaleContainer(container container.Container, client container.Client, params UpdateParams) {
|
func restartStaleContainer(container container.Container, client container.Client, params types.UpdateParams) {
|
||||||
// Since we can't shutdown a watchtower container immediately, we need to
|
// 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
|
// 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
|
// from re-using the same container name so we first rename the current
|
||||||
|
|
@ -107,7 +118,7 @@ func restartStaleContainer(container container.Container, client container.Clien
|
||||||
if newContainerID, err := client.StartContainer(container); err != nil {
|
if newContainerID, err := client.StartContainer(container); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
} else if container.Stale && params.LifecycleHooks {
|
} else if container.Stale && params.LifecycleHooks {
|
||||||
executePostUpdateCommand(client, newContainerID)
|
lifecycle.ExecutePostUpdateCommand(client, newContainerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,82 +141,3 @@ func checkDependencies(containers []container.Container) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func executePreCheck(client container.Client, params UpdateParams) {
|
|
||||||
containers, err := client.ListContainers(params.Filter)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, container := range containers {
|
|
||||||
executePreCheckCommand(client, container)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func executePostCheck(client container.Client, params UpdateParams) {
|
|
||||||
containers, err := client.ListContainers(params.Filter)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, container := range containers {
|
|
||||||
executePostCheckCommand(client, container)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func executePreCheckCommand(client container.Client, container container.Container) {
|
|
||||||
command := container.GetLifecyclePreCheckCommand()
|
|
||||||
if len(command) == 0 {
|
|
||||||
log.Debug("No pre-check command supplied. Skipping")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Executing pre-check command.")
|
|
||||||
if err := client.ExecuteCommand(container.ID(), command); err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func executePostCheckCommand(client container.Client, container container.Container) {
|
|
||||||
command := container.GetLifecyclePostCheckCommand()
|
|
||||||
if len(command) == 0 {
|
|
||||||
log.Debug("No post-check command supplied. Skipping")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Executing post-check command.")
|
|
||||||
if err := client.ExecuteCommand(container.ID(), command); err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func executePreUpdateCommand(client container.Client, container container.Container) {
|
|
||||||
|
|
||||||
command := container.GetLifecyclePreUpdateCommand()
|
|
||||||
if len(command) == 0 {
|
|
||||||
log.Debug("No pre-update command supplied. Skipping")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Executing pre-update command.")
|
|
||||||
if err := client.ExecuteCommand(container.ID(), command); err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func executePostUpdateCommand(client container.Client, newContainerID string) {
|
|
||||||
newContainer, err := client.GetContainer(newContainerID)
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
command := newContainer.GetLifecyclePostUpdateCommand()
|
|
||||||
if len(command) == 0 {
|
|
||||||
log.Debug("No post-update command supplied. Skipping")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("Executing post-update command.")
|
|
||||||
if err := client.ExecuteCommand(newContainerID, command); err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"github.com/containrrr/watchtower/internal/actions"
|
"github.com/containrrr/watchtower/internal/actions"
|
||||||
"github.com/containrrr/watchtower/pkg/container"
|
"github.com/containrrr/watchtower/pkg/container"
|
||||||
"github.com/containrrr/watchtower/pkg/container/mocks"
|
"github.com/containrrr/watchtower/pkg/container/mocks"
|
||||||
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
cli "github.com/docker/docker/client"
|
cli "github.com/docker/docker/client"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -59,7 +60,7 @@ var _ = Describe("the update action", func() {
|
||||||
When("there are multiple containers using the same image", func() {
|
When("there are multiple containers using the same image", func() {
|
||||||
It("should only try to remove the image once", func() {
|
It("should only try to remove the image once", func() {
|
||||||
|
|
||||||
err := actions.Update(client, actions.UpdateParams{ Cleanup: true })
|
err := actions.Update(client, types.UpdateParams{ Cleanup: true })
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
|
||||||
})
|
})
|
||||||
|
|
@ -75,7 +76,7 @@ var _ = Describe("the update action", func() {
|
||||||
time.Now(),
|
time.Now(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
err := actions.Update(client, actions.UpdateParams{ Cleanup: true })
|
err := actions.Update(client, types.UpdateParams{ Cleanup: true })
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(2))
|
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(2))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package container
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/containrrr/watchtower/pkg/registry"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -12,7 +13,7 @@ import (
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
dockerclient "github.com/docker/docker/client"
|
sdkClient "github.com/docker/docker/client"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
|
@ -40,7 +41,7 @@ type Client interface {
|
||||||
// * DOCKER_TLS_VERIFY whether to verify tls certificates
|
// * DOCKER_TLS_VERIFY whether to verify tls certificates
|
||||||
// * DOCKER_API_VERSION the minimum docker api version to work with
|
// * DOCKER_API_VERSION the minimum docker api version to work with
|
||||||
func NewClient(pullImages bool, includeStopped bool, reviveStopped bool, removeVolumes bool) Client {
|
func NewClient(pullImages bool, includeStopped bool, reviveStopped bool, removeVolumes bool) Client {
|
||||||
cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
|
cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error instantiating Docker client: %s", err)
|
log.Fatalf("Error instantiating Docker client: %s", err)
|
||||||
|
|
@ -56,7 +57,7 @@ func NewClient(pullImages bool, includeStopped bool, reviveStopped bool, removeV
|
||||||
}
|
}
|
||||||
|
|
||||||
type dockerClient struct {
|
type dockerClient struct {
|
||||||
api dockerclient.CommonAPIClient
|
api sdkClient.CommonAPIClient
|
||||||
pullImages bool
|
pullImages bool
|
||||||
removeVolumes bool
|
removeVolumes bool
|
||||||
includeStopped bool
|
includeStopped bool
|
||||||
|
|
@ -231,53 +232,60 @@ func (client dockerClient) RenameContainer(c Container, newName string) error {
|
||||||
return client.api.ContainerRename(bg, c.ID(), newName)
|
return client.api.ContainerRename(bg, c.ID(), newName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client dockerClient) IsContainerStale(c Container) (bool, error) {
|
func (client dockerClient) IsContainerStale(container Container) (bool, error) {
|
||||||
bg := context.Background()
|
ctx := context.Background()
|
||||||
oldImageInfo := c.imageInfo
|
|
||||||
imageName := c.ImageName()
|
|
||||||
|
|
||||||
if client.pullImages {
|
if !client.pullImages {
|
||||||
log.Debugf("Pulling %s for %s", imageName, c.Name())
|
log.Debugf("Skipping image pull.")
|
||||||
|
} else if err := client.PullImage(ctx, container); err != nil {
|
||||||
var opts types.ImagePullOptions // ImagePullOptions can take a RegistryAuth arg to authenticate against a private registry
|
return false, err
|
||||||
auth, err := EncodedAuth(imageName)
|
|
||||||
log.Debugf("Got auth value: %s", auth)
|
|
||||||
log.Debugf("Got image name: %s", imageName)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("Error loading authentication credentials %s", err)
|
|
||||||
return false, err
|
|
||||||
} else if auth == "" {
|
|
||||||
log.Debugf("No authentication credentials found for %s", imageName)
|
|
||||||
opts = types.ImagePullOptions{} // empty/no auth credentials
|
|
||||||
} else {
|
|
||||||
opts = types.ImagePullOptions{RegistryAuth: auth, PrivilegeFunc: DefaultAuthHandler}
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := client.api.ImagePull(bg, imageName, opts)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("Error pulling image %s, %s", imageName, err)
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer response.Close()
|
|
||||||
|
|
||||||
// the pull request will be aborted prematurely unless the response is read
|
|
||||||
if _, err = ioutil.ReadAll(response); err != nil {
|
|
||||||
log.Error(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newImageInfo, _, err := client.api.ImageInspectWithRaw(bg, imageName)
|
return client.HasNewImage(ctx, container)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client dockerClient) HasNewImage(ctx context.Context, container Container) (bool, error) {
|
||||||
|
oldImageID := container.imageInfo.ID
|
||||||
|
imageName := container.ImageName()
|
||||||
|
|
||||||
|
newImageInfo, _, err := client.api.ImageInspectWithRaw(ctx, imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if newImageInfo.ID != oldImageInfo.ID {
|
if newImageInfo.ID == oldImageID {
|
||||||
log.Infof("Found new %s image (%s)", imageName, newImageInfo.ID)
|
log.Debugf("No new images found for %s", container.Name())
|
||||||
return true, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("No new images found for %s", c.Name())
|
log.Infof("Found new %s image (%s)", imageName, newImageInfo.ID)
|
||||||
return false, nil
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client dockerClient) PullImage(ctx context.Context, container Container) error {
|
||||||
|
containerName := container.Name()
|
||||||
|
imageName := container.ImageName()
|
||||||
|
log.Debugf("Pulling %s for %s", imageName, containerName)
|
||||||
|
|
||||||
|
opts, err := registry.GetPullOptions(imageName)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Error loading authentication credentials %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := client.api.ImagePull(ctx, imageName, opts)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Error pulling image %s, %s", imageName, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer response.Close()
|
||||||
|
// the pull request will be aborted prematurely unless the response is read
|
||||||
|
if _, err = ioutil.ReadAll(response); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client dockerClient) RemoveImageByID(id string) error {
|
func (client dockerClient) RemoveImageByID(id string) error {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@ type Container struct {
|
||||||
imageInfo *types.ImageInspect
|
imageInfo *types.ImageInspect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ContainerInfo fetches JSON info for the container
|
||||||
|
func (c Container) ContainerInfo() *types.ContainerJSON {
|
||||||
|
return c.containerInfo
|
||||||
|
}
|
||||||
|
|
||||||
// ID returns the Docker container ID.
|
// ID returns the Docker container ID.
|
||||||
func (c Container) ID() string {
|
func (c Container) ID() string {
|
||||||
return c.containerInfo.ID
|
return c.containerInfo.ID
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/containrrr/watchtower/pkg/container/mocks"
|
"github.com/containrrr/watchtower/pkg/container/mocks"
|
||||||
|
"github.com/containrrr/watchtower/pkg/filters"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
cli "github.com/docker/docker/client"
|
cli "github.com/docker/docker/client"
|
||||||
|
|
@ -34,14 +35,14 @@ var _ = Describe("the container", func() {
|
||||||
})
|
})
|
||||||
When("listing containers without any filter", func() {
|
When("listing containers without any filter", func() {
|
||||||
It("should return all available containers", func() {
|
It("should return all available containers", func() {
|
||||||
containers, err := client.ListContainers(noFilter)
|
containers, err := client.ListContainers(filters.NoFilter)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(len(containers) == 2).To(BeTrue())
|
Expect(len(containers) == 2).To(BeTrue())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
When("listing containers with a filter matching nothing", func() {
|
When("listing containers with a filter matching nothing", func() {
|
||||||
It("should return an empty array", func() {
|
It("should return an empty array", func() {
|
||||||
filter := filterByNames([]string{"lollercoaster"}, noFilter)
|
filter := filters.FilterByNames([]string{"lollercoaster"}, filters.NoFilter)
|
||||||
containers, err := client.ListContainers(filter)
|
containers, err := client.ListContainers(filter)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(len(containers) == 0).To(BeTrue())
|
Expect(len(containers) == 0).To(BeTrue())
|
||||||
|
|
@ -49,7 +50,7 @@ var _ = Describe("the container", func() {
|
||||||
})
|
})
|
||||||
When("listing containers with a watchtower filter", func() {
|
When("listing containers with a watchtower filter", func() {
|
||||||
It("should return only the watchtower container", func() {
|
It("should return only the watchtower container", func() {
|
||||||
containers, err := client.ListContainers(WatchtowerContainersFilter)
|
containers, err := client.ListContainers(filters.WatchtowerContainersFilter)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(len(containers) == 1).To(BeTrue())
|
Expect(len(containers) == 1).To(BeTrue())
|
||||||
Expect(containers[0].ImageName()).To(Equal("containrrr/watchtower:latest"))
|
Expect(containers[0].ImageName()).To(Equal("containrrr/watchtower:latest"))
|
||||||
|
|
@ -62,7 +63,7 @@ var _ = Describe("the container", func() {
|
||||||
pullImages: false,
|
pullImages: false,
|
||||||
includeStopped: true,
|
includeStopped: true,
|
||||||
}
|
}
|
||||||
containers, err := client.ListContainers(noFilter)
|
containers, err := client.ListContainers(filters.NoFilter)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(len(containers) > 0).To(BeTrue())
|
Expect(len(containers) > 0).To(BeTrue())
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
package container
|
package filters
|
||||||
|
|
||||||
import t "github.com/containrrr/watchtower/pkg/types"
|
import t "github.com/containrrr/watchtower/pkg/types"
|
||||||
|
|
||||||
// WatchtowerContainersFilter filters only watchtower containers
|
// WatchtowerContainersFilter filters only watchtower containers
|
||||||
func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatchtower() }
|
func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatchtower() }
|
||||||
|
|
||||||
// Filter no containers and returns all
|
// NoFilter will not filter out any containers
|
||||||
func noFilter(t.FilterableContainer) bool { return true }
|
func NoFilter(t.FilterableContainer) bool { return true }
|
||||||
|
|
||||||
// Filters containers which don't have a specified name
|
// FilterByNames returns all containers that match the specified name
|
||||||
func filterByNames(names []string, baseFilter t.Filter) t.Filter {
|
func FilterByNames(names []string, baseFilter t.Filter) t.Filter {
|
||||||
if len(names) == 0 {
|
if len(names) == 0 {
|
||||||
return baseFilter
|
return baseFilter
|
||||||
}
|
}
|
||||||
|
|
@ -24,8 +24,8 @@ func filterByNames(names []string, baseFilter t.Filter) t.Filter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters out containers that don't have the 'enableLabel'
|
// FilterByEnableLabel returns all containers that have the enabled label set
|
||||||
func filterByEnableLabel(baseFilter t.Filter) t.Filter {
|
func FilterByEnableLabel(baseFilter t.Filter) t.Filter {
|
||||||
return func(c t.FilterableContainer) bool {
|
return func(c t.FilterableContainer) bool {
|
||||||
// If label filtering is enabled, containers should only be considered
|
// If label filtering is enabled, containers should only be considered
|
||||||
// if the label is specifically set.
|
// if the label is specifically set.
|
||||||
|
|
@ -38,8 +38,8 @@ func filterByEnableLabel(baseFilter t.Filter) t.Filter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters out containers that have a 'enableLabel' and is set to disable.
|
// FilterByDisabledLabel returns all containers that have the enabled label set to disable
|
||||||
func filterByDisabledLabel(baseFilter t.Filter) t.Filter {
|
func FilterByDisabledLabel(baseFilter t.Filter) t.Filter {
|
||||||
return func(c t.FilterableContainer) bool {
|
return func(c t.FilterableContainer) bool {
|
||||||
enabledLabel, ok := c.Enabled()
|
enabledLabel, ok := c.Enabled()
|
||||||
if ok && !enabledLabel {
|
if ok && !enabledLabel {
|
||||||
|
|
@ -53,13 +53,13 @@ func filterByDisabledLabel(baseFilter t.Filter) t.Filter {
|
||||||
|
|
||||||
// BuildFilter creates the needed filter of containers
|
// BuildFilter creates the needed filter of containers
|
||||||
func BuildFilter(names []string, enableLabel bool) t.Filter {
|
func BuildFilter(names []string, enableLabel bool) t.Filter {
|
||||||
filter := noFilter
|
filter := NoFilter
|
||||||
filter = filterByNames(names, filter)
|
filter = FilterByNames(names, filter)
|
||||||
if enableLabel {
|
if enableLabel {
|
||||||
// If label filtering is enabled, containers should only be considered
|
// If label filtering is enabled, containers should only be considered
|
||||||
// if the label is specifically set.
|
// if the label is specifically set.
|
||||||
filter = filterByEnableLabel(filter)
|
filter = FilterByEnableLabel(filter)
|
||||||
}
|
}
|
||||||
filter = filterByDisabledLabel(filter)
|
filter = FilterByDisabledLabel(filter)
|
||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package container
|
package filters
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -20,7 +20,7 @@ func TestWatchtowerContainersFilter(t *testing.T) {
|
||||||
func TestNoFilter(t *testing.T) {
|
func TestNoFilter(t *testing.T) {
|
||||||
container := new(mocks.FilterableContainer)
|
container := new(mocks.FilterableContainer)
|
||||||
|
|
||||||
assert.True(t, noFilter(container))
|
assert.True(t, NoFilter(container))
|
||||||
|
|
||||||
container.AssertExpectations(t)
|
container.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
@ -28,12 +28,12 @@ func TestNoFilter(t *testing.T) {
|
||||||
func TestFilterByNames(t *testing.T) {
|
func TestFilterByNames(t *testing.T) {
|
||||||
var names []string
|
var names []string
|
||||||
|
|
||||||
filter := filterByNames(names, nil)
|
filter := FilterByNames(names, nil)
|
||||||
assert.Nil(t, filter)
|
assert.Nil(t, filter)
|
||||||
|
|
||||||
names = append(names, "test")
|
names = append(names, "test")
|
||||||
|
|
||||||
filter = filterByNames(names, noFilter)
|
filter = FilterByNames(names, NoFilter)
|
||||||
assert.NotNil(t, filter)
|
assert.NotNil(t, filter)
|
||||||
|
|
||||||
container := new(mocks.FilterableContainer)
|
container := new(mocks.FilterableContainer)
|
||||||
|
|
@ -48,7 +48,7 @@ func TestFilterByNames(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilterByEnableLabel(t *testing.T) {
|
func TestFilterByEnableLabel(t *testing.T) {
|
||||||
filter := filterByEnableLabel(noFilter)
|
filter := FilterByEnableLabel(NoFilter)
|
||||||
assert.NotNil(t, filter)
|
assert.NotNil(t, filter)
|
||||||
|
|
||||||
container := new(mocks.FilterableContainer)
|
container := new(mocks.FilterableContainer)
|
||||||
|
|
@ -68,7 +68,7 @@ func TestFilterByEnableLabel(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilterByDisabledLabel(t *testing.T) {
|
func TestFilterByDisabledLabel(t *testing.T) {
|
||||||
filter := filterByDisabledLabel(noFilter)
|
filter := FilterByDisabledLabel(NoFilter)
|
||||||
assert.NotNil(t, filter)
|
assert.NotNil(t, filter)
|
||||||
|
|
||||||
container := new(mocks.FilterableContainer)
|
container := new(mocks.FilterableContainer)
|
||||||
93
pkg/lifecycle/lifecycle.go
Normal file
93
pkg/lifecycle/lifecycle.go
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
package lifecycle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/containrrr/watchtower/pkg/container"
|
||||||
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExecutePreChecks tries to run the pre-check lifecycle hook for all containers included by the current filter.
|
||||||
|
func ExecutePreChecks(client container.Client, params types.UpdateParams) {
|
||||||
|
containers, err := client.ListContainers(params.Filter)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, container := range containers {
|
||||||
|
ExecutePreCheckCommand(client, container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutePostChecks tries to run the post-check lifecycle hook for all containers included by the current filter.
|
||||||
|
func ExecutePostChecks(client container.Client, params types.UpdateParams) {
|
||||||
|
containers, err := client.ListContainers(params.Filter)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, container := range containers {
|
||||||
|
ExecutePostCheckCommand(client, container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutePreCheckCommand tries to run the pre-check lifecycle hook for a single container.
|
||||||
|
func ExecutePreCheckCommand(client container.Client, container container.Container) {
|
||||||
|
command := container.GetLifecyclePreCheckCommand()
|
||||||
|
if len(command) == 0 {
|
||||||
|
log.Debug("No pre-check command supplied. Skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Executing pre-check command.")
|
||||||
|
if err := client.ExecuteCommand(container.ID(), command); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutePostCheckCommand tries to run the post-check lifecycle hook for a single container.
|
||||||
|
func ExecutePostCheckCommand(client container.Client, container container.Container) {
|
||||||
|
command := container.GetLifecyclePostCheckCommand()
|
||||||
|
if len(command) == 0 {
|
||||||
|
log.Debug("No post-check command supplied. Skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Executing post-check command.")
|
||||||
|
if err := client.ExecuteCommand(container.ID(), command); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutePreUpdateCommand tries to run the pre-update lifecycle hook for a single container.
|
||||||
|
func ExecutePreUpdateCommand(client container.Client, container container.Container) {
|
||||||
|
|
||||||
|
command := container.GetLifecyclePreUpdateCommand()
|
||||||
|
if len(command) == 0 {
|
||||||
|
log.Debug("No pre-update command supplied. Skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Executing pre-update command.")
|
||||||
|
if err := client.ExecuteCommand(container.ID(), command); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecutePostUpdateCommand tries to run the post-update lifecycle hook for a single container.
|
||||||
|
func ExecutePostUpdateCommand(client container.Client, newContainerID string) {
|
||||||
|
newContainer, err := client.GetContainer(newContainerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
command := newContainer.GetLifecyclePostUpdateCommand()
|
||||||
|
if len(command) == 0 {
|
||||||
|
log.Debug("No post-update command supplied. Skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Executing post-update command.")
|
||||||
|
if err := client.ExecuteCommand(newContainerID, command); err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
33
pkg/registry/registry.go
Normal file
33
pkg/registry/registry.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetPullOptions creates a struct with all options needed for pulling images from a registry
|
||||||
|
func GetPullOptions(imageName string) (types.ImagePullOptions, error) {
|
||||||
|
auth, err := EncodedAuth(imageName)
|
||||||
|
log.Debugf("Got image name: %s", imageName)
|
||||||
|
if err != nil {
|
||||||
|
return types.ImagePullOptions{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Got auth value: %s", auth)
|
||||||
|
if auth == "" {
|
||||||
|
return types.ImagePullOptions{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.ImagePullOptions{
|
||||||
|
RegistryAuth: auth,
|
||||||
|
PrivilegeFunc: DefaultAuthHandler,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultAuthHandler will be invoked if an AuthConfig is rejected
|
||||||
|
// 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
|
||||||
|
func DefaultAuthHandler() (string, error) {
|
||||||
|
log.Debug("Authentication request was rejected. Trying again without authentication")
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package container
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
@ -97,11 +97,3 @@ func CredentialsStore(configFile configfile.ConfigFile) credentials.Store {
|
||||||
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
|
|
||||||
// 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
|
|
||||||
func DefaultAuthHandler() (string, error) {
|
|
||||||
log.Debug("Authentication request was rejected. Trying again without authentication")
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package container
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
package container
|
package sorter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/containrrr/watchtower/pkg/container"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ByCreated allows a list of Container structs to be sorted by the container's
|
// ByCreated allows a list of Container structs to be sorted by the container's
|
||||||
// created date.
|
// created date.
|
||||||
type ByCreated []Container
|
type ByCreated []container.Container
|
||||||
|
|
||||||
func (c ByCreated) Len() int { return len(c) }
|
func (c ByCreated) Len() int { return len(c) }
|
||||||
func (c ByCreated) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
func (c ByCreated) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
||||||
|
|
@ -15,12 +16,12 @@ func (c ByCreated) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
|
||||||
// Less will compare two elements (identified by index) in the Container
|
// Less will compare two elements (identified by index) in the Container
|
||||||
// list by created-date.
|
// list by created-date.
|
||||||
func (c ByCreated) Less(i, j int) bool {
|
func (c ByCreated) Less(i, j int) bool {
|
||||||
t1, err := time.Parse(time.RFC3339Nano, c[i].containerInfo.Created)
|
t1, err := time.Parse(time.RFC3339Nano, c[i].ContainerInfo().Created)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t1 = time.Now()
|
t1 = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
t2, _ := time.Parse(time.RFC3339Nano, c[j].containerInfo.Created)
|
t2, _ := time.Parse(time.RFC3339Nano, c[j].ContainerInfo().Created)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t1 = time.Now()
|
t1 = time.Now()
|
||||||
}
|
}
|
||||||
|
|
@ -33,18 +34,18 @@ func (c ByCreated) Less(i, j int) bool {
|
||||||
// the front of the list while containers with links will be sorted after all
|
// the front of the list while containers with links will be sorted after all
|
||||||
// of their dependencies. This sort order ensures that linked containers can
|
// of their dependencies. This sort order ensures that linked containers can
|
||||||
// be started in the correct order.
|
// be started in the correct order.
|
||||||
func SortByDependencies(containers []Container) ([]Container, error) {
|
func SortByDependencies(containers []container.Container) ([]container.Container, error) {
|
||||||
sorter := dependencySorter{}
|
sorter := dependencySorter{}
|
||||||
return sorter.Sort(containers)
|
return sorter.Sort(containers)
|
||||||
}
|
}
|
||||||
|
|
||||||
type dependencySorter struct {
|
type dependencySorter struct {
|
||||||
unvisited []Container
|
unvisited []container.Container
|
||||||
marked map[string]bool
|
marked map[string]bool
|
||||||
sorted []Container
|
sorted []container.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *dependencySorter) Sort(containers []Container) ([]Container, error) {
|
func (ds *dependencySorter) Sort(containers []container.Container) ([]container.Container, error) {
|
||||||
ds.unvisited = containers
|
ds.unvisited = containers
|
||||||
ds.marked = map[string]bool{}
|
ds.marked = map[string]bool{}
|
||||||
|
|
||||||
|
|
@ -57,10 +58,10 @@ func (ds *dependencySorter) Sort(containers []Container) ([]Container, error) {
|
||||||
return ds.sorted, nil
|
return ds.sorted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *dependencySorter) visit(c Container) error {
|
func (ds *dependencySorter) visit(c container.Container) error {
|
||||||
|
|
||||||
if _, ok := ds.marked[c.Name()]; ok {
|
if _, ok := ds.marked[c.Name()]; ok {
|
||||||
return fmt.Errorf("Circular reference to %s", c.Name())
|
return fmt.Errorf("circular reference to %s", c.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark any visited node so that circular references can be detected
|
// Mark any visited node so that circular references can be detected
|
||||||
|
|
@ -83,7 +84,7 @@ func (ds *dependencySorter) visit(c Container) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *dependencySorter) findUnvisited(name string) *Container {
|
func (ds *dependencySorter) findUnvisited(name string) *container.Container {
|
||||||
for _, c := range ds.unvisited {
|
for _, c := range ds.unvisited {
|
||||||
if c.Name() == name {
|
if c.Name() == name {
|
||||||
return &c
|
return &c
|
||||||
|
|
@ -93,7 +94,7 @@ func (ds *dependencySorter) findUnvisited(name string) *Container {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *dependencySorter) removeUnvisited(c Container) {
|
func (ds *dependencySorter) removeUnvisited(c container.Container) {
|
||||||
var idx int
|
var idx int
|
||||||
for i := range ds.unvisited {
|
for i := range ds.unvisited {
|
||||||
if ds.unvisited[i].Name() == c.Name() {
|
if ds.unvisited[i].Name() == c.Name() {
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
package actions
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
t "github.com/containrrr/watchtower/pkg/types"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateParams contains all different options available to alter the behavior of the Update func
|
// UpdateParams contains all different options available to alter the behavior of the Update func
|
||||||
type UpdateParams struct {
|
type UpdateParams struct {
|
||||||
Filter t.Filter
|
Filter Filter
|
||||||
Cleanup bool
|
Cleanup bool
|
||||||
NoRestart bool
|
NoRestart bool
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
Loading…
Add table
Add a link
Reference in a new issue