mirror of https://github.com/containrrr/watchtower
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			378 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
			
		
		
	
	
			378 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
package container
 | 
						|
 | 
						|
import (
 | 
						|
	"github.com/docker/docker/api/types/network"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/containrrr/watchtower/internal/util"
 | 
						|
	"github.com/containrrr/watchtower/pkg/container/mocks"
 | 
						|
	"github.com/containrrr/watchtower/pkg/filters"
 | 
						|
	t "github.com/containrrr/watchtower/pkg/types"
 | 
						|
 | 
						|
	"github.com/docker/docker/api/types"
 | 
						|
	"github.com/docker/docker/api/types/backend"
 | 
						|
	cli "github.com/docker/docker/client"
 | 
						|
	"github.com/docker/docker/errdefs"
 | 
						|
	"github.com/onsi/gomega/gbytes"
 | 
						|
	"github.com/onsi/gomega/ghttp"
 | 
						|
	"github.com/sirupsen/logrus"
 | 
						|
 | 
						|
	. "github.com/onsi/ginkgo"
 | 
						|
	. "github.com/onsi/gomega"
 | 
						|
	gt "github.com/onsi/gomega/types"
 | 
						|
 | 
						|
	"context"
 | 
						|
	"net/http"
 | 
						|
)
 | 
						|
 | 
						|
var _ = Describe("the client", func() {
 | 
						|
	var docker *cli.Client
 | 
						|
	var mockServer *ghttp.Server
 | 
						|
	BeforeEach(func() {
 | 
						|
		mockServer = ghttp.NewServer()
 | 
						|
		docker, _ = cli.NewClientWithOpts(
 | 
						|
			cli.WithHost(mockServer.URL()),
 | 
						|
			cli.WithHTTPClient(mockServer.HTTPTestServer.Client()))
 | 
						|
	})
 | 
						|
	AfterEach(func() {
 | 
						|
		mockServer.Close()
 | 
						|
	})
 | 
						|
	Describe("WarnOnHeadPullFailed", func() {
 | 
						|
		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}}
 | 
						|
			It("should always return true", func() {
 | 
						|
				Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeTrue())
 | 
						|
				Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue())
 | 
						|
			})
 | 
						|
		})
 | 
						|
		When(`warn on head failure is set to "auto"`, func() {
 | 
						|
			c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnAuto}}
 | 
						|
			It("should return false for unknown repos", func() {
 | 
						|
				Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())
 | 
						|
			})
 | 
						|
			It("should return true for known repos", func() {
 | 
						|
				Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeTrue())
 | 
						|
			})
 | 
						|
		})
 | 
						|
		When(`warn on head failure is set to "never"`, func() {
 | 
						|
			c := dockerClient{ClientOptions: ClientOptions{WarnOnHeadFailed: WarnNever}}
 | 
						|
			It("should never return true", func() {
 | 
						|
				Expect(c.WarnOnHeadPullFailed(containerUnknown)).To(BeFalse())
 | 
						|
				Expect(c.WarnOnHeadPullFailed(containerKnown)).To(BeFalse())
 | 
						|
			})
 | 
						|
		})
 | 
						|
	})
 | 
						|
	When("pulling the latest image", 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"))
 | 
						|
				err := c.PullImage(context.Background(), pinnedContainer)
 | 
						|
				Expect(err).To(MatchError(`container uses a pinned image, and cannot be updated by watchtower`))
 | 
						|
			})
 | 
						|
		})
 | 
						|
	})
 | 
						|
	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}))
 | 
						|
 | 
						|
				cid := container.ContainerInfo().ID
 | 
						|
				mockServer.AppendHandlers(
 | 
						|
					mocks.KillContainerHandler(cid, mocks.Found),
 | 
						|
					mocks.GetContainerHandler(cid, containerStopped.ContainerInfo()),
 | 
						|
					mocks.RemoveContainerHandler(cid, mocks.Found),
 | 
						|
					mocks.GetContainerHandler(cid, nil),
 | 
						|
				)
 | 
						|
 | 
						|
				Expect(dockerClient{api: docker}.StopContainer(container, time.Minute)).To(Succeed())
 | 
						|
			})
 | 
						|
		})
 | 
						|
		When("the container does not exist after stopping", func() {
 | 
						|
			It("should not cause an error", func() {
 | 
						|
				container := MockContainer(WithContainerState(types.ContainerState{Running: true}))
 | 
						|
 | 
						|
				cid := container.ContainerInfo().ID
 | 
						|
				mockServer.AppendHandlers(
 | 
						|
					mocks.KillContainerHandler(cid, mocks.Found),
 | 
						|
					mocks.GetContainerHandler(cid, nil),
 | 
						|
					mocks.RemoveContainerHandler(cid, mocks.Missing),
 | 
						|
				)
 | 
						|
 | 
						|
				Expect(dockerClient{api: docker}.StopContainer(container, time.Minute)).To(Succeed())
 | 
						|
			})
 | 
						|
		})
 | 
						|
	})
 | 
						|
	When("removing a image", func() {
 | 
						|
		When("debug logging is enabled", func() {
 | 
						|
			It("should log removed and untagged images", func() {
 | 
						|
				imageA := util.GenerateRandomSHA256()
 | 
						|
				imageAParent := util.GenerateRandomSHA256()
 | 
						|
				images := map[string][]string{imageA: {imageAParent}}
 | 
						|
				mockServer.AppendHandlers(mocks.RemoveImageHandler(images))
 | 
						|
				c := dockerClient{api: docker}
 | 
						|
 | 
						|
				resetLogrus, logbuf := captureLogrus(logrus.DebugLevel)
 | 
						|
				defer resetLogrus()
 | 
						|
 | 
						|
				Expect(c.RemoveImageByID(t.ImageID(imageA))).To(Succeed())
 | 
						|
 | 
						|
				shortA := t.ImageID(imageA).ShortID()
 | 
						|
				shortAParent := t.ImageID(imageAParent).ShortID()
 | 
						|
 | 
						|
				Eventually(logbuf).Should(gbytes.Say(`deleted="%v, %v" untagged="?%v"?`, shortA, shortAParent, shortA))
 | 
						|
			})
 | 
						|
		})
 | 
						|
		When("image is not found", func() {
 | 
						|
			It("should return an error", func() {
 | 
						|
				image := util.GenerateRandomSHA256()
 | 
						|
				mockServer.AppendHandlers(mocks.RemoveImageHandler(nil))
 | 
						|
				c := dockerClient{api: docker}
 | 
						|
 | 
						|
				err := c.RemoveImageByID(t.ImageID(image))
 | 
						|
				Expect(errdefs.IsNotFound(err)).To(BeTrue())
 | 
						|
			})
 | 
						|
		})
 | 
						|
	})
 | 
						|
	When("listing containers", func() {
 | 
						|
		When("no filter is provided", func() {
 | 
						|
			It("should return all available containers", func() {
 | 
						|
				mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
 | 
						|
				mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
 | 
						|
				client := dockerClient{
 | 
						|
					api:           docker,
 | 
						|
					ClientOptions: ClientOptions{},
 | 
						|
				}
 | 
						|
				containers, err := client.ListContainers(filters.NoFilter)
 | 
						|
				Expect(err).NotTo(HaveOccurred())
 | 
						|
				Expect(containers).To(HaveLen(2))
 | 
						|
			})
 | 
						|
		})
 | 
						|
		When("a filter matching nothing", func() {
 | 
						|
			It("should return an empty array", func() {
 | 
						|
				mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
 | 
						|
				mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
 | 
						|
				filter := filters.FilterByNames([]string{"lollercoaster"}, filters.NoFilter)
 | 
						|
				client := dockerClient{
 | 
						|
					api:           docker,
 | 
						|
					ClientOptions: ClientOptions{},
 | 
						|
				}
 | 
						|
				containers, err := client.ListContainers(filter)
 | 
						|
				Expect(err).NotTo(HaveOccurred())
 | 
						|
				Expect(containers).To(BeEmpty())
 | 
						|
			})
 | 
						|
		})
 | 
						|
		When("a watchtower filter is provided", func() {
 | 
						|
			It("should return only the watchtower container", func() {
 | 
						|
				mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
 | 
						|
				mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
 | 
						|
				client := dockerClient{
 | 
						|
					api:           docker,
 | 
						|
					ClientOptions: ClientOptions{},
 | 
						|
				}
 | 
						|
				containers, err := client.ListContainers(filters.WatchtowerContainersFilter)
 | 
						|
				Expect(err).NotTo(HaveOccurred())
 | 
						|
				Expect(containers).To(ConsistOf(withContainerImageName(Equal("containrrr/watchtower:latest"))))
 | 
						|
			})
 | 
						|
		})
 | 
						|
		When(`include stopped is enabled`, func() {
 | 
						|
			It("should return both stopped and running containers", func() {
 | 
						|
				mockServer.AppendHandlers(mocks.ListContainersHandler("running", "exited", "created"))
 | 
						|
				mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Stopped, &mocks.Watchtower, &mocks.Running)...)
 | 
						|
				client := dockerClient{
 | 
						|
					api:           docker,
 | 
						|
					ClientOptions: ClientOptions{IncludeStopped: true},
 | 
						|
				}
 | 
						|
				containers, err := client.ListContainers(filters.NoFilter)
 | 
						|
				Expect(err).NotTo(HaveOccurred())
 | 
						|
				Expect(containers).To(ContainElement(havingRunningState(false)))
 | 
						|
			})
 | 
						|
		})
 | 
						|
		When(`include restarting is enabled`, func() {
 | 
						|
			It("should return both restarting and running containers", func() {
 | 
						|
				mockServer.AppendHandlers(mocks.ListContainersHandler("running", "restarting"))
 | 
						|
				mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running, &mocks.Restarting)...)
 | 
						|
				client := dockerClient{
 | 
						|
					api:           docker,
 | 
						|
					ClientOptions: ClientOptions{IncludeRestarting: true},
 | 
						|
				}
 | 
						|
				containers, err := client.ListContainers(filters.NoFilter)
 | 
						|
				Expect(err).NotTo(HaveOccurred())
 | 
						|
				Expect(containers).To(ContainElement(havingRestartingState(true)))
 | 
						|
			})
 | 
						|
		})
 | 
						|
		When(`include restarting is disabled`, func() {
 | 
						|
			It("should not return restarting containers", func() {
 | 
						|
				mockServer.AppendHandlers(mocks.ListContainersHandler("running"))
 | 
						|
				mockServer.AppendHandlers(mocks.GetContainerHandlers(&mocks.Watchtower, &mocks.Running)...)
 | 
						|
				client := dockerClient{
 | 
						|
					api:           docker,
 | 
						|
					ClientOptions: ClientOptions{IncludeRestarting: false},
 | 
						|
				}
 | 
						|
				containers, err := client.ListContainers(filters.NoFilter)
 | 
						|
				Expect(err).NotTo(HaveOccurred())
 | 
						|
				Expect(containers).NotTo(ContainElement(havingRestartingState(true)))
 | 
						|
			})
 | 
						|
		})
 | 
						|
		When(`a container uses container network mode`, func() {
 | 
						|
			When(`the network container can be resolved`, func() {
 | 
						|
				It("should return the container name instead of the ID", func() {
 | 
						|
					consumerContainerRef := mocks.NetConsumerOK
 | 
						|
					mockServer.AppendHandlers(mocks.GetContainerHandlers(&consumerContainerRef)...)
 | 
						|
					client := dockerClient{
 | 
						|
						api:           docker,
 | 
						|
						ClientOptions: ClientOptions{},
 | 
						|
					}
 | 
						|
					container, err := client.GetContainer(consumerContainerRef.ContainerID())
 | 
						|
					Expect(err).NotTo(HaveOccurred())
 | 
						|
					networkMode := container.ContainerInfo().HostConfig.NetworkMode
 | 
						|
					Expect(networkMode.ConnectedContainer()).To(Equal(mocks.NetSupplierContainerName))
 | 
						|
				})
 | 
						|
			})
 | 
						|
			When(`the network container cannot be resolved`, func() {
 | 
						|
				It("should still return the container ID", func() {
 | 
						|
					consumerContainerRef := mocks.NetConsumerInvalidSupplier
 | 
						|
					mockServer.AppendHandlers(mocks.GetContainerHandlers(&consumerContainerRef)...)
 | 
						|
					client := dockerClient{
 | 
						|
						api:           docker,
 | 
						|
						ClientOptions: ClientOptions{},
 | 
						|
					}
 | 
						|
					container, err := client.GetContainer(consumerContainerRef.ContainerID())
 | 
						|
					Expect(err).NotTo(HaveOccurred())
 | 
						|
					networkMode := container.ContainerInfo().HostConfig.NetworkMode
 | 
						|
					Expect(networkMode.ConnectedContainer()).To(Equal(mocks.NetSupplierNotFoundID))
 | 
						|
				})
 | 
						|
			})
 | 
						|
		})
 | 
						|
	})
 | 
						|
	Describe(`ExecuteCommand`, func() {
 | 
						|
		When(`logging`, func() {
 | 
						|
			It("should include container id field", func() {
 | 
						|
				client := dockerClient{
 | 
						|
					api:           docker,
 | 
						|
					ClientOptions: ClientOptions{},
 | 
						|
				}
 | 
						|
 | 
						|
				// Capture logrus output in buffer
 | 
						|
				resetLogrus, logbuf := captureLogrus(logrus.DebugLevel)
 | 
						|
				defer resetLogrus()
 | 
						|
 | 
						|
				user := ""
 | 
						|
				containerID := t.ContainerID("ex-cont-id")
 | 
						|
				execID := "ex-exec-id"
 | 
						|
				cmd := "exec-cmd"
 | 
						|
 | 
						|
				mockServer.AppendHandlers(
 | 
						|
					// API.ContainerExecCreate
 | 
						|
					ghttp.CombineHandlers(
 | 
						|
						ghttp.VerifyRequest("POST", HaveSuffix("containers/%v/exec", containerID)),
 | 
						|
						ghttp.VerifyJSONRepresenting(types.ExecConfig{
 | 
						|
							User:   user,
 | 
						|
							Detach: false,
 | 
						|
							Tty:    true,
 | 
						|
							Cmd: []string{
 | 
						|
								"sh",
 | 
						|
								"-c",
 | 
						|
								cmd,
 | 
						|
							},
 | 
						|
						}),
 | 
						|
						ghttp.RespondWithJSONEncoded(http.StatusOK, types.IDResponse{ID: execID}),
 | 
						|
					),
 | 
						|
					// API.ContainerExecStart
 | 
						|
					ghttp.CombineHandlers(
 | 
						|
						ghttp.VerifyRequest("POST", HaveSuffix("exec/%v/start", execID)),
 | 
						|
						ghttp.VerifyJSONRepresenting(types.ExecStartCheck{
 | 
						|
							Detach: false,
 | 
						|
							Tty:    true,
 | 
						|
						}),
 | 
						|
						ghttp.RespondWith(http.StatusOK, nil),
 | 
						|
					),
 | 
						|
					// API.ContainerExecInspect
 | 
						|
					ghttp.CombineHandlers(
 | 
						|
						ghttp.VerifyRequest("GET", HaveSuffix("exec/ex-exec-id/json")),
 | 
						|
						ghttp.RespondWithJSONEncoded(http.StatusOK, backend.ExecInspect{
 | 
						|
							ID:       execID,
 | 
						|
							Running:  false,
 | 
						|
							ExitCode: nil,
 | 
						|
							ProcessConfig: &backend.ExecProcessConfig{
 | 
						|
								Entrypoint: "sh",
 | 
						|
								Arguments:  []string{"-c", cmd},
 | 
						|
								User:       user,
 | 
						|
							},
 | 
						|
							ContainerID: string(containerID),
 | 
						|
						}),
 | 
						|
					),
 | 
						|
				)
 | 
						|
 | 
						|
				_, err := client.ExecuteCommand(containerID, cmd, 1)
 | 
						|
				Expect(err).NotTo(HaveOccurred())
 | 
						|
				// Note: Since Execute requires opening up a raw TCP stream to the daemon for the output, this will fail
 | 
						|
				// when using the mock API server. Regardless of the outcome, the log should include the container ID
 | 
						|
				Eventually(logbuf).Should(gbytes.Say(`containerID="?ex-cont-id"?`))
 | 
						|
			})
 | 
						|
		})
 | 
						|
	})
 | 
						|
	Describe(`GetNetworkConfig`, func() {
 | 
						|
		When(`providing a container with network aliases`, func() {
 | 
						|
			It(`should omit the container ID alias`, func() {
 | 
						|
				client := dockerClient{
 | 
						|
					api:           docker,
 | 
						|
					ClientOptions: ClientOptions{IncludeRestarting: false},
 | 
						|
				}
 | 
						|
				container := MockContainer(WithImageName("docker.io/prefix/imagename:latest"))
 | 
						|
 | 
						|
				aliases := []string{"One", "Two", container.ID().ShortID(), "Four"}
 | 
						|
				endpoints := map[string]*network.EndpointSettings{
 | 
						|
					`test`: {Aliases: aliases},
 | 
						|
				}
 | 
						|
				container.containerInfo.NetworkSettings = &types.NetworkSettings{Networks: endpoints}
 | 
						|
				Expect(container.ContainerInfo().NetworkSettings.Networks[`test`].Aliases).To(Equal(aliases))
 | 
						|
				Expect(client.GetNetworkConfig(container).EndpointsConfig[`test`].Aliases).To(Equal([]string{"One", "Two", "Four"}))
 | 
						|
			})
 | 
						|
		})
 | 
						|
	})
 | 
						|
})
 | 
						|
 | 
						|
// Capture logrus output in buffer
 | 
						|
func captureLogrus(level logrus.Level) (func(), *gbytes.Buffer) {
 | 
						|
 | 
						|
	logbuf := gbytes.NewBuffer()
 | 
						|
 | 
						|
	origOut := logrus.StandardLogger().Out
 | 
						|
	logrus.SetOutput(logbuf)
 | 
						|
 | 
						|
	origLev := logrus.StandardLogger().Level
 | 
						|
	logrus.SetLevel(level)
 | 
						|
 | 
						|
	return func() {
 | 
						|
		logrus.SetOutput(origOut)
 | 
						|
		logrus.SetLevel(origLev)
 | 
						|
	}, logbuf
 | 
						|
}
 | 
						|
 | 
						|
// Gomega matcher helpers
 | 
						|
 | 
						|
func withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher {
 | 
						|
	return WithTransform(containerImageName, matcher)
 | 
						|
}
 | 
						|
 | 
						|
func containerImageName(container t.Container) string {
 | 
						|
	return container.ImageName()
 | 
						|
}
 | 
						|
 | 
						|
func havingRestartingState(expected bool) gt.GomegaMatcher {
 | 
						|
	return WithTransform(func(container t.Container) bool {
 | 
						|
		return container.ContainerInfo().State.Restarting
 | 
						|
	}, Equal(expected))
 | 
						|
}
 | 
						|
 | 
						|
func havingRunningState(expected bool) gt.GomegaMatcher {
 | 
						|
	return WithTransform(func(container t.Container) bool {
 | 
						|
		return container.ContainerInfo().State.Running
 | 
						|
	}, Equal(expected))
 | 
						|
}
 |