fix: always use container interface (#1516)

pull/1466/head
nils måsén 1 year ago committed by GitHub
parent 25fdb40312
commit dd1ec09668
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,12 +1,13 @@
package actions_test package actions_test
import ( import (
"github.com/sirupsen/logrus"
"testing" "testing"
"time" "time"
"github.com/sirupsen/logrus"
"github.com/containrrr/watchtower/internal/actions" "github.com/containrrr/watchtower/internal/actions"
"github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/types"
. "github.com/containrrr/watchtower/internal/actions/mocks" . "github.com/containrrr/watchtower/internal/actions/mocks"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
@ -37,7 +38,7 @@ var _ = Describe("the actions package", func() {
It("should not do anything", func() { It("should not do anything", func() {
client := CreateMockClient( client := CreateMockClient(
&TestData{ &TestData{
Containers: []container.Container{ Containers: []types.Container{
CreateMockContainer( CreateMockContainer(
"test-container", "test-container",
"test-container", "test-container",
@ -59,7 +60,7 @@ var _ = Describe("the actions package", func() {
client = CreateMockClient( client = CreateMockClient(
&TestData{ &TestData{
NameOfContainerToKeep: "test-container-02", NameOfContainerToKeep: "test-container-02",
Containers: []container.Container{ Containers: []types.Container{
CreateMockContainer( CreateMockContainer(
"test-container-01", "test-container-01",
"test-container-01", "test-container-01",
@ -89,7 +90,7 @@ var _ = Describe("the actions package", func() {
BeforeEach(func() { BeforeEach(func() {
client = CreateMockClient( client = CreateMockClient(
&TestData{ &TestData{
Containers: []container.Container{ Containers: []types.Container{
CreateMockContainer( CreateMockContainer(
"test-container-01", "test-container-01",
"test-container-01", "test-container-01",

@ -2,16 +2,15 @@ package actions
import ( import (
"fmt" "fmt"
"github.com/containrrr/watchtower/pkg/types"
"sort" "sort"
"time" "time"
"github.com/containrrr/watchtower/pkg/container"
"github.com/containrrr/watchtower/pkg/filters" "github.com/containrrr/watchtower/pkg/filters"
"github.com/containrrr/watchtower/pkg/sorter" "github.com/containrrr/watchtower/pkg/sorter"
"github.com/containrrr/watchtower/pkg/types"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/containrrr/watchtower/pkg/container"
) )
// CheckForSanity makes sure everything is sane before starting // CheckForSanity makes sure everything is sane before starting
@ -55,7 +54,7 @@ func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool,
return cleanupExcessWatchtowers(containers, client, cleanup) return cleanupExcessWatchtowers(containers, client, cleanup)
} }
func cleanupExcessWatchtowers(containers []container.Container, client container.Client, cleanup bool) error { func cleanupExcessWatchtowers(containers []types.Container, client container.Client, cleanup bool) error {
var stopErrors int var stopErrors int
sort.Sort(sorter.ByCreated(containers)) sort.Sort(sorter.ByCreated(containers))

@ -5,8 +5,6 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/containrrr/watchtower/pkg/container"
t "github.com/containrrr/watchtower/pkg/types" t "github.com/containrrr/watchtower/pkg/types"
) )
@ -21,7 +19,7 @@ type MockClient struct {
type TestData struct { type TestData struct {
TriedToRemoveImageCount int TriedToRemoveImageCount int
NameOfContainerToKeep string NameOfContainerToKeep string
Containers []container.Container Containers []t.Container
Staleness map[string]bool Staleness map[string]bool
} }
@ -40,12 +38,12 @@ func CreateMockClient(data *TestData, pullImages bool, removeVolumes bool) MockC
} }
// ListContainers is a mock method returning the provided container testdata // ListContainers is a mock method returning the provided container testdata
func (client MockClient) ListContainers(_ t.Filter) ([]container.Container, error) { func (client MockClient) ListContainers(_ t.Filter) ([]t.Container, error) {
return client.TestData.Containers, nil return client.TestData.Containers, nil
} }
// StopContainer is a mock method // StopContainer is a mock method
func (client MockClient) StopContainer(c container.Container, _ time.Duration) error { func (client MockClient) StopContainer(c t.Container, _ time.Duration) error {
if c.Name() == client.TestData.NameOfContainerToKeep { if c.Name() == client.TestData.NameOfContainerToKeep {
return errors.New("tried to stop the instance we want to keep") return errors.New("tried to stop the instance we want to keep")
} }
@ -53,12 +51,12 @@ func (client MockClient) StopContainer(c container.Container, _ time.Duration) e
} }
// StartContainer is a mock method // StartContainer is a mock method
func (client MockClient) StartContainer(_ container.Container) (t.ContainerID, error) { func (client MockClient) StartContainer(_ t.Container) (t.ContainerID, error) {
return "", nil return "", nil
} }
// RenameContainer is a mock method // RenameContainer is a mock method
func (client MockClient) RenameContainer(_ container.Container, _ string) error { func (client MockClient) RenameContainer(_ t.Container, _ string) error {
return nil return nil
} }
@ -69,7 +67,7 @@ func (client MockClient) RemoveImageByID(_ t.ImageID) error {
} }
// GetContainer is a mock method // GetContainer is a mock method
func (client MockClient) GetContainer(_ t.ContainerID) (container.Container, error) { func (client MockClient) GetContainer(_ t.ContainerID) (t.Container, error) {
return client.TestData.Containers[0], nil return client.TestData.Containers[0], nil
} }
@ -88,7 +86,7 @@ func (client MockClient) ExecuteCommand(_ t.ContainerID, command string, _ int)
} }
// IsContainerStale is true if not explicitly stated in TestData for the mock client // IsContainerStale is true if not explicitly stated in TestData for the mock client
func (client MockClient) IsContainerStale(cont container.Container) (bool, t.ImageID, error) { func (client MockClient) IsContainerStale(cont t.Container) (bool, t.ImageID, error) {
stale, found := client.TestData.Staleness[cont.Name()] stale, found := client.TestData.Staleness[cont.Name()]
if !found { if !found {
stale = true stale = true
@ -97,6 +95,6 @@ func (client MockClient) IsContainerStale(cont container.Container) (bool, t.Ima
} }
// WarnOnHeadPullFailed is always true for the mock client // WarnOnHeadPullFailed is always true for the mock client
func (client MockClient) WarnOnHeadPullFailed(_ container.Container) bool { func (client MockClient) WarnOnHeadPullFailed(_ t.Container) bool {
return true return true
} }

@ -14,7 +14,7 @@ import (
) )
// CreateMockContainer creates a container substitute valid for testing // CreateMockContainer creates a container substitute valid for testing
func CreateMockContainer(id string, name string, image string, created time.Time) container.Container { func CreateMockContainer(id string, name string, image string, created time.Time) wt.Container {
content := types.ContainerJSON{ content := types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{ ContainerJSONBase: &types.ContainerJSONBase{
ID: id, ID: id,
@ -31,7 +31,7 @@ func CreateMockContainer(id string, name string, image string, created time.Time
ExposedPorts: map[nat.Port]struct{}{}, ExposedPorts: map[nat.Port]struct{}{},
}, },
} }
return *container.NewContainer( return container.NewContainer(
&content, &content,
CreateMockImageInfo(image), CreateMockImageInfo(image),
) )
@ -48,12 +48,12 @@ func CreateMockImageInfo(image string) *types.ImageInspect {
} }
// CreateMockContainerWithImageInfo should only be used for testing // CreateMockContainerWithImageInfo should only be used for testing
func CreateMockContainerWithImageInfo(id string, name string, image string, created time.Time, imageInfo types.ImageInspect) container.Container { func CreateMockContainerWithImageInfo(id string, name string, image string, created time.Time, imageInfo types.ImageInspect) wt.Container {
return CreateMockContainerWithImageInfoP(id, name, image, created, &imageInfo) return CreateMockContainerWithImageInfoP(id, name, image, created, &imageInfo)
} }
// CreateMockContainerWithImageInfoP should only be used for testing // CreateMockContainerWithImageInfoP should only be used for testing
func CreateMockContainerWithImageInfoP(id string, name string, image string, created time.Time, imageInfo *types.ImageInspect) container.Container { func CreateMockContainerWithImageInfoP(id string, name string, image string, created time.Time, imageInfo *types.ImageInspect) wt.Container {
content := types.ContainerJSON{ content := types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{ ContainerJSONBase: &types.ContainerJSONBase{
ID: id, ID: id,
@ -66,21 +66,21 @@ func CreateMockContainerWithImageInfoP(id string, name string, image string, cre
Labels: make(map[string]string), Labels: make(map[string]string),
}, },
} }
return *container.NewContainer( return container.NewContainer(
&content, &content,
imageInfo, imageInfo,
) )
} }
// CreateMockContainerWithDigest should only be used for testing // CreateMockContainerWithDigest should only be used for testing
func CreateMockContainerWithDigest(id string, name string, image string, created time.Time, digest string) container.Container { func CreateMockContainerWithDigest(id string, name string, image string, created time.Time, digest string) wt.Container {
c := CreateMockContainer(id, name, image, created) c := CreateMockContainer(id, name, image, created)
c.ImageInfo().RepoDigests = []string{digest} c.ImageInfo().RepoDigests = []string{digest}
return c return c
} }
// CreateMockContainerWithConfig creates a container substitute valid for testing // CreateMockContainerWithConfig creates a container substitute valid for testing
func CreateMockContainerWithConfig(id string, name string, image string, running bool, restarting bool, created time.Time, config *dockerContainer.Config) container.Container { func CreateMockContainerWithConfig(id string, name string, image string, running bool, restarting bool, created time.Time, config *dockerContainer.Config) wt.Container {
content := types.ContainerJSON{ content := types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{ ContainerJSONBase: &types.ContainerJSONBase{
ID: id, ID: id,
@ -97,14 +97,14 @@ func CreateMockContainerWithConfig(id string, name string, image string, running
}, },
Config: config, Config: config,
} }
return *container.NewContainer( return container.NewContainer(
&content, &content,
CreateMockImageInfo(image), CreateMockImageInfo(image),
) )
} }
// CreateContainerForProgress creates a container substitute for tracking session/update progress // CreateContainerForProgress creates a container substitute for tracking session/update progress
func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (container.Container, wt.ImageID) { func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (wt.Container, wt.ImageID) {
indexStr := strconv.Itoa(idPrefix + index) indexStr := strconv.Itoa(idPrefix + index)
mockID := indexStr + strings.Repeat("0", 61-len(indexStr)) mockID := indexStr + strings.Repeat("0", 61-len(indexStr))
contID := "c79" + mockID contID := "c79" + mockID
@ -120,7 +120,7 @@ func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (con
} }
// CreateMockContainerWithLinks should only be used for testing // CreateMockContainerWithLinks should only be used for testing
func CreateMockContainerWithLinks(id string, name string, image string, created time.Time, links []string, imageInfo *types.ImageInspect) container.Container { func CreateMockContainerWithLinks(id string, name string, image string, created time.Time, links []string, imageInfo *types.ImageInspect) wt.Container {
content := types.ContainerJSON{ content := types.ContainerJSON{
ContainerJSONBase: &types.ContainerJSONBase{ ContainerJSONBase: &types.ContainerJSONBase{
ID: id, ID: id,
@ -136,7 +136,7 @@ func CreateMockContainerWithLinks(id string, name string, image string, created
Labels: make(map[string]string), Labels: make(map[string]string),
}, },
} }
return *container.NewContainer( return container.NewContainer(
&content, &content,
imageInfo, imageInfo,
) )

@ -57,7 +57,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
} else { } else {
progress.AddScanned(targetContainer, newestImage) progress.AddScanned(targetContainer, newestImage)
} }
containers[i].Stale = stale containers[i].SetStale(stale)
if stale { if stale {
staleCount++ staleCount++
@ -71,7 +71,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
UpdateImplicitRestart(containers) UpdateImplicitRestart(containers)
var containersToUpdate []container.Container var containersToUpdate []types.Container
if !params.MonitorOnly { if !params.MonitorOnly {
for _, c := range containers { for _, c := range containers {
if !c.IsMonitorOnly() { if !c.IsMonitorOnly() {
@ -96,7 +96,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
return progress.Report(), nil return progress.Report(), nil
} }
func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error { func performRollingRestart(containers []types.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error {
cleanupImageIDs := make(map[types.ImageID]bool, len(containers)) cleanupImageIDs := make(map[types.ImageID]bool, len(containers))
failed := make(map[types.ContainerID]error, len(containers)) failed := make(map[types.ContainerID]error, len(containers))
@ -108,7 +108,7 @@ func performRollingRestart(containers []container.Container, client container.Cl
} else { } else {
if err := restartStaleContainer(containers[i], client, params); err != nil { if err := restartStaleContainer(containers[i], client, params); err != nil {
failed[containers[i].ID()] = err failed[containers[i].ID()] = err
} else if containers[i].Stale { } else if containers[i].IsStale() {
// Only add (previously) stale containers' images to cleanup // Only add (previously) stale containers' images to cleanup
cleanupImageIDs[containers[i].ImageID()] = true cleanupImageIDs[containers[i].ImageID()] = true
} }
@ -122,7 +122,7 @@ func performRollingRestart(containers []container.Container, client container.Cl
return failed return failed
} }
func stopContainersInReversedOrder(containers []container.Container, client container.Client, params types.UpdateParams) (failed map[types.ContainerID]error, stopped map[types.ImageID]bool) { func stopContainersInReversedOrder(containers []types.Container, client container.Client, params types.UpdateParams) (failed map[types.ContainerID]error, stopped map[types.ImageID]bool) {
failed = make(map[types.ContainerID]error, len(containers)) failed = make(map[types.ContainerID]error, len(containers))
stopped = make(map[types.ImageID]bool, len(containers)) stopped = make(map[types.ImageID]bool, len(containers))
for i := len(containers) - 1; i >= 0; i-- { for i := len(containers) - 1; i >= 0; i-- {
@ -137,7 +137,7 @@ func stopContainersInReversedOrder(containers []container.Container, client cont
return return
} }
func stopStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error { func stopStaleContainer(container types.Container, client container.Client, params types.UpdateParams) error {
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 nil return nil
@ -148,7 +148,7 @@ func stopStaleContainer(container container.Container, client container.Client,
} }
// Perform an additional check here to prevent us from stopping a linked container we cannot restart // Perform an additional check here to prevent us from stopping a linked container we cannot restart
if container.LinkedToRestarting { if container.IsLinkedToRestarting() {
if err := container.VerifyConfiguration(); err != nil { if err := container.VerifyConfiguration(); err != nil {
return err return err
} }
@ -174,7 +174,7 @@ func stopStaleContainer(container container.Container, client container.Client,
return nil return nil
} }
func restartContainersInSortedOrder(containers []container.Container, client container.Client, params types.UpdateParams, stoppedImages map[types.ImageID]bool) map[types.ContainerID]error { func restartContainersInSortedOrder(containers []types.Container, client container.Client, params types.UpdateParams, stoppedImages map[types.ImageID]bool) map[types.ContainerID]error {
cleanupImageIDs := make(map[types.ImageID]bool, len(containers)) cleanupImageIDs := make(map[types.ImageID]bool, len(containers))
failed := make(map[types.ContainerID]error, len(containers)) failed := make(map[types.ContainerID]error, len(containers))
@ -185,7 +185,7 @@ func restartContainersInSortedOrder(containers []container.Container, client con
if stoppedImages[c.SafeImageID()] { if stoppedImages[c.SafeImageID()] {
if err := restartStaleContainer(c, client, params); err != nil { if err := restartStaleContainer(c, client, params); err != nil {
failed[c.ID()] = err failed[c.ID()] = err
} else if c.Stale { } else if c.IsStale() {
// Only add (previously) stale containers' images to cleanup // Only add (previously) stale containers' images to cleanup
cleanupImageIDs[c.ImageID()] = true cleanupImageIDs[c.ImageID()] = true
} }
@ -210,7 +210,7 @@ func cleanupImages(client container.Client, imageIDs map[types.ImageID]bool) {
} }
} }
func restartStaleContainer(container container.Container, client container.Client, params types.UpdateParams) error { func restartStaleContainer(container types.Container, client container.Client, params types.UpdateParams) error {
// 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
@ -235,7 +235,7 @@ func restartStaleContainer(container container.Container, client container.Clien
// UpdateImplicitRestart iterates through the passed containers, setting the // UpdateImplicitRestart iterates through the passed containers, setting the
// `LinkedToRestarting` flag if any of it's linked containers are marked for restart // `LinkedToRestarting` flag if any of it's linked containers are marked for restart
func UpdateImplicitRestart(containers []container.Container) { func UpdateImplicitRestart(containers []types.Container) {
for ci, c := range containers { for ci, c := range containers {
if c.ToRestart() { if c.ToRestart() {
@ -249,7 +249,7 @@ func UpdateImplicitRestart(containers []container.Container) {
"linked": c.Name(), "linked": c.Name(),
}).Debug("container is linked to restarting") }).Debug("container is linked to restarting")
// NOTE: To mutate the array, the `c` variable cannot be used as it's a copy // NOTE: To mutate the array, the `c` variable cannot be used as it's a copy
containers[ci].LinkedToRestarting = true containers[ci].SetLinkedToRestarting(true)
} }
} }
@ -257,7 +257,7 @@ func UpdateImplicitRestart(containers []container.Container) {
// linkedContainerMarkedForRestart returns the name of the first link that matches a // linkedContainerMarkedForRestart returns the name of the first link that matches a
// container marked for restart // container marked for restart
func linkedContainerMarkedForRestart(links []string, containers []container.Container) string { func linkedContainerMarkedForRestart(links []string, containers []types.Container) string {
for _, linkName := range links { for _, linkName := range links {
for _, candidate := range containers { for _, candidate := range containers {
if candidate.Name() == linkName && candidate.ToRestart() { if candidate.Name() == linkName && candidate.ToRestart() {

@ -4,7 +4,6 @@ import (
"time" "time"
"github.com/containrrr/watchtower/internal/actions" "github.com/containrrr/watchtower/internal/actions"
"github.com/containrrr/watchtower/pkg/container"
"github.com/containrrr/watchtower/pkg/types" "github.com/containrrr/watchtower/pkg/types"
dockerTypes "github.com/docker/docker/api/types" dockerTypes "github.com/docker/docker/api/types"
dockerContainer "github.com/docker/docker/api/types/container" dockerContainer "github.com/docker/docker/api/types/container"
@ -18,7 +17,7 @@ import (
func getCommonTestData(keepContainer string) *TestData { func getCommonTestData(keepContainer string) *TestData {
return &TestData{ return &TestData{
NameOfContainerToKeep: keepContainer, NameOfContainerToKeep: keepContainer,
Containers: []container.Container{ Containers: []types.Container{
CreateMockContainer( CreateMockContainer(
"test-container-01", "test-container-01",
"test-container-01", "test-container-01",
@ -59,7 +58,7 @@ func getLinkedTestData(withImageInfo bool) *TestData {
return &TestData{ return &TestData{
Staleness: map[string]bool{linkingContainer.Name(): false}, Staleness: map[string]bool{linkingContainer.Name(): false},
Containers: []container.Container{ Containers: []types.Container{
staleContainer, staleContainer,
linkingContainer, linkingContainer,
}, },
@ -130,7 +129,7 @@ var _ = Describe("the update action", func() {
client := CreateMockClient( client := CreateMockClient(
&TestData{ &TestData{
NameOfContainerToKeep: "test-container-02", NameOfContainerToKeep: "test-container-02",
Containers: []container.Container{ Containers: []types.Container{
CreateMockContainer( CreateMockContainer(
"test-container-01", "test-container-01",
"test-container-01", "test-container-01",
@ -163,7 +162,7 @@ var _ = Describe("the update action", func() {
It("should not update any containers", func() { It("should not update any containers", func() {
client := CreateMockClient( client := CreateMockClient(
&TestData{ &TestData{
Containers: []container.Container{ Containers: []types.Container{
CreateMockContainer( CreateMockContainer(
"test-container-01", "test-container-01",
"test-container-01", "test-container-01",
@ -194,7 +193,7 @@ var _ = Describe("the update action", func() {
client := CreateMockClient( client := CreateMockClient(
&TestData{ &TestData{
//NameOfContainerToKeep: "test-container-02", //NameOfContainerToKeep: "test-container-02",
Containers: []container.Container{ Containers: []types.Container{
CreateMockContainerWithConfig( CreateMockContainerWithConfig(
"test-container-02", "test-container-02",
"test-container-02", "test-container-02",
@ -227,7 +226,7 @@ var _ = Describe("the update action", func() {
client := CreateMockClient( client := CreateMockClient(
&TestData{ &TestData{
//NameOfContainerToKeep: "test-container-02", //NameOfContainerToKeep: "test-container-02",
Containers: []container.Container{ Containers: []types.Container{
CreateMockContainerWithConfig( CreateMockContainerWithConfig(
"test-container-02", "test-container-02",
"test-container-02", "test-container-02",
@ -259,7 +258,7 @@ var _ = Describe("the update action", func() {
client := CreateMockClient( client := CreateMockClient(
&TestData{ &TestData{
//NameOfContainerToKeep: "test-container-02", //NameOfContainerToKeep: "test-container-02",
Containers: []container.Container{ Containers: []types.Container{
CreateMockContainerWithConfig( CreateMockContainerWithConfig(
"test-container-02", "test-container-02",
"test-container-02", "test-container-02",
@ -300,7 +299,7 @@ var _ = Describe("the update action", func() {
ExposedPorts: map[nat.Port]struct{}{}, ExposedPorts: map[nat.Port]struct{}{},
}) })
provider.Stale = true provider.SetStale(true)
consumer := CreateMockContainerWithConfig( consumer := CreateMockContainerWithConfig(
"test-container-consumer", "test-container-consumer",
@ -316,7 +315,7 @@ var _ = Describe("the update action", func() {
ExposedPorts: map[nat.Port]struct{}{}, ExposedPorts: map[nat.Port]struct{}{},
}) })
containers := []container.Container{ containers := []types.Container{
provider, provider,
consumer, consumer,
} }
@ -338,7 +337,7 @@ var _ = Describe("the update action", func() {
client := CreateMockClient( client := CreateMockClient(
&TestData{ &TestData{
//NameOfContainerToKeep: "test-container-02", //NameOfContainerToKeep: "test-container-02",
Containers: []container.Container{ Containers: []types.Container{
CreateMockContainerWithConfig( CreateMockContainerWithConfig(
"test-container-02", "test-container-02",
"test-container-02", "test-container-02",
@ -370,7 +369,7 @@ var _ = Describe("the update action", func() {
client := CreateMockClient( client := CreateMockClient(
&TestData{ &TestData{
//NameOfContainerToKeep: "test-container-02", //NameOfContainerToKeep: "test-container-02",
Containers: []container.Container{ Containers: []types.Container{
CreateMockContainerWithConfig( CreateMockContainerWithConfig(
"test-container-02", "test-container-02",
"test-container-02", "test-container-02",

@ -25,15 +25,15 @@ const defaultStopSignal = "SIGTERM"
// A Client is the interface through which watchtower interacts with the // A Client is the interface through which watchtower interacts with the
// Docker API. // Docker API.
type Client interface { type Client interface {
ListContainers(t.Filter) ([]Container, error) ListContainers(t.Filter) ([]t.Container, error)
GetContainer(containerID t.ContainerID) (Container, error) GetContainer(containerID t.ContainerID) (t.Container, error)
StopContainer(Container, time.Duration) error StopContainer(t.Container, time.Duration) error
StartContainer(Container) (t.ContainerID, error) StartContainer(t.Container) (t.ContainerID, error)
RenameContainer(Container, string) error RenameContainer(t.Container, string) error
IsContainerStale(Container) (stale bool, latestImage t.ImageID, err error) IsContainerStale(t.Container) (stale bool, latestImage t.ImageID, err error)
ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error) ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error)
RemoveImageByID(t.ImageID) error RemoveImageByID(t.ImageID) error
WarnOnHeadPullFailed(container Container) bool WarnOnHeadPullFailed(container t.Container) bool
} }
// NewClient returns a new Client instance which can be used to interact with // NewClient returns a new Client instance which can be used to interact with
@ -82,7 +82,7 @@ type dockerClient struct {
ClientOptions ClientOptions
} }
func (client dockerClient) WarnOnHeadPullFailed(container Container) bool { func (client dockerClient) WarnOnHeadPullFailed(container t.Container) bool {
if client.WarnOnHeadFailed == WarnAlways { if client.WarnOnHeadFailed == WarnAlways {
return true return true
} }
@ -93,8 +93,8 @@ func (client dockerClient) WarnOnHeadPullFailed(container Container) bool {
return registry.WarnOnAPIConsumption(container) return registry.WarnOnAPIConsumption(container)
} }
func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) { func (client dockerClient) ListContainers(fn t.Filter) ([]t.Container, error) {
cs := []Container{} cs := []t.Container{}
bg := context.Background() bg := context.Background()
if client.IncludeStopped && client.IncludeRestarting { if client.IncludeStopped && client.IncludeRestarting {
@ -149,24 +149,24 @@ func (client dockerClient) createListFilter() filters.Args {
return filterArgs return filterArgs
} }
func (client dockerClient) GetContainer(containerID t.ContainerID) (Container, error) { func (client dockerClient) GetContainer(containerID t.ContainerID) (t.Container, error) {
bg := context.Background() bg := context.Background()
containerInfo, err := client.api.ContainerInspect(bg, string(containerID)) containerInfo, err := client.api.ContainerInspect(bg, string(containerID))
if err != nil { if err != nil {
return Container{}, err return &Container{}, err
} }
imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image) imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image)
if err != nil { if err != nil {
log.Warnf("Failed to retrieve container image info: %v", err) log.Warnf("Failed to retrieve container image info: %v", err)
return Container{containerInfo: &containerInfo, imageInfo: nil}, nil return &Container{containerInfo: &containerInfo, imageInfo: nil}, nil
} }
return Container{containerInfo: &containerInfo, imageInfo: &imageInfo}, nil return &Container{containerInfo: &containerInfo, imageInfo: &imageInfo}, nil
} }
func (client dockerClient) StopContainer(c Container, timeout time.Duration) error { func (client dockerClient) StopContainer(c t.Container, timeout time.Duration) error {
bg := context.Background() bg := context.Background()
signal := c.StopSignal() signal := c.StopSignal()
if signal == "" { if signal == "" {
@ -186,7 +186,7 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
// TODO: This should probably be checked. // TODO: This should probably be checked.
_ = client.waitForStopOrTimeout(c, timeout) _ = client.waitForStopOrTimeout(c, timeout)
if c.containerInfo.HostConfig.AutoRemove { if c.ContainerInfo().HostConfig.AutoRemove {
log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", shortID) log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", shortID)
} else { } else {
log.Debugf("Removing container %s", shortID) log.Debugf("Removing container %s", shortID)
@ -208,11 +208,11 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
return nil return nil
} }
func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) { func (client dockerClient) StartContainer(c t.Container) (t.ContainerID, error) {
bg := context.Background() bg := context.Background()
config := c.runtimeConfig() config := c.GetCreateConfig()
hostConfig := c.hostConfig() hostConfig := c.GetCreateHostConfig()
networkConfig := &network.NetworkingConfig{EndpointsConfig: c.containerInfo.NetworkSettings.Networks} networkConfig := &network.NetworkingConfig{EndpointsConfig: c.ContainerInfo().NetworkSettings.Networks}
// simpleNetworkConfig is a networkConfig with only 1 network. // simpleNetworkConfig is a networkConfig with only 1 network.
// see: https://github.com/docker/docker/issues/29265 // see: https://github.com/docker/docker/issues/29265
simpleNetworkConfig := func() *network.NetworkingConfig { simpleNetworkConfig := func() *network.NetworkingConfig {
@ -260,7 +260,7 @@ func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) {
} }
func (client dockerClient) doStartContainer(bg context.Context, c Container, creation container.CreateResponse) error { func (client dockerClient) doStartContainer(bg context.Context, c t.Container, creation container.CreateResponse) error {
name := c.Name() name := c.Name()
log.Debugf("Starting container %s (%s)", name, t.ContainerID(creation.ID).ShortID()) log.Debugf("Starting container %s (%s)", name, t.ContainerID(creation.ID).ShortID())
@ -271,13 +271,13 @@ func (client dockerClient) doStartContainer(bg context.Context, c Container, cre
return nil return nil
} }
func (client dockerClient) RenameContainer(c Container, newName string) error { func (client dockerClient) RenameContainer(c t.Container, newName string) error {
bg := context.Background() bg := context.Background()
log.Debugf("Renaming container %s (%s) to %s", c.Name(), c.ID().ShortID(), newName) log.Debugf("Renaming container %s (%s) to %s", c.Name(), c.ID().ShortID(), newName)
return client.api.ContainerRename(bg, string(c.ID()), newName) return client.api.ContainerRename(bg, string(c.ID()), newName)
} }
func (client dockerClient) IsContainerStale(container Container) (stale bool, latestImage t.ImageID, err error) { func (client dockerClient) IsContainerStale(container t.Container) (stale bool, latestImage t.ImageID, err error) {
ctx := context.Background() ctx := context.Background()
if !client.PullImages || container.IsNoPull() { if !client.PullImages || container.IsNoPull() {
@ -289,8 +289,8 @@ func (client dockerClient) IsContainerStale(container Container) (stale bool, la
return client.HasNewImage(ctx, container) return client.HasNewImage(ctx, container)
} }
func (client dockerClient) HasNewImage(ctx context.Context, container Container) (hasNew bool, latestImage t.ImageID, err error) { func (client dockerClient) HasNewImage(ctx context.Context, container t.Container) (hasNew bool, latestImage t.ImageID, err error) {
currentImageID := t.ImageID(container.containerInfo.ContainerJSONBase.Image) currentImageID := t.ImageID(container.ContainerInfo().ContainerJSONBase.Image)
imageName := container.ImageName() imageName := container.ImageName()
newImageInfo, _, err := client.api.ImageInspectWithRaw(ctx, imageName) newImageInfo, _, err := client.api.ImageInspectWithRaw(ctx, imageName)
@ -310,7 +310,7 @@ func (client dockerClient) HasNewImage(ctx context.Context, container Container)
// PullImage pulls the latest image for the supplied container, optionally skipping if it's digest can be confirmed // PullImage pulls the latest image for the supplied container, optionally skipping if it's digest can be confirmed
// to match the one that the registry reports via a HEAD request // to match the one that the registry reports via a HEAD request
func (client dockerClient) PullImage(ctx context.Context, container Container) error { func (client dockerClient) PullImage(ctx context.Context, container t.Container) error {
containerName := container.Name() containerName := container.Name()
imageName := container.ImageName() imageName := container.ImageName()
@ -478,7 +478,7 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e
return false, nil return false, nil
} }
func (client dockerClient) waitForStopOrTimeout(c Container, waitTime time.Duration) error { func (client dockerClient) waitForStopOrTimeout(c t.Container, waitTime time.Duration) error {
bg := context.Background() bg := context.Background()
timeout := time.After(waitTime) timeout := time.After(waitTime)

@ -35,8 +35,8 @@ var _ = Describe("the client", func() {
mockServer.Close() mockServer.Close()
}) })
Describe("WarnOnHeadPullFailed", func() { Describe("WarnOnHeadPullFailed", func() {
containerUnknown := *MockContainer(WithImageName("unknown.repo/prefix/imagename:latest")) containerUnknown := MockContainer(WithImageName("unknown.repo/prefix/imagename:latest"))
containerKnown := *MockContainer(WithImageName("docker.io/prefix/imagename:latest")) containerKnown := MockContainer(WithImageName("docker.io/prefix/imagename:latest"))
When(`warn on head failure is set to "always"`, func() { When(`warn on head failure is set to "always"`, func() {
c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAlways}} c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAlways}}
@ -66,7 +66,7 @@ var _ = Describe("the client", func() {
When("the image consist of a pinned hash", func() { When("the image consist of a pinned hash", func() {
It("should gracefully fail with a useful message", func() { It("should gracefully fail with a useful message", func() {
c := dockerClient{} c := dockerClient{}
pinnedContainer := *MockContainer(WithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b")) pinnedContainer := MockContainer(WithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b"))
c.PullImage(context.Background(), pinnedContainer) c.PullImage(context.Background(), pinnedContainer)
}) })
}) })
@ -74,8 +74,8 @@ var _ = Describe("the client", func() {
When("removing a running container", func() { When("removing a running container", func() {
When("the container still exist after stopping", func() { When("the container still exist after stopping", func() {
It("should attempt to remove the container", func() { It("should attempt to remove the container", func() {
container := *MockContainer(WithContainerState(types.ContainerState{Running: true})) container := MockContainer(WithContainerState(types.ContainerState{Running: true}))
containerStopped := *MockContainer(WithContainerState(types.ContainerState{Running: false})) containerStopped := MockContainer(WithContainerState(types.ContainerState{Running: false}))
cid := container.ContainerInfo().ID cid := container.ContainerInfo().ID
mockServer.AppendHandlers( mockServer.AppendHandlers(
@ -90,7 +90,7 @@ var _ = Describe("the client", func() {
}) })
When("the container does not exist after stopping", func() { When("the container does not exist after stopping", func() {
It("should not cause an error", func() { It("should not cause an error", func() {
container := *MockContainer(WithContainerState(types.ContainerState{Running: true})) container := MockContainer(WithContainerState(types.ContainerState{Running: true}))
cid := container.ContainerInfo().ID cid := container.ContainerInfo().ID
mockServer.AppendHandlers( mockServer.AppendHandlers(
@ -261,18 +261,18 @@ func withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher {
return WithTransform(containerImageName, matcher) return WithTransform(containerImageName, matcher)
} }
func containerImageName(container Container) string { func containerImageName(container t.Container) string {
return container.ImageName() return container.ImageName()
} }
func havingRestartingState(expected bool) gt.GomegaMatcher { func havingRestartingState(expected bool) gt.GomegaMatcher {
return WithTransform(func(container Container) bool { return WithTransform(func(container t.Container) bool {
return container.containerInfo.State.Restarting return container.ContainerInfo().State.Restarting
}, Equal(expected)) }, Equal(expected))
} }
func havingRunningState(expected bool) gt.GomegaMatcher { func havingRunningState(expected bool) gt.GomegaMatcher {
return WithTransform(func(container Container) bool { return WithTransform(func(container t.Container) bool {
return container.containerInfo.State.Running return container.ContainerInfo().State.Running
}, Equal(expected)) }, Equal(expected))
} }

@ -32,6 +32,26 @@ type Container struct {
imageInfo *types.ImageInspect imageInfo *types.ImageInspect
} }
// IsLinkedToRestarting returns the current value of the LinkedToRestarting field for the container
func (c *Container) IsLinkedToRestarting() bool {
return c.LinkedToRestarting
}
// IsStale returns the current value of the Stale field for the container
func (c *Container) IsStale() bool {
return c.Stale
}
// SetLinkedToRestarting sets the LinkedToRestarting field for the container
func (c *Container) SetLinkedToRestarting(value bool) {
c.LinkedToRestarting = value
}
// SetStale implements sets the Stale field for the container
func (c *Container) SetStale(value bool) {
c.Stale = value
}
// ContainerInfo fetches JSON info for the container // ContainerInfo fetches JSON info for the container
func (c Container) ContainerInfo() *types.ContainerJSON { func (c Container) ContainerInfo() *types.ContainerJSON {
return c.containerInfo return c.containerInfo
@ -240,18 +260,23 @@ func (c Container) StopSignal() string {
return c.getLabelValueOrEmpty(signalLabel) return c.getLabelValueOrEmpty(signalLabel)
} }
// GetCreateConfig returns the container's current Config converted into a format
// that can be re-submitted to the Docker create API.
//
// Ideally, we'd just be able to take the ContainerConfig from the old container // Ideally, we'd just be able to take the ContainerConfig from the old container
// and use it as the starting point for creating the new container; however, // and use it as the starting point for creating the new container; however,
// the ContainerConfig that comes back from the Inspect call merges the default // the ContainerConfig that comes back from the Inspect call merges the default
// configuration (the stuff specified in the metadata for the image itself) // configuration (the stuff specified in the metadata for the image itself)
// with the overridden configuration (the stuff that you might specify as part // with the overridden configuration (the stuff that you might specify as part
// of the "docker run"). In order to avoid unintentionally overriding the // of the "docker run").
//
// In order to avoid unintentionally overriding the
// defaults in the new image we need to separate the override options from the // defaults in the new image we need to separate the override options from the
// default options. To do this we have to compare the ContainerConfig for the // default options. To do this we have to compare the ContainerConfig for the
// running container with the ContainerConfig from the image that container was // running container with the ContainerConfig from the image that container was
// started from. This function returns a ContainerConfig which contains just // started from. This function returns a ContainerConfig which contains just
// the options overridden at runtime. // the options overridden at runtime.
func (c Container) runtimeConfig() *dockercontainer.Config { func (c Container) GetCreateConfig() *dockercontainer.Config {
config := c.containerInfo.Config config := c.containerInfo.Config
hostConfig := c.containerInfo.HostConfig hostConfig := c.containerInfo.HostConfig
imageConfig := c.imageInfo.Config imageConfig := c.imageInfo.Config
@ -295,9 +320,9 @@ func (c Container) runtimeConfig() *dockercontainer.Config {
return config return config
} }
// Any links in the HostConfig need to be re-written before they can be // GetCreateHostConfig returns the container's current HostConfig with any links
// re-submitted to the Docker create API. // re-written so that they can be re-submitted to the Docker create API.
func (c Container) hostConfig() *dockercontainer.HostConfig { func (c Container) GetCreateHostConfig() *dockercontainer.HostConfig {
hostConfig := c.containerInfo.HostConfig hostConfig := c.containerInfo.HostConfig
for i, link := range hostConfig.Links { for i, link := range hostConfig.Links {

@ -29,7 +29,7 @@ func ExecutePostChecks(client container.Client, params types.UpdateParams) {
} }
// ExecutePreCheckCommand tries to run the pre-check lifecycle hook for a single container. // ExecutePreCheckCommand tries to run the pre-check lifecycle hook for a single container.
func ExecutePreCheckCommand(client container.Client, container container.Container) { func ExecutePreCheckCommand(client container.Client, container types.Container) {
clog := log.WithField("container", container.Name()) clog := log.WithField("container", container.Name())
command := container.GetLifecyclePreCheckCommand() command := container.GetLifecyclePreCheckCommand()
if len(command) == 0 { if len(command) == 0 {
@ -45,7 +45,7 @@ func ExecutePreCheckCommand(client container.Client, container container.Contain
} }
// ExecutePostCheckCommand tries to run the post-check lifecycle hook for a single container. // ExecutePostCheckCommand tries to run the post-check lifecycle hook for a single container.
func ExecutePostCheckCommand(client container.Client, container container.Container) { func ExecutePostCheckCommand(client container.Client, container types.Container) {
clog := log.WithField("container", container.Name()) clog := log.WithField("container", container.Name())
command := container.GetLifecyclePostCheckCommand() command := container.GetLifecyclePostCheckCommand()
if len(command) == 0 { if len(command) == 0 {
@ -61,7 +61,7 @@ func ExecutePostCheckCommand(client container.Client, container container.Contai
} }
// ExecutePreUpdateCommand tries to run the pre-update lifecycle hook for a single container. // ExecutePreUpdateCommand tries to run the pre-update lifecycle hook for a single container.
func ExecutePreUpdateCommand(client container.Client, container container.Container) (SkipUpdate bool, err error) { func ExecutePreUpdateCommand(client container.Client, container types.Container) (SkipUpdate bool, err error) {
timeout := container.PreUpdateTimeout() timeout := container.PreUpdateTimeout()
command := container.GetLifecyclePreUpdateCommand() command := container.GetLifecyclePreUpdateCommand()
clog := log.WithField("container", container.Name()) clog := log.WithField("container", container.Name())

@ -2,13 +2,14 @@ package sorter
import ( import (
"fmt" "fmt"
"github.com/containrrr/watchtower/pkg/container"
"time" "time"
"github.com/containrrr/watchtower/pkg/types"
) )
// 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.Container type ByCreated []types.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] }
@ -34,18 +35,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) ([]container.Container, error) { func SortByDependencies(containers []types.Container) ([]types.Container, error) {
sorter := dependencySorter{} sorter := dependencySorter{}
return sorter.Sort(containers) return sorter.Sort(containers)
} }
type dependencySorter struct { type dependencySorter struct {
unvisited []container.Container unvisited []types.Container
marked map[string]bool marked map[string]bool
sorted []container.Container sorted []types.Container
} }
func (ds *dependencySorter) Sort(containers []container.Container) ([]container.Container, error) { func (ds *dependencySorter) Sort(containers []types.Container) ([]types.Container, error) {
ds.unvisited = containers ds.unvisited = containers
ds.marked = map[string]bool{} ds.marked = map[string]bool{}
@ -58,7 +59,7 @@ func (ds *dependencySorter) Sort(containers []container.Container) ([]container.
return ds.sorted, nil return ds.sorted, nil
} }
func (ds *dependencySorter) visit(c container.Container) error { func (ds *dependencySorter) visit(c types.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())
@ -84,7 +85,7 @@ func (ds *dependencySorter) visit(c container.Container) error {
return nil return nil
} }
func (ds *dependencySorter) findUnvisited(name string) *container.Container { func (ds *dependencySorter) findUnvisited(name string) *types.Container {
for _, c := range ds.unvisited { for _, c := range ds.unvisited {
if c.Name() == name { if c.Name() == name {
return &c return &c
@ -94,7 +95,7 @@ func (ds *dependencySorter) findUnvisited(name string) *container.Container {
return nil return nil
} }
func (ds *dependencySorter) removeUnvisited(c container.Container) { func (ds *dependencySorter) removeUnvisited(c types.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,8 +1,10 @@
package types package types
import ( import (
"github.com/docker/docker/api/types"
"strings" "strings"
"github.com/docker/docker/api/types"
dc "github.com/docker/docker/api/types/container"
) )
// ImageID is a hash string representing a container image // ImageID is a hash string representing a container image
@ -62,4 +64,15 @@ type Container interface {
GetLifecyclePostCheckCommand() string GetLifecyclePostCheckCommand() string
GetLifecyclePreUpdateCommand() string GetLifecyclePreUpdateCommand() string
GetLifecyclePostUpdateCommand() string GetLifecyclePostUpdateCommand() string
VerifyConfiguration() error
SetStale(bool)
IsStale() bool
IsNoPull() bool
SetLinkedToRestarting(bool)
IsLinkedToRestarting() bool
PreUpdateTimeout() int
PostUpdateTimeout() int
IsRestarting() bool
GetCreateConfig() *dc.Config
GetCreateHostConfig() *dc.HostConfig
} }

Loading…
Cancel
Save