From dd1ec09668b44f90e9fd368f47bfc67e82bdeef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Wed, 12 Apr 2023 17:36:01 +0200 Subject: [PATCH] fix: always use container interface (#1516) --- internal/actions/actions_suite_test.go | 11 +++--- internal/actions/check.go | 7 ++-- internal/actions/mocks/client.go | 18 ++++----- internal/actions/mocks/container.go | 22 +++++------ internal/actions/update.go | 26 ++++++------- internal/actions/update_test.go | 23 ++++++----- pkg/container/client.go | 54 +++++++++++++------------- pkg/container/client_test.go | 22 +++++------ pkg/container/container.go | 35 ++++++++++++++--- pkg/lifecycle/lifecycle.go | 6 +-- pkg/sorter/sort.go | 19 ++++----- pkg/types/container.go | 15 ++++++- 12 files changed, 147 insertions(+), 111 deletions(-) diff --git a/internal/actions/actions_suite_test.go b/internal/actions/actions_suite_test.go index 5110fea..c320564 100644 --- a/internal/actions/actions_suite_test.go +++ b/internal/actions/actions_suite_test.go @@ -1,12 +1,13 @@ package actions_test import ( - "github.com/sirupsen/logrus" "testing" "time" + "github.com/sirupsen/logrus" + "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/onsi/ginkgo" @@ -37,7 +38,7 @@ var _ = Describe("the actions package", func() { It("should not do anything", func() { client := CreateMockClient( &TestData{ - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container", "test-container", @@ -59,7 +60,7 @@ var _ = Describe("the actions package", func() { client = CreateMockClient( &TestData{ NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container-01", "test-container-01", @@ -89,7 +90,7 @@ var _ = Describe("the actions package", func() { BeforeEach(func() { client = CreateMockClient( &TestData{ - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container-01", "test-container-01", diff --git a/internal/actions/check.go b/internal/actions/check.go index 436931f..3bfcf7d 100644 --- a/internal/actions/check.go +++ b/internal/actions/check.go @@ -2,16 +2,15 @@ package actions import ( "fmt" - "github.com/containrrr/watchtower/pkg/types" "sort" "time" + "github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/filters" "github.com/containrrr/watchtower/pkg/sorter" + "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" - - "github.com/containrrr/watchtower/pkg/container" ) // CheckForSanity makes sure everything is sane before starting @@ -55,7 +54,7 @@ func CheckForMultipleWatchtowerInstances(client container.Client, cleanup bool, 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 sort.Sort(sorter.ByCreated(containers)) diff --git a/internal/actions/mocks/client.go b/internal/actions/mocks/client.go index 2afc43c..7b4162a 100644 --- a/internal/actions/mocks/client.go +++ b/internal/actions/mocks/client.go @@ -5,8 +5,6 @@ import ( "fmt" "time" - "github.com/containrrr/watchtower/pkg/container" - t "github.com/containrrr/watchtower/pkg/types" ) @@ -21,7 +19,7 @@ type MockClient struct { type TestData struct { TriedToRemoveImageCount int NameOfContainerToKeep string - Containers []container.Container + Containers []t.Container 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 -func (client MockClient) ListContainers(_ t.Filter) ([]container.Container, error) { +func (client MockClient) ListContainers(_ t.Filter) ([]t.Container, error) { return client.TestData.Containers, nil } // 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 { 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 -func (client MockClient) StartContainer(_ container.Container) (t.ContainerID, error) { +func (client MockClient) StartContainer(_ t.Container) (t.ContainerID, error) { return "", nil } // RenameContainer is a mock method -func (client MockClient) RenameContainer(_ container.Container, _ string) error { +func (client MockClient) RenameContainer(_ t.Container, _ string) error { return nil } @@ -69,7 +67,7 @@ func (client MockClient) RemoveImageByID(_ t.ImageID) error { } // 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 } @@ -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 -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()] if !found { stale = true @@ -97,6 +95,6 @@ func (client MockClient) IsContainerStale(cont container.Container) (bool, t.Ima } // WarnOnHeadPullFailed is always true for the mock client -func (client MockClient) WarnOnHeadPullFailed(_ container.Container) bool { +func (client MockClient) WarnOnHeadPullFailed(_ t.Container) bool { return true } diff --git a/internal/actions/mocks/container.go b/internal/actions/mocks/container.go index 3272d63..e830587 100644 --- a/internal/actions/mocks/container.go +++ b/internal/actions/mocks/container.go @@ -14,7 +14,7 @@ import ( ) // 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{ ContainerJSONBase: &types.ContainerJSONBase{ ID: id, @@ -31,7 +31,7 @@ func CreateMockContainer(id string, name string, image string, created time.Time ExposedPorts: map[nat.Port]struct{}{}, }, } - return *container.NewContainer( + return container.NewContainer( &content, CreateMockImageInfo(image), ) @@ -48,12 +48,12 @@ func CreateMockImageInfo(image string) *types.ImageInspect { } // 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) } // 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{ ContainerJSONBase: &types.ContainerJSONBase{ ID: id, @@ -66,21 +66,21 @@ func CreateMockContainerWithImageInfoP(id string, name string, image string, cre Labels: make(map[string]string), }, } - return *container.NewContainer( + return container.NewContainer( &content, imageInfo, ) } // 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.ImageInfo().RepoDigests = []string{digest} return c } // 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{ ContainerJSONBase: &types.ContainerJSONBase{ ID: id, @@ -97,14 +97,14 @@ func CreateMockContainerWithConfig(id string, name string, image string, running }, Config: config, } - return *container.NewContainer( + return container.NewContainer( &content, CreateMockImageInfo(image), ) } // 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) mockID := indexStr + strings.Repeat("0", 61-len(indexStr)) contID := "c79" + mockID @@ -120,7 +120,7 @@ func CreateContainerForProgress(index int, idPrefix int, nameFormat string) (con } // 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{ ContainerJSONBase: &types.ContainerJSONBase{ ID: id, @@ -136,7 +136,7 @@ func CreateMockContainerWithLinks(id string, name string, image string, created Labels: make(map[string]string), }, } - return *container.NewContainer( + return container.NewContainer( &content, imageInfo, ) diff --git a/internal/actions/update.go b/internal/actions/update.go index baae47f..9c97f27 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -57,7 +57,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e } else { progress.AddScanned(targetContainer, newestImage) } - containers[i].Stale = stale + containers[i].SetStale(stale) if stale { staleCount++ @@ -71,7 +71,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e UpdateImplicitRestart(containers) - var containersToUpdate []container.Container + var containersToUpdate []types.Container if !params.MonitorOnly { for _, c := range containers { if !c.IsMonitorOnly() { @@ -96,7 +96,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e 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)) failed := make(map[types.ContainerID]error, len(containers)) @@ -108,7 +108,7 @@ func performRollingRestart(containers []container.Container, client container.Cl } else { if err := restartStaleContainer(containers[i], client, params); err != nil { failed[containers[i].ID()] = err - } else if containers[i].Stale { + } else if containers[i].IsStale() { // Only add (previously) stale containers' images to cleanup cleanupImageIDs[containers[i].ImageID()] = true } @@ -122,7 +122,7 @@ func performRollingRestart(containers []container.Container, client container.Cl 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)) stopped = make(map[types.ImageID]bool, len(containers)) for i := len(containers) - 1; i >= 0; i-- { @@ -137,7 +137,7 @@ func stopContainersInReversedOrder(containers []container.Container, client cont 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() { log.Debugf("This is the watchtower container %s", container.Name()) 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 - if container.LinkedToRestarting { + if container.IsLinkedToRestarting() { if err := container.VerifyConfiguration(); err != nil { return err } @@ -174,7 +174,7 @@ func stopStaleContainer(container container.Container, client container.Client, 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)) failed := make(map[types.ContainerID]error, len(containers)) @@ -185,7 +185,7 @@ func restartContainersInSortedOrder(containers []container.Container, client con if stoppedImages[c.SafeImageID()] { if err := restartStaleContainer(c, client, params); err != nil { failed[c.ID()] = err - } else if c.Stale { + } else if c.IsStale() { // Only add (previously) stale containers' images to cleanup 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 // 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 @@ -235,7 +235,7 @@ func restartStaleContainer(container container.Container, client container.Clien // UpdateImplicitRestart iterates through the passed containers, setting the // `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 { if c.ToRestart() { @@ -249,7 +249,7 @@ func UpdateImplicitRestart(containers []container.Container) { "linked": c.Name(), }).Debug("container is linked to restarting") // 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 // 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 _, candidate := range containers { if candidate.Name() == linkName && candidate.ToRestart() { diff --git a/internal/actions/update_test.go b/internal/actions/update_test.go index eb540b1..24534de 100644 --- a/internal/actions/update_test.go +++ b/internal/actions/update_test.go @@ -4,7 +4,6 @@ import ( "time" "github.com/containrrr/watchtower/internal/actions" - "github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/types" dockerTypes "github.com/docker/docker/api/types" dockerContainer "github.com/docker/docker/api/types/container" @@ -18,7 +17,7 @@ import ( func getCommonTestData(keepContainer string) *TestData { return &TestData{ NameOfContainerToKeep: keepContainer, - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container-01", "test-container-01", @@ -59,7 +58,7 @@ func getLinkedTestData(withImageInfo bool) *TestData { return &TestData{ Staleness: map[string]bool{linkingContainer.Name(): false}, - Containers: []container.Container{ + Containers: []types.Container{ staleContainer, linkingContainer, }, @@ -130,7 +129,7 @@ var _ = Describe("the update action", func() { client := CreateMockClient( &TestData{ NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container-01", "test-container-01", @@ -163,7 +162,7 @@ var _ = Describe("the update action", func() { It("should not update any containers", func() { client := CreateMockClient( &TestData{ - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainer( "test-container-01", "test-container-01", @@ -194,7 +193,7 @@ var _ = Describe("the update action", func() { client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", @@ -227,7 +226,7 @@ var _ = Describe("the update action", func() { client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", @@ -259,7 +258,7 @@ var _ = Describe("the update action", func() { client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", @@ -300,7 +299,7 @@ var _ = Describe("the update action", func() { ExposedPorts: map[nat.Port]struct{}{}, }) - provider.Stale = true + provider.SetStale(true) consumer := CreateMockContainerWithConfig( "test-container-consumer", @@ -316,7 +315,7 @@ var _ = Describe("the update action", func() { ExposedPorts: map[nat.Port]struct{}{}, }) - containers := []container.Container{ + containers := []types.Container{ provider, consumer, } @@ -338,7 +337,7 @@ var _ = Describe("the update action", func() { client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", @@ -370,7 +369,7 @@ var _ = Describe("the update action", func() { client := CreateMockClient( &TestData{ //NameOfContainerToKeep: "test-container-02", - Containers: []container.Container{ + Containers: []types.Container{ CreateMockContainerWithConfig( "test-container-02", "test-container-02", diff --git a/pkg/container/client.go b/pkg/container/client.go index 753f195..5f393e7 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -25,15 +25,15 @@ const defaultStopSignal = "SIGTERM" // A Client is the interface through which watchtower interacts with the // Docker API. type Client interface { - ListContainers(t.Filter) ([]Container, error) - GetContainer(containerID t.ContainerID) (Container, error) - StopContainer(Container, time.Duration) error - StartContainer(Container) (t.ContainerID, error) - RenameContainer(Container, string) error - IsContainerStale(Container) (stale bool, latestImage t.ImageID, err error) + ListContainers(t.Filter) ([]t.Container, error) + GetContainer(containerID t.ContainerID) (t.Container, error) + StopContainer(t.Container, time.Duration) error + StartContainer(t.Container) (t.ContainerID, error) + RenameContainer(t.Container, string) error + IsContainerStale(t.Container) (stale bool, latestImage t.ImageID, err error) ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err 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 @@ -82,7 +82,7 @@ type dockerClient struct { ClientOptions } -func (client dockerClient) WarnOnHeadPullFailed(container Container) bool { +func (client dockerClient) WarnOnHeadPullFailed(container t.Container) bool { if client.WarnOnHeadFailed == WarnAlways { return true } @@ -93,8 +93,8 @@ func (client dockerClient) WarnOnHeadPullFailed(container Container) bool { return registry.WarnOnAPIConsumption(container) } -func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) { - cs := []Container{} +func (client dockerClient) ListContainers(fn t.Filter) ([]t.Container, error) { + cs := []t.Container{} bg := context.Background() if client.IncludeStopped && client.IncludeRestarting { @@ -149,24 +149,24 @@ func (client dockerClient) createListFilter() filters.Args { return filterArgs } -func (client dockerClient) GetContainer(containerID t.ContainerID) (Container, error) { +func (client dockerClient) GetContainer(containerID t.ContainerID) (t.Container, error) { bg := context.Background() containerInfo, err := client.api.ContainerInspect(bg, string(containerID)) if err != nil { - return Container{}, err + return &Container{}, err } imageInfo, _, err := client.api.ImageInspectWithRaw(bg, containerInfo.Image) if err != nil { 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() signal := c.StopSignal() if signal == "" { @@ -186,7 +186,7 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err // TODO: This should probably be checked. _ = client.waitForStopOrTimeout(c, timeout) - if c.containerInfo.HostConfig.AutoRemove { + if c.ContainerInfo().HostConfig.AutoRemove { log.Debugf("AutoRemove container %s, skipping ContainerRemove call.", shortID) } else { log.Debugf("Removing container %s", shortID) @@ -208,11 +208,11 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err return nil } -func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) { +func (client dockerClient) StartContainer(c t.Container) (t.ContainerID, error) { bg := context.Background() - config := c.runtimeConfig() - hostConfig := c.hostConfig() - networkConfig := &network.NetworkingConfig{EndpointsConfig: c.containerInfo.NetworkSettings.Networks} + config := c.GetCreateConfig() + hostConfig := c.GetCreateHostConfig() + networkConfig := &network.NetworkingConfig{EndpointsConfig: c.ContainerInfo().NetworkSettings.Networks} // simpleNetworkConfig is a networkConfig with only 1 network. // see: https://github.com/docker/docker/issues/29265 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() 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 } -func (client dockerClient) RenameContainer(c Container, newName string) error { +func (client dockerClient) RenameContainer(c t.Container, newName string) error { bg := context.Background() log.Debugf("Renaming container %s (%s) to %s", c.Name(), c.ID().ShortID(), 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() if !client.PullImages || container.IsNoPull() { @@ -289,8 +289,8 @@ func (client dockerClient) IsContainerStale(container Container) (stale bool, la return client.HasNewImage(ctx, container) } -func (client dockerClient) HasNewImage(ctx context.Context, container Container) (hasNew bool, latestImage t.ImageID, err error) { - currentImageID := t.ImageID(container.containerInfo.ContainerJSONBase.Image) +func (client dockerClient) HasNewImage(ctx context.Context, container t.Container) (hasNew bool, latestImage t.ImageID, err error) { + currentImageID := t.ImageID(container.ContainerInfo().ContainerJSONBase.Image) imageName := container.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 // 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() imageName := container.ImageName() @@ -478,7 +478,7 @@ func (client dockerClient) waitForExecOrTimeout(bg context.Context, ID string, e 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() timeout := time.After(waitTime) diff --git a/pkg/container/client_test.go b/pkg/container/client_test.go index 1100ac9..645102e 100644 --- a/pkg/container/client_test.go +++ b/pkg/container/client_test.go @@ -35,8 +35,8 @@ var _ = Describe("the client", func() { mockServer.Close() }) Describe("WarnOnHeadPullFailed", func() { - containerUnknown := *MockContainer(WithImageName("unknown.repo/prefix/imagename:latest")) - containerKnown := *MockContainer(WithImageName("docker.io/prefix/imagename:latest")) + containerUnknown := MockContainer(WithImageName("unknown.repo/prefix/imagename:latest")) + containerKnown := MockContainer(WithImageName("docker.io/prefix/imagename:latest")) When(`warn on head failure is set to "always"`, func() { c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAlways}} @@ -66,7 +66,7 @@ var _ = Describe("the client", func() { When("the image consist of a pinned hash", func() { It("should gracefully fail with a useful message", func() { c := dockerClient{} - pinnedContainer := *MockContainer(WithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b")) + pinnedContainer := MockContainer(WithImageName("sha256:fa5269854a5e615e51a72b17ad3fd1e01268f278a6684c8ed3c5f0cdce3f230b")) c.PullImage(context.Background(), pinnedContainer) }) }) @@ -74,8 +74,8 @@ var _ = Describe("the client", func() { When("removing a running container", func() { When("the container still exist after stopping", func() { It("should attempt to remove the container", func() { - container := *MockContainer(WithContainerState(types.ContainerState{Running: true})) - containerStopped := *MockContainer(WithContainerState(types.ContainerState{Running: false})) + container := MockContainer(WithContainerState(types.ContainerState{Running: true})) + containerStopped := MockContainer(WithContainerState(types.ContainerState{Running: false})) cid := container.ContainerInfo().ID mockServer.AppendHandlers( @@ -90,7 +90,7 @@ var _ = Describe("the client", func() { }) When("the container does not exist after stopping", 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 mockServer.AppendHandlers( @@ -261,18 +261,18 @@ func withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher { return WithTransform(containerImageName, matcher) } -func containerImageName(container Container) string { +func containerImageName(container t.Container) string { return container.ImageName() } func havingRestartingState(expected bool) gt.GomegaMatcher { - return WithTransform(func(container Container) bool { - return container.containerInfo.State.Restarting + return WithTransform(func(container t.Container) bool { + return container.ContainerInfo().State.Restarting }, Equal(expected)) } func havingRunningState(expected bool) gt.GomegaMatcher { - return WithTransform(func(container Container) bool { - return container.containerInfo.State.Running + return WithTransform(func(container t.Container) bool { + return container.ContainerInfo().State.Running }, Equal(expected)) } diff --git a/pkg/container/container.go b/pkg/container/container.go index b52cdf6..20ae2e0 100644 --- a/pkg/container/container.go +++ b/pkg/container/container.go @@ -32,6 +32,26 @@ type Container struct { 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 func (c Container) ContainerInfo() *types.ContainerJSON { return c.containerInfo @@ -240,18 +260,23 @@ func (c Container) StopSignal() string { 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 // 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 // configuration (the stuff specified in the metadata for the image itself) // 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 // default options. To do this we have to compare the ContainerConfig for the // running container with the ContainerConfig from the image that container was // started from. This function returns a ContainerConfig which contains just // the options overridden at runtime. -func (c Container) runtimeConfig() *dockercontainer.Config { +func (c Container) GetCreateConfig() *dockercontainer.Config { config := c.containerInfo.Config hostConfig := c.containerInfo.HostConfig imageConfig := c.imageInfo.Config @@ -295,9 +320,9 @@ func (c Container) runtimeConfig() *dockercontainer.Config { return config } -// Any links in the HostConfig need to be re-written before they can be -// re-submitted to the Docker create API. -func (c Container) hostConfig() *dockercontainer.HostConfig { +// GetCreateHostConfig returns the container's current HostConfig with any links +// re-written so that they can be re-submitted to the Docker create API. +func (c Container) GetCreateHostConfig() *dockercontainer.HostConfig { hostConfig := c.containerInfo.HostConfig for i, link := range hostConfig.Links { diff --git a/pkg/lifecycle/lifecycle.go b/pkg/lifecycle/lifecycle.go index ed4ac20..c0f962e 100644 --- a/pkg/lifecycle/lifecycle.go +++ b/pkg/lifecycle/lifecycle.go @@ -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. -func ExecutePreCheckCommand(client container.Client, container container.Container) { +func ExecutePreCheckCommand(client container.Client, container types.Container) { clog := log.WithField("container", container.Name()) command := container.GetLifecyclePreCheckCommand() 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. -func ExecutePostCheckCommand(client container.Client, container container.Container) { +func ExecutePostCheckCommand(client container.Client, container types.Container) { clog := log.WithField("container", container.Name()) command := container.GetLifecyclePostCheckCommand() 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. -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() command := container.GetLifecyclePreUpdateCommand() clog := log.WithField("container", container.Name()) diff --git a/pkg/sorter/sort.go b/pkg/sorter/sort.go index 1e27f1b..b9d1e12 100644 --- a/pkg/sorter/sort.go +++ b/pkg/sorter/sort.go @@ -2,13 +2,14 @@ package sorter import ( "fmt" - "github.com/containrrr/watchtower/pkg/container" "time" + + "github.com/containrrr/watchtower/pkg/types" ) // ByCreated allows a list of Container structs to be sorted by the container's // created date. -type ByCreated []container.Container +type ByCreated []types.Container func (c ByCreated) Len() int { return len(c) } 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 // of their dependencies. This sort order ensures that linked containers can // be started in the correct order. -func SortByDependencies(containers []container.Container) ([]container.Container, error) { +func SortByDependencies(containers []types.Container) ([]types.Container, error) { sorter := dependencySorter{} return sorter.Sort(containers) } type dependencySorter struct { - unvisited []container.Container + unvisited []types.Container 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.marked = map[string]bool{} @@ -58,7 +59,7 @@ func (ds *dependencySorter) Sort(containers []container.Container) ([]container. 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 { return fmt.Errorf("circular reference to %s", c.Name()) @@ -84,7 +85,7 @@ func (ds *dependencySorter) visit(c container.Container) error { return nil } -func (ds *dependencySorter) findUnvisited(name string) *container.Container { +func (ds *dependencySorter) findUnvisited(name string) *types.Container { for _, c := range ds.unvisited { if c.Name() == name { return &c @@ -94,7 +95,7 @@ func (ds *dependencySorter) findUnvisited(name string) *container.Container { return nil } -func (ds *dependencySorter) removeUnvisited(c container.Container) { +func (ds *dependencySorter) removeUnvisited(c types.Container) { var idx int for i := range ds.unvisited { if ds.unvisited[i].Name() == c.Name() { diff --git a/pkg/types/container.go b/pkg/types/container.go index 22742e9..752fd11 100644 --- a/pkg/types/container.go +++ b/pkg/types/container.go @@ -1,8 +1,10 @@ package types import ( - "github.com/docker/docker/api/types" "strings" + + "github.com/docker/docker/api/types" + dc "github.com/docker/docker/api/types/container" ) // ImageID is a hash string representing a container image @@ -62,4 +64,15 @@ type Container interface { GetLifecyclePostCheckCommand() string GetLifecyclePreUpdateCommand() 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 }