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.
279 lines
9.9 KiB
Go
279 lines
9.9 KiB
Go
package container
|
|
|
|
import (
|
|
"time"
|
|
|
|
"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/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"))
|
|
c.PullImage(context.Background(), pinnedContainer)
|
|
})
|
|
})
|
|
})
|
|
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("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("watchtower", "running")...)
|
|
client := dockerClient{
|
|
api: docker,
|
|
ClientOptions: ClientOptions{PullImages: false},
|
|
}
|
|
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("watchtower", "running")...)
|
|
filter := filters.FilterByNames([]string{"lollercoaster"}, filters.NoFilter)
|
|
client := dockerClient{
|
|
api: docker,
|
|
ClientOptions: ClientOptions{PullImages: false},
|
|
}
|
|
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("watchtower", "running")...)
|
|
client := dockerClient{
|
|
api: docker,
|
|
ClientOptions: ClientOptions{PullImages: false},
|
|
}
|
|
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("stopped", "watchtower", "running")...)
|
|
client := dockerClient{
|
|
api: docker,
|
|
ClientOptions: ClientOptions{PullImages: false, 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("watchtower", "running", "restarting")...)
|
|
client := dockerClient{
|
|
api: docker,
|
|
ClientOptions: ClientOptions{PullImages: false, 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("watchtower", "running")...)
|
|
client := dockerClient{
|
|
api: docker,
|
|
ClientOptions: ClientOptions{PullImages: false, IncludeRestarting: false},
|
|
}
|
|
containers, err := client.ListContainers(filters.NoFilter)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(containers).NotTo(ContainElement(havingRestartingState(true)))
|
|
})
|
|
})
|
|
})
|
|
Describe(`ExecuteCommand`, func() {
|
|
When(`logging`, func() {
|
|
It("should include container id field", func() {
|
|
client := dockerClient{
|
|
api: docker,
|
|
ClientOptions: ClientOptions{PullImages: false},
|
|
}
|
|
|
|
// Capture logrus output in buffer
|
|
logbuf := gbytes.NewBuffer()
|
|
origOut := logrus.StandardLogger().Out
|
|
defer logrus.SetOutput(origOut)
|
|
logrus.SetOutput(logbuf)
|
|
|
|
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"?`))
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// Gomega matcher helpers
|
|
|
|
func withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher {
|
|
return WithTransform(containerImageName, matcher)
|
|
}
|
|
|
|
func containerImageName(container Container) string {
|
|
return container.ImageName()
|
|
}
|
|
|
|
func havingRestartingState(expected bool) gt.GomegaMatcher {
|
|
return WithTransform(func(container 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
|
|
}, Equal(expected))
|
|
}
|