refactor: extract code from the container package

This commit is contained in:
Simon Aronsson 2020-01-11 23:35:25 +01:00
parent 4130b110c6
commit d1abce889a
15 changed files with 253 additions and 185 deletions

View file

@ -3,6 +3,7 @@ package container
import (
"bytes"
"fmt"
"github.com/containrrr/watchtower/pkg/registry"
"io/ioutil"
"strings"
"time"
@ -12,7 +13,7 @@ import (
"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"
sdkClient "github.com/docker/docker/client"
log "github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
@ -40,7 +41,7 @@ type Client interface {
// * 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, reviveStopped bool, removeVolumes bool) Client {
cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv)
cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv)
if err != nil {
log.Fatalf("Error instantiating Docker client: %s", err)
@ -56,7 +57,7 @@ func NewClient(pullImages bool, includeStopped bool, reviveStopped bool, removeV
}
type dockerClient struct {
api dockerclient.CommonAPIClient
api sdkClient.CommonAPIClient
pullImages bool
removeVolumes bool
includeStopped bool
@ -231,53 +232,60 @@ func (client dockerClient) RenameContainer(c Container, newName string) error {
return client.api.ContainerRename(bg, c.ID(), newName)
}
func (client dockerClient) IsContainerStale(c Container) (bool, error) {
bg := context.Background()
oldImageInfo := c.imageInfo
imageName := c.ImageName()
func (client dockerClient) IsContainerStale(container Container) (bool, error) {
ctx := context.Background()
if client.pullImages {
log.Debugf("Pulling %s for %s", imageName, c.Name())
var opts types.ImagePullOptions // ImagePullOptions can take a RegistryAuth arg to authenticate against a private registry
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)
}
if !client.pullImages {
log.Debugf("Skipping image pull.")
} else if err := client.PullImage(ctx, container); err != nil {
return false, 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 {
return false, err
}
if newImageInfo.ID != oldImageInfo.ID {
log.Infof("Found new %s image (%s)", imageName, newImageInfo.ID)
return true, nil
if newImageInfo.ID == oldImageID {
log.Debugf("No new images found for %s", container.Name())
return false, nil
}
log.Debugf("No new images found for %s", c.Name())
return false, nil
log.Infof("Found new %s image (%s)", imageName, newImageInfo.ID)
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 {

View file

@ -28,6 +28,11 @@ type Container struct {
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.
func (c Container) ID() string {
return c.containerInfo.ID

View file

@ -2,6 +2,7 @@ package container
import (
"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/container"
cli "github.com/docker/docker/client"
@ -34,14 +35,14 @@ var _ = Describe("the container", func() {
})
When("listing containers without any filter", func() {
It("should return all available containers", func() {
containers, err := client.ListContainers(noFilter)
containers, err := client.ListContainers(filters.NoFilter)
Expect(err).NotTo(HaveOccurred())
Expect(len(containers) == 2).To(BeTrue())
})
})
When("listing containers with a filter matching nothing", 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)
Expect(err).NotTo(HaveOccurred())
Expect(len(containers) == 0).To(BeTrue())
@ -49,7 +50,7 @@ var _ = Describe("the container", func() {
})
When("listing containers with a watchtower filter", 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(len(containers) == 1).To(BeTrue())
Expect(containers[0].ImageName()).To(Equal("containrrr/watchtower:latest"))
@ -62,7 +63,7 @@ var _ = Describe("the container", func() {
pullImages: false,
includeStopped: true,
}
containers, err := client.ListContainers(noFilter)
containers, err := client.ListContainers(filters.NoFilter)
Expect(err).NotTo(HaveOccurred())
Expect(len(containers) > 0).To(BeTrue())
})

View file

@ -1,15 +1,15 @@
package container
package filters
import t "github.com/containrrr/watchtower/pkg/types"
// WatchtowerContainersFilter filters only watchtower containers
func WatchtowerContainersFilter(c t.FilterableContainer) bool { return c.IsWatchtower() }
// Filter no containers and returns all
func noFilter(t.FilterableContainer) bool { return true }
// NoFilter will not filter out any containers
func NoFilter(t.FilterableContainer) bool { return true }
// Filters containers which don't have a specified name
func filterByNames(names []string, baseFilter t.Filter) t.Filter {
// FilterByNames returns all containers that match the specified name
func FilterByNames(names []string, baseFilter t.Filter) t.Filter {
if len(names) == 0 {
return baseFilter
}
@ -24,8 +24,8 @@ func filterByNames(names []string, baseFilter t.Filter) t.Filter {
}
}
// Filters out containers that don't have the 'enableLabel'
func filterByEnableLabel(baseFilter t.Filter) t.Filter {
// FilterByEnableLabel returns all containers that have the enabled label set
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.
@ -38,8 +38,8 @@ func filterByEnableLabel(baseFilter t.Filter) t.Filter {
}
}
// Filters out containers that have a 'enableLabel' and is set to disable.
func filterByDisabledLabel(baseFilter t.Filter) t.Filter {
// FilterByDisabledLabel returns all containers that have the enabled label set to disable
func FilterByDisabledLabel(baseFilter t.Filter) t.Filter {
return func(c t.FilterableContainer) bool {
enabledLabel, ok := c.Enabled()
if ok && !enabledLabel {
@ -53,13 +53,13 @@ func filterByDisabledLabel(baseFilter t.Filter) t.Filter {
// BuildFilter creates the needed filter of containers
func BuildFilter(names []string, enableLabel bool) t.Filter {
filter := noFilter
filter = filterByNames(names, filter)
filter := NoFilter
filter = FilterByNames(names, filter)
if enableLabel {
// If label filtering is enabled, containers should only be considered
// if the label is specifically set.
filter = filterByEnableLabel(filter)
filter = FilterByEnableLabel(filter)
}
filter = filterByDisabledLabel(filter)
filter = FilterByDisabledLabel(filter)
return filter
}

View file

@ -1,4 +1,4 @@
package container
package filters
import (
"testing"
@ -20,7 +20,7 @@ func TestWatchtowerContainersFilter(t *testing.T) {
func TestNoFilter(t *testing.T) {
container := new(mocks.FilterableContainer)
assert.True(t, noFilter(container))
assert.True(t, NoFilter(container))
container.AssertExpectations(t)
}
@ -28,12 +28,12 @@ func TestNoFilter(t *testing.T) {
func TestFilterByNames(t *testing.T) {
var names []string
filter := filterByNames(names, nil)
filter := FilterByNames(names, nil)
assert.Nil(t, filter)
names = append(names, "test")
filter = filterByNames(names, noFilter)
filter = FilterByNames(names, NoFilter)
assert.NotNil(t, filter)
container := new(mocks.FilterableContainer)
@ -48,7 +48,7 @@ func TestFilterByNames(t *testing.T) {
}
func TestFilterByEnableLabel(t *testing.T) {
filter := filterByEnableLabel(noFilter)
filter := FilterByEnableLabel(NoFilter)
assert.NotNil(t, filter)
container := new(mocks.FilterableContainer)
@ -68,7 +68,7 @@ func TestFilterByEnableLabel(t *testing.T) {
}
func TestFilterByDisabledLabel(t *testing.T) {
filter := filterByDisabledLabel(noFilter)
filter := FilterByDisabledLabel(NoFilter)
assert.NotNil(t, filter)
container := new(mocks.FilterableContainer)

View 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
View 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
}

View file

@ -1,4 +1,4 @@
package container
package registry
import (
"errors"
@ -97,11 +97,3 @@ func CredentialsStore(configFile configfile.ConfigFile) credentials.Store {
func EncodeAuth(auth types.AuthConfig) (string, error) {
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
}

View file

@ -1,4 +1,4 @@
package container
package registry
import (
"github.com/stretchr/testify/assert"

View file

@ -1,13 +1,14 @@
package container
package sorter
import (
"fmt"
"github.com/containrrr/watchtower/pkg/container"
"time"
)
// ByCreated allows a list of Container structs to be sorted by the container's
// created date.
type ByCreated []Container
type ByCreated []container.Container
func (c ByCreated) Len() int { return len(c) }
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
// list by created-date.
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 {
t1 = time.Now()
}
t2, _ := time.Parse(time.RFC3339Nano, c[j].containerInfo.Created)
t2, _ := time.Parse(time.RFC3339Nano, c[j].ContainerInfo().Created)
if err != nil {
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
// of their dependencies. This sort order ensures that linked containers can
// be started in the correct order.
func SortByDependencies(containers []Container) ([]Container, error) {
func SortByDependencies(containers []container.Container) ([]container.Container, error) {
sorter := dependencySorter{}
return sorter.Sort(containers)
}
type dependencySorter struct {
unvisited []Container
unvisited []container.Container
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.marked = map[string]bool{}
@ -57,10 +58,10 @@ func (ds *dependencySorter) Sort(containers []Container) ([]Container, error) {
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 {
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
@ -83,7 +84,7 @@ func (ds *dependencySorter) visit(c Container) error {
return nil
}
func (ds *dependencySorter) findUnvisited(name string) *Container {
func (ds *dependencySorter) findUnvisited(name string) *container.Container {
for _, c := range ds.unvisited {
if c.Name() == name {
return &c
@ -93,7 +94,7 @@ func (ds *dependencySorter) findUnvisited(name string) *Container {
return nil
}
func (ds *dependencySorter) removeUnvisited(c Container) {
func (ds *dependencySorter) removeUnvisited(c container.Container) {
var idx int
for i := range ds.unvisited {
if ds.unvisited[i].Name() == c.Name() {

View file

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