Skip to content

Commit

Permalink
feat: ignore removal error due to non-existing containers (#1481)
Browse files Browse the repository at this point in the history
Co-authored-by: nils måsén <nils@piksel.se>
Fixes #1480
  • Loading branch information
nothub committed Dec 6, 2022
1 parent a4d00bf commit 3190ce2
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 12 deletions.
4 changes: 4 additions & 0 deletions pkg/container/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
log.Debugf("Removing container %s", shortID)

if err := client.api.ContainerRemove(bg, idStr, types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.RemoveVolumes}); err != nil {
if sdkClient.IsErrNotFound(err) {
log.Debugf("Container %s not found, skipping removal.", shortID)
return nil
}
return err
}
}
Expand Down
34 changes: 34 additions & 0 deletions pkg/container/client_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package container

import (
"time"

"github.com/containrrr/watchtower/pkg/container/mocks"
"github.com/containrrr/watchtower/pkg/filters"
t "github.com/containrrr/watchtower/pkg/types"
Expand Down Expand Up @@ -69,6 +71,38 @@ 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}))

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() {
Expand Down
89 changes: 77 additions & 12 deletions pkg/container/mocks/ApiServer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package mocks
import (
"encoding/json"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
O "github.com/onsi/gomega"
"github.com/onsi/gomega/ghttp"
"io/ioutil"
"net/http"
"net/url"
"path/filepath"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
O "github.com/onsi/gomega"
"github.com/onsi/gomega/ghttp"
)

func getMockJSONFile(relPath string) ([]byte, error) {
Expand Down Expand Up @@ -42,14 +43,14 @@ func respondWithJSONFile(relPath string, statusCode int, optionalHeader ...http.
func GetContainerHandlers(containerFiles ...string) []http.HandlerFunc {
handlers := make([]http.HandlerFunc, 0, len(containerFiles)*2)
for _, file := range containerFiles {
handlers = append(handlers, getContainerHandler(file))
handlers = append(handlers, getContainerFileHandler(file))

// Also append the image request since that will be called for every container
if file == "running" {
// The "running" container is the only one using image02
handlers = append(handlers, getImageHandler(1))
handlers = append(handlers, getImageFileHandler(1))
} else {
handlers = append(handlers, getImageHandler(0))
handlers = append(handlers, getImageFileHandler(0))
}
}
return handlers
Expand All @@ -75,15 +76,36 @@ var imageIds = []string{
"sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd",
}

func getContainerHandler(file string) http.HandlerFunc {
func getContainerFileHandler(file string) http.HandlerFunc {
id, ok := containerFileIds[file]
failTestUnless(ok)
return ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", O.HaveSuffix("/containers/%v/json", id)),
return getContainerHandler(
id,
RespondWithJSONFile(fmt.Sprintf("./mocks/data/container_%v.json", file), http.StatusOK),
)
}

func getContainerHandler(containerId string, responseHandler http.HandlerFunc) http.HandlerFunc {
return ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", O.HaveSuffix("/containers/%v/json", containerId)),
responseHandler,
)
}

// GetContainerHandler mocks the GET containers/{id}/json endpoint
func GetContainerHandler(containerID string, containerInfo *types.ContainerJSON) http.HandlerFunc {
responseHandler := containerNotFoundResponse(containerID)
if containerInfo != nil {
responseHandler = ghttp.RespondWithJSONEncoded(http.StatusOK, containerInfo)
}
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))
}

// ListContainersHandler mocks the GET containers/json endpoint, filtering the returned containers based on statuses
func ListContainersHandler(statuses ...string) http.HandlerFunc {
filterArgs := createFilterArgs(statuses)
Expand Down Expand Up @@ -116,13 +138,56 @@ func respondWithFilteredContainers(filters filters.Args) http.HandlerFunc {
return ghttp.RespondWithJSONEncoded(http.StatusOK, filteredContainers)
}

func getImageHandler(index int) http.HandlerFunc {
func getImageHandler(imageId string, responseHandler http.HandlerFunc) http.HandlerFunc {
return ghttp.CombineHandlers(
ghttp.VerifyRequest("GET", O.HaveSuffix("/images/%v/json", imageIds[index])),
ghttp.VerifyRequest("GET", O.HaveSuffix("/images/%s/json", imageId)),
responseHandler,
)
}

func getImageFileHandler(index int) http.HandlerFunc {
return getImageHandler(imageIds[index],
RespondWithJSONFile(fmt.Sprintf("./mocks/data/image%02d.json", index+1), http.StatusOK),
)
}

func failTestUnless(ok bool) {
O.ExpectWithOffset(2, ok).To(O.BeTrue(), "test setup failed")
}

// KillContainerHandler mocks the POST containers/{id}/kill endpoint
func KillContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {
responseHandler := noContentStatusResponse
if !found {
responseHandler = containerNotFoundResponse(containerID)
}
return ghttp.CombineHandlers(
ghttp.VerifyRequest("POST", O.HaveSuffix("containers/%s/kill", containerID)),
responseHandler,
)
}

// RemoveContainerHandler mocks the DELETE containers/{id} endpoint
func RemoveContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {
responseHandler := noContentStatusResponse
if !found {
responseHandler = containerNotFoundResponse(containerID)
}
return ghttp.CombineHandlers(
ghttp.VerifyRequest("DELETE", O.HaveSuffix("containers/%s", containerID)),
responseHandler,
)
}

func containerNotFoundResponse(containerID string) http.HandlerFunc {
return ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{message: "No such container: " + containerID})
}

var noContentStatusResponse = ghttp.RespondWith(http.StatusNoContent, nil)

type FoundStatus bool

const (
Found FoundStatus = true
Missing FoundStatus = false
)

0 comments on commit 3190ce2

Please sign in to comment.