Skip to content

Commit fdbbbe7

Browse files
committed
feat(clean): log removed/untagged images
1 parent dd1ec09 commit fdbbbe7

File tree

4 files changed

+128
-8
lines changed

4 files changed

+128
-8
lines changed

internal/util/rand_sha256.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package util
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"math/rand"
7+
)
8+
9+
// GenerateRandomSHA256 generates a random 64 character SHA 256 hash string
10+
func GenerateRandomSHA256() string {
11+
return GenerateRandomPrefixedSHA256()[7:]
12+
}
13+
14+
// GenerateRandomPrefixedSHA256 generates a random 64 character SHA 256 hash string, prefixed with `sha256:`
15+
func GenerateRandomPrefixedSHA256() string {
16+
hash := make([]byte, 32)
17+
_, _ = rand.Read(hash)
18+
sb := bytes.NewBufferString("sha256:")
19+
sb.Grow(64)
20+
for _, h := range hash {
21+
_, _ = fmt.Fprintf(sb, "%02x", h)
22+
}
23+
return sb.String()
24+
}

pkg/container/client.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ type Client interface {
3939
// NewClient returns a new Client instance which can be used to interact with
4040
// the Docker API.
4141
// The client reads its configuration from the following environment variables:
42-
// * DOCKER_HOST the docker-engine host to send api requests to
43-
// * DOCKER_TLS_VERIFY whether to verify tls certificates
44-
// * DOCKER_API_VERSION the minimum docker api version to work with
42+
// - DOCKER_HOST the docker-engine host to send api requests to
43+
// - DOCKER_TLS_VERIFY whether to verify tls certificates
44+
// - DOCKER_API_VERSION the minimum docker api version to work with
4545
func NewClient(opts ClientOptions) Client {
4646
cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv)
4747

@@ -369,13 +369,34 @@ func (client dockerClient) PullImage(ctx context.Context, container t.Container)
369369
func (client dockerClient) RemoveImageByID(id t.ImageID) error {
370370
log.Infof("Removing image %s", id.ShortID())
371371

372-
_, err := client.api.ImageRemove(
372+
items, err := client.api.ImageRemove(
373373
context.Background(),
374374
string(id),
375375
types.ImageRemoveOptions{
376376
Force: true,
377377
})
378378

379+
if log.IsLevelEnabled(log.DebugLevel) {
380+
deleted := strings.Builder{}
381+
untagged := strings.Builder{}
382+
for _, item := range items {
383+
if item.Deleted != "" {
384+
if deleted.Len() > 0 {
385+
deleted.WriteString(`, `)
386+
}
387+
deleted.WriteString(t.ImageID(item.Deleted).ShortID())
388+
}
389+
if item.Untagged != "" {
390+
if untagged.Len() > 0 {
391+
untagged.WriteString(`, `)
392+
}
393+
untagged.WriteString(t.ImageID(item.Untagged).ShortID())
394+
}
395+
}
396+
fields := log.Fields{`deleted`: deleted.String(), `untagged`: untagged.String()}
397+
log.WithFields(fields).Debug("Image removal completed")
398+
}
399+
379400
return err
380401
}
381402

pkg/container/client_test.go

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ package container
33
import (
44
"time"
55

6+
"github.com/containrrr/watchtower/internal/util"
67
"github.com/containrrr/watchtower/pkg/container/mocks"
78
"github.com/containrrr/watchtower/pkg/filters"
89
t "github.com/containrrr/watchtower/pkg/types"
910

1011
"github.com/docker/docker/api/types"
1112
"github.com/docker/docker/api/types/backend"
1213
cli "github.com/docker/docker/client"
14+
"github.com/docker/docker/errdefs"
1315
"github.com/onsi/gomega/gbytes"
1416
"github.com/onsi/gomega/ghttp"
1517
"github.com/sirupsen/logrus"
@@ -103,6 +105,37 @@ var _ = Describe("the client", func() {
103105
})
104106
})
105107
})
108+
When("removing a image", func() {
109+
When("debug logging is enabled", func() {
110+
It("should log removed and untagged images", func() {
111+
imageA := util.GenerateRandomSHA256()
112+
imageAParent := util.GenerateRandomSHA256()
113+
images := map[string][]string{imageA: {imageAParent}}
114+
mockServer.AppendHandlers(mocks.RemoveImageHandler(images))
115+
c := dockerClient{api: docker}
116+
117+
resetLogrus, logbuf := captureLogrus(logrus.DebugLevel)
118+
defer resetLogrus()
119+
120+
Expect(c.RemoveImageByID(t.ImageID(imageA))).To(Succeed())
121+
122+
shortA := t.ImageID(imageA).ShortID()
123+
shortAParent := t.ImageID(imageAParent).ShortID()
124+
125+
Eventually(logbuf).Should(gbytes.Say(`deleted="%v, %v" untagged="?%v"?`, shortA, shortAParent, shortA))
126+
})
127+
})
128+
When("image is not found", func() {
129+
It("should return an error", func() {
130+
image := util.GenerateRandomSHA256()
131+
mockServer.AppendHandlers(mocks.RemoveImageHandler(nil))
132+
c := dockerClient{api: docker}
133+
134+
err := c.RemoveImageByID(t.ImageID(image))
135+
Expect(errdefs.IsNotFound(err)).To(BeTrue())
136+
})
137+
})
138+
})
106139
When("listing containers", func() {
107140
When("no filter is provided", func() {
108141
It("should return all available containers", func() {
@@ -193,10 +226,8 @@ var _ = Describe("the client", func() {
193226
}
194227

195228
// Capture logrus output in buffer
196-
logbuf := gbytes.NewBuffer()
197-
origOut := logrus.StandardLogger().Out
198-
defer logrus.SetOutput(origOut)
199-
logrus.SetOutput(logbuf)
229+
resetLogrus, logbuf := captureLogrus(logrus.DebugLevel)
230+
defer resetLogrus()
200231

201232
user := ""
202233
containerID := t.ContainerID("ex-cont-id")
@@ -255,6 +286,23 @@ var _ = Describe("the client", func() {
255286
})
256287
})
257288

289+
// Capture logrus output in buffer
290+
func captureLogrus(level logrus.Level) (func(), *gbytes.Buffer) {
291+
292+
logbuf := gbytes.NewBuffer()
293+
294+
origOut := logrus.StandardLogger().Out
295+
logrus.SetOutput(logbuf)
296+
297+
origLev := logrus.StandardLogger().Level
298+
logrus.SetLevel(level)
299+
300+
return func() {
301+
logrus.SetOutput(origOut)
302+
logrus.SetLevel(origLev)
303+
}, logbuf
304+
}
305+
258306
// Gomega matcher helpers
259307

260308
func withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher {

pkg/container/mocks/ApiServer.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88
"net/url"
99
"path/filepath"
10+
"strings"
1011

1112
"github.com/docker/docker/api/types"
1213
"github.com/docker/docker/api/types/filters"
@@ -190,3 +191,29 @@ const (
190191
Found FoundStatus = true
191192
Missing FoundStatus = false
192193
)
194+
195+
// RemoveImageHandler mocks the DELETE images/ID endpoint, simulating removal of the given imagesWithParents
196+
func RemoveImageHandler(imagesWithParents map[string][]string) http.HandlerFunc {
197+
return ghttp.CombineHandlers(
198+
ghttp.VerifyRequest("DELETE", O.MatchRegexp("/images/.*")),
199+
func(w http.ResponseWriter, r *http.Request) {
200+
parts := strings.Split(r.URL.Path, `/`)
201+
image := parts[len(parts)-1]
202+
203+
if parents, found := imagesWithParents[image]; found {
204+
items := []types.ImageDeleteResponseItem{
205+
{Untagged: image},
206+
{Deleted: image},
207+
}
208+
for _, parent := range parents {
209+
items = append(items, types.ImageDeleteResponseItem{Deleted: parent})
210+
}
211+
ghttp.RespondWithJSONEncoded(http.StatusOK, items)(w, r)
212+
} else {
213+
ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{
214+
message: "Something went wrong.",
215+
})(w, r)
216+
}
217+
},
218+
)
219+
}

0 commit comments

Comments
 (0)