Skip to content

Commit 3190ce2

Browse files
nothubpiksel
andauthored
feat: ignore removal error due to non-existing containers (#1481)
Co-authored-by: nils måsén <nils@piksel.se> Fixes #1480
1 parent a4d00bf commit 3190ce2

File tree

3 files changed

+115
-12
lines changed

3 files changed

+115
-12
lines changed

pkg/container/client.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
192192
log.Debugf("Removing container %s", shortID)
193193

194194
if err := client.api.ContainerRemove(bg, idStr, types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.RemoveVolumes}); err != nil {
195+
if sdkClient.IsErrNotFound(err) {
196+
log.Debugf("Container %s not found, skipping removal.", shortID)
197+
return nil
198+
}
195199
return err
196200
}
197201
}

pkg/container/client_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package container
22

33
import (
4+
"time"
5+
46
"github.com/containrrr/watchtower/pkg/container/mocks"
57
"github.com/containrrr/watchtower/pkg/filters"
68
t "github.com/containrrr/watchtower/pkg/types"
@@ -69,6 +71,38 @@ var _ = Describe("the client", func() {
6971
})
7072
})
7173
})
74+
When("removing a running container", func() {
75+
When("the container still exist after stopping", func() {
76+
It("should attempt to remove the container", func() {
77+
container := *MockContainer(WithContainerState(types.ContainerState{Running: true}))
78+
containerStopped := *MockContainer(WithContainerState(types.ContainerState{Running: false}))
79+
80+
cid := container.ContainerInfo().ID
81+
mockServer.AppendHandlers(
82+
mocks.KillContainerHandler(cid, mocks.Found),
83+
mocks.GetContainerHandler(cid, containerStopped.ContainerInfo()),
84+
mocks.RemoveContainerHandler(cid, mocks.Found),
85+
mocks.GetContainerHandler(cid, nil),
86+
)
87+
88+
Expect(dockerClient{api: docker}.StopContainer(container, time.Minute)).To(Succeed())
89+
})
90+
})
91+
When("the container does not exist after stopping", func() {
92+
It("should not cause an error", func() {
93+
container := *MockContainer(WithContainerState(types.ContainerState{Running: true}))
94+
95+
cid := container.ContainerInfo().ID
96+
mockServer.AppendHandlers(
97+
mocks.KillContainerHandler(cid, mocks.Found),
98+
mocks.GetContainerHandler(cid, nil),
99+
mocks.RemoveContainerHandler(cid, mocks.Missing),
100+
)
101+
102+
Expect(dockerClient{api: docker}.StopContainer(container, time.Minute)).To(Succeed())
103+
})
104+
})
105+
})
72106
When("listing containers", func() {
73107
When("no filter is provided", func() {
74108
It("should return all available containers", func() {

pkg/container/mocks/ApiServer.go

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ package mocks
33
import (
44
"encoding/json"
55
"fmt"
6-
"github.com/docker/docker/api/types"
7-
"github.com/docker/docker/api/types/filters"
8-
O "github.com/onsi/gomega"
9-
"github.com/onsi/gomega/ghttp"
106
"io/ioutil"
117
"net/http"
128
"net/url"
139
"path/filepath"
10+
11+
"github.com/docker/docker/api/types"
12+
"github.com/docker/docker/api/types/filters"
13+
O "github.com/onsi/gomega"
14+
"github.com/onsi/gomega/ghttp"
1415
)
1516

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

4748
// Also append the image request since that will be called for every container
4849
if file == "running" {
4950
// The "running" container is the only one using image02
50-
handlers = append(handlers, getImageHandler(1))
51+
handlers = append(handlers, getImageFileHandler(1))
5152
} else {
52-
handlers = append(handlers, getImageHandler(0))
53+
handlers = append(handlers, getImageFileHandler(0))
5354
}
5455
}
5556
return handlers
@@ -75,15 +76,36 @@ var imageIds = []string{
7576
"sha256:19d07168491a3f9e2798a9bed96544e34d57ddc4757a4ac5bb199dea896c87fd",
7677
}
7778

78-
func getContainerHandler(file string) http.HandlerFunc {
79+
func getContainerFileHandler(file string) http.HandlerFunc {
7980
id, ok := containerFileIds[file]
8081
failTestUnless(ok)
81-
return ghttp.CombineHandlers(
82-
ghttp.VerifyRequest("GET", O.HaveSuffix("/containers/%v/json", id)),
82+
return getContainerHandler(
83+
id,
8384
RespondWithJSONFile(fmt.Sprintf("./mocks/data/container_%v.json", file), http.StatusOK),
8485
)
8586
}
8687

88+
func getContainerHandler(containerId string, responseHandler http.HandlerFunc) http.HandlerFunc {
89+
return ghttp.CombineHandlers(
90+
ghttp.VerifyRequest("GET", O.HaveSuffix("/containers/%v/json", containerId)),
91+
responseHandler,
92+
)
93+
}
94+
95+
// GetContainerHandler mocks the GET containers/{id}/json endpoint
96+
func GetContainerHandler(containerID string, containerInfo *types.ContainerJSON) http.HandlerFunc {
97+
responseHandler := containerNotFoundResponse(containerID)
98+
if containerInfo != nil {
99+
responseHandler = ghttp.RespondWithJSONEncoded(http.StatusOK, containerInfo)
100+
}
101+
return getContainerHandler(containerID, responseHandler)
102+
}
103+
104+
// GetImageHandler mocks the GET images/{id}/json endpoint
105+
func GetImageHandler(imageInfo *types.ImageInspect) http.HandlerFunc {
106+
return getImageHandler(imageInfo.ID, ghttp.RespondWithJSONEncoded(http.StatusOK, imageInfo))
107+
}
108+
87109
// ListContainersHandler mocks the GET containers/json endpoint, filtering the returned containers based on statuses
88110
func ListContainersHandler(statuses ...string) http.HandlerFunc {
89111
filterArgs := createFilterArgs(statuses)
@@ -116,13 +138,56 @@ func respondWithFilteredContainers(filters filters.Args) http.HandlerFunc {
116138
return ghttp.RespondWithJSONEncoded(http.StatusOK, filteredContainers)
117139
}
118140

119-
func getImageHandler(index int) http.HandlerFunc {
141+
func getImageHandler(imageId string, responseHandler http.HandlerFunc) http.HandlerFunc {
120142
return ghttp.CombineHandlers(
121-
ghttp.VerifyRequest("GET", O.HaveSuffix("/images/%v/json", imageIds[index])),
143+
ghttp.VerifyRequest("GET", O.HaveSuffix("/images/%s/json", imageId)),
144+
responseHandler,
145+
)
146+
}
147+
148+
func getImageFileHandler(index int) http.HandlerFunc {
149+
return getImageHandler(imageIds[index],
122150
RespondWithJSONFile(fmt.Sprintf("./mocks/data/image%02d.json", index+1), http.StatusOK),
123151
)
124152
}
125153

126154
func failTestUnless(ok bool) {
127155
O.ExpectWithOffset(2, ok).To(O.BeTrue(), "test setup failed")
128156
}
157+
158+
// KillContainerHandler mocks the POST containers/{id}/kill endpoint
159+
func KillContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {
160+
responseHandler := noContentStatusResponse
161+
if !found {
162+
responseHandler = containerNotFoundResponse(containerID)
163+
}
164+
return ghttp.CombineHandlers(
165+
ghttp.VerifyRequest("POST", O.HaveSuffix("containers/%s/kill", containerID)),
166+
responseHandler,
167+
)
168+
}
169+
170+
// RemoveContainerHandler mocks the DELETE containers/{id} endpoint
171+
func RemoveContainerHandler(containerID string, found FoundStatus) http.HandlerFunc {
172+
responseHandler := noContentStatusResponse
173+
if !found {
174+
responseHandler = containerNotFoundResponse(containerID)
175+
}
176+
return ghttp.CombineHandlers(
177+
ghttp.VerifyRequest("DELETE", O.HaveSuffix("containers/%s", containerID)),
178+
responseHandler,
179+
)
180+
}
181+
182+
func containerNotFoundResponse(containerID string) http.HandlerFunc {
183+
return ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{message: "No such container: " + containerID})
184+
}
185+
186+
var noContentStatusResponse = ghttp.RespondWith(http.StatusNoContent, nil)
187+
188+
type FoundStatus bool
189+
190+
const (
191+
Found FoundStatus = true
192+
Missing FoundStatus = false
193+
)

0 commit comments

Comments
 (0)