Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ignore removal error due to non-existing containers #1481

Merged
merged 2 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
)