From a762ec39aa9869f21f95765709b5dcd694ced341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Wed, 7 Dec 2022 15:28:59 +0100 Subject: [PATCH] add client timeout tests --- internal/testing/delayhttp/delayhttp.go | 35 +++++++++ pkg/container/client_test.go | 96 +++++++++++++++++++++++++ pkg/container/mocks/ApiServer.go | 35 +++++++-- 3 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 internal/testing/delayhttp/delayhttp.go diff --git a/internal/testing/delayhttp/delayhttp.go b/internal/testing/delayhttp/delayhttp.go new file mode 100644 index 0000000..a9702c9 --- /dev/null +++ b/internal/testing/delayhttp/delayhttp.go @@ -0,0 +1,35 @@ +// Package delayhttp creates http.HandlerFunc's that delays the response. +// Useful for testing timeout scenarios. +package delayhttp + +import ( + "net/http" + "time" +) + +// WithChannel returns a handler that delays until it recieves something on returnChan +func WithChannel(returnChan chan struct{}) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Wait until channel sends return code + <-returnChan + } +} + +// WithCancel returns a handler that delays until the cancel func is called. +// Useful together with defer to clean up tests. +func WithCancel() (http.HandlerFunc, func()) { + returnChan := make(chan struct{}, 1) + return WithChannel(returnChan), func() { + returnChan <- struct{}{} + } +} + +// WithTimeout returns a handler that delays until the passed duration has elapsed +func WithTimeout(delay time.Duration) http.HandlerFunc { + returnChan := make(chan struct{}, 1) + go func() { + time.Sleep(delay) + returnChan <- struct{}{} + }() + return WithChannel(returnChan) +} diff --git a/pkg/container/client_test.go b/pkg/container/client_test.go index 1100ac9..1057648 100644 --- a/pkg/container/client_test.go +++ b/pkg/container/client_test.go @@ -3,9 +3,12 @@ package container import ( "time" + "github.com/containrrr/watchtower/internal/testing/delayhttp" "github.com/containrrr/watchtower/pkg/container/mocks" "github.com/containrrr/watchtower/pkg/filters" + "github.com/containrrr/watchtower/pkg/registry" t "github.com/containrrr/watchtower/pkg/types" + wt "github.com/containrrr/watchtower/pkg/types" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/backend" @@ -25,6 +28,7 @@ import ( var _ = Describe("the client", func() { var docker *cli.Client var mockServer *ghttp.Server + mockRegServer := ghttp.NewTLSServer() BeforeEach(func() { mockServer = ghttp.NewServer() docker, _ = cli.NewClientWithOpts( @@ -33,6 +37,7 @@ var _ = Describe("the client", func() { }) AfterEach(func() { mockServer.Close() + mockRegServer.Reset() }) Describe("WarnOnHeadPullFailed", func() { containerUnknown := *MockContainer(WithImageName("unknown.repo/prefix/imagename:latest")) @@ -103,6 +108,97 @@ var _ = Describe("the client", func() { }) }) }) + When("checking if container is stale", func() { + + testRegPullFallback := func(client *dockerClient, cnt *Container, regResponses int) (bool, error) { + delayedResponseHandler, cancel := delayhttp.WithCancel() + defer cancel() + + // TODO: Add mock handlers for repository requests + regRequestHandlers := [][]http.HandlerFunc{ + { + ghttp.VerifyRequest("GET", HaveSuffix("v2/")), + func(_ http.ResponseWriter, _ *http.Request) { + Fail("registry request is not implemented") + }, + }, + } + + for i, regRequestPair := range regRequestHandlers { + + verifyHandler := regRequestPair[0] + responseHandler := regRequestPair[1] + + if i >= regResponses { + // Registry should not be responding + responseHandler = delayedResponseHandler + } + + mockRegServer.AppendHandlers(ghttp.CombineHandlers( + verifyHandler, + responseHandler, + )) + + if i >= regResponses { + // No need to add further handlers since the last one is blocking + break + } + } + + newImage := types.ImageInspect{ID: "newer_id"} + + // Docker should respond normally + mockServer.AppendHandlers( + mocks.PullImageHandlerOK(), + mocks.GetImageHandlerOK(cnt.ImageName(), &newImage), + ) + + stale, latest, err := client.IsContainerStale(*cnt) + + Expect(stale).To(Equal(latest == wt.ImageID(newImage.ID))) + + return stale, err + + } + + When("head request times out", func() { + It("should gracefully fail and continue using pull", func() { + mockContainer := MockContainer(WithImageName(mockRegServer.Addr() + "/prefix/imagename:latest")) + + regClient := registry.NewClientWithHTTPClient(mockRegServer.HTTPTestServer.Client()) + regClient.Timeout = time.Second * 2 + client := dockerClient{ + api: docker, + ClientOptions: ClientOptions{PullImages: true}, + reg: regClient, + } + + stale, err := testRegPullFallback(&client, mockContainer, 0) + Expect(err).NotTo(HaveOccurred()) + Expect(stale).To(BeTrue()) + + }) + }) + When("client request times out", func() { + It("should fail with a useful message", func() { + mockContainer := MockContainer(WithImageName(mockRegServer.Addr() + "/prefix/imagename:latest")) + + regClient := registry.NewClientWithHTTPClient(mockRegServer.HTTPTestServer.Client()) + client := dockerClient{ + api: docker, + ClientOptions: ClientOptions{ + Timeout: time.Second * 2, + PullImages: true, + }, + reg: regClient, + } + + _, err := testRegPullFallback(&client, mockContainer, 0) + Expect(err).To(MatchError(context.DeadlineExceeded)) + + }) + }) + }) When("listing containers", func() { When("no filter is provided", func() { It("should return all available containers", func() { diff --git a/pkg/container/mocks/ApiServer.go b/pkg/container/mocks/ApiServer.go index 1a05355..3cd13af 100644 --- a/pkg/container/mocks/ApiServer.go +++ b/pkg/container/mocks/ApiServer.go @@ -92,7 +92,10 @@ func getContainerHandler(containerId string, responseHandler http.HandlerFunc) h ) } -// GetContainerHandler mocks the GET containers/{id}/json endpoint +// GetContainerHandler mocks the GET containers/{containerID}/json endpoint. +// +// If containerInfo is nil, it returns an 200 OK http result with the containerInfo as the body. +// Otherwise, it returns the appropriate 404 NotFound result for the containerID. func GetContainerHandler(containerID string, containerInfo *types.ContainerJSON) http.HandlerFunc { responseHandler := containerNotFoundResponse(containerID) if containerInfo != nil { @@ -101,9 +104,29 @@ func GetContainerHandler(containerID string, containerInfo *types.ContainerJSON) return getContainerHandler(containerID, responseHandler) } -// GetImageHandler mocks the GET images/{id}/json endpoint -func GetImageHandler(imageInfo *types.ImageInspect) http.HandlerFunc { - return getImageHandler(imageInfo.ID, ghttp.RespondWithJSONEncoded(http.StatusOK, imageInfo)) +// GetContainerHandlerOK mocks the GET containers/{containerInfo.ID}/json endpoint, returning the containerInfo +func GetContainerHandlerOK(containerInfo *types.ContainerJSON) http.HandlerFunc { + O.ExpectWithOffset(1, containerInfo).ToNot(O.BeNil()) + return GetContainerHandler(containerInfo.ID, containerInfo) +} + +// GetImageHandler mocks the GET images/{imageName}/json endpoint, returns a 200 OK response with the imageInfo +func GetImageHandlerOK(imageName string, imageInfo *types.ImageInspect) http.HandlerFunc { + return getImageHandler(imageName, ghttp.RespondWithJSONEncoded(http.StatusOK, imageInfo)) +} + +// GetImageHandlerNotFound mocks the GET images/{imageName}/json endpoint, returning a 404 NotFound response +// with the appropriate error message for imageName +func GetImageHandlerNotFound(imageName string) http.HandlerFunc { + body := errorMessage{"no such image: " + imageName} + return getImageHandler(imageName, ghttp.RespondWithJSONEncoded(http.StatusNotFound, body)) +} + +func PullImageHandlerOK() http.HandlerFunc { + return ghttp.CombineHandlers( + ghttp.VerifyRequest("POST", O.HaveSuffix("images/create")), + ghttp.RespondWith(http.StatusNoContent, nil), + ) } // ListContainersHandler mocks the GET containers/json endpoint, filtering the returned containers based on statuses @@ -179,8 +202,10 @@ func RemoveContainerHandler(containerID string, found FoundStatus) http.HandlerF ) } +type errorMessage struct{ message string } + func containerNotFoundResponse(containerID string) http.HandlerFunc { - return ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{message: "No such container: " + containerID}) + return ghttp.RespondWithJSONEncoded(http.StatusNotFound, errorMessage{"No such container: " + containerID}) } var noContentStatusResponse = ghttp.RespondWith(http.StatusNoContent, nil)