feat(clean): log removed/untagged images (#1466)

pull/1630/head
nils måsén 2 years ago committed by GitHub
parent dd1ec09668
commit 0a5bd54fb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,24 @@
package util
import (
"bytes"
"fmt"
"math/rand"
)
// GenerateRandomSHA256 generates a random 64 character SHA 256 hash string
func GenerateRandomSHA256() string {
return GenerateRandomPrefixedSHA256()[7:]
}
// GenerateRandomPrefixedSHA256 generates a random 64 character SHA 256 hash string, prefixed with `sha256:`
func GenerateRandomPrefixedSHA256() string {
hash := make([]byte, 32)
_, _ = rand.Read(hash)
sb := bytes.NewBufferString("sha256:")
sb.Grow(64)
for _, h := range hash {
_, _ = fmt.Fprintf(sb, "%02x", h)
}
return sb.String()
}

@ -1,8 +1,10 @@
package util package util
import ( import (
"github.com/stretchr/testify/assert" "regexp"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestSliceEqual_True(t *testing.T) { func TestSliceEqual_True(t *testing.T) {
@ -62,3 +64,15 @@ func TestStructMapSubtract(t *testing.T) {
assert.Equal(t, map[string]struct{}{"a": x, "b": x, "c": x}, m1) assert.Equal(t, map[string]struct{}{"a": x, "b": x, "c": x}, m1)
assert.Equal(t, map[string]struct{}{"a": x, "c": x}, m2) assert.Equal(t, map[string]struct{}{"a": x, "c": x}, m2)
} }
// GenerateRandomSHA256 generates a random 64 character SHA 256 hash string
func TestGenerateRandomSHA256(t *testing.T) {
res := GenerateRandomSHA256()
assert.Len(t, res, 64)
assert.NotContains(t, res, "sha256:")
}
func TestGenerateRandomPrefixedSHA256(t *testing.T) {
res := GenerateRandomPrefixedSHA256()
assert.Regexp(t, regexp.MustCompile("sha256:[0-9|a-f]{64}"), res)
}

@ -39,9 +39,9 @@ type Client interface {
// NewClient returns a new Client instance which can be used to interact with // NewClient returns a new Client instance which can be used to interact with
// the Docker API. // the Docker API.
// The client reads its configuration from the following environment variables: // The client reads its configuration from the following environment variables:
// * DOCKER_HOST the docker-engine host to send api requests to // - DOCKER_HOST the docker-engine host to send api requests to
// * DOCKER_TLS_VERIFY whether to verify tls certificates // - DOCKER_TLS_VERIFY whether to verify tls certificates
// * DOCKER_API_VERSION the minimum docker api version to work with // - DOCKER_API_VERSION the minimum docker api version to work with
func NewClient(opts ClientOptions) Client { func NewClient(opts ClientOptions) Client {
cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv) cli, err := sdkClient.NewClientWithOpts(sdkClient.FromEnv)
@ -369,13 +369,34 @@ func (client dockerClient) PullImage(ctx context.Context, container t.Container)
func (client dockerClient) RemoveImageByID(id t.ImageID) error { func (client dockerClient) RemoveImageByID(id t.ImageID) error {
log.Infof("Removing image %s", id.ShortID()) log.Infof("Removing image %s", id.ShortID())
_, err := client.api.ImageRemove( items, err := client.api.ImageRemove(
context.Background(), context.Background(),
string(id), string(id),
types.ImageRemoveOptions{ types.ImageRemoveOptions{
Force: true, Force: true,
}) })
if log.IsLevelEnabled(log.DebugLevel) {
deleted := strings.Builder{}
untagged := strings.Builder{}
for _, item := range items {
if item.Deleted != "" {
if deleted.Len() > 0 {
deleted.WriteString(`, `)
}
deleted.WriteString(t.ImageID(item.Deleted).ShortID())
}
if item.Untagged != "" {
if untagged.Len() > 0 {
untagged.WriteString(`, `)
}
untagged.WriteString(t.ImageID(item.Untagged).ShortID())
}
}
fields := log.Fields{`deleted`: deleted.String(), `untagged`: untagged.String()}
log.WithFields(fields).Debug("Image removal completed")
}
return err return err
} }

@ -3,6 +3,7 @@ package container
import ( import (
"time" "time"
"github.com/containrrr/watchtower/internal/util"
"github.com/containrrr/watchtower/pkg/container/mocks" "github.com/containrrr/watchtower/pkg/container/mocks"
"github.com/containrrr/watchtower/pkg/filters" "github.com/containrrr/watchtower/pkg/filters"
t "github.com/containrrr/watchtower/pkg/types" t "github.com/containrrr/watchtower/pkg/types"
@ -10,6 +11,7 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/backend" "github.com/docker/docker/api/types/backend"
cli "github.com/docker/docker/client" cli "github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/onsi/gomega/gbytes" "github.com/onsi/gomega/gbytes"
"github.com/onsi/gomega/ghttp" "github.com/onsi/gomega/ghttp"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -103,6 +105,37 @@ var _ = Describe("the client", func() {
}) })
}) })
}) })
When("removing a image", func() {
When("debug logging is enabled", func() {
It("should log removed and untagged images", func() {
imageA := util.GenerateRandomSHA256()
imageAParent := util.GenerateRandomSHA256()
images := map[string][]string{imageA: {imageAParent}}
mockServer.AppendHandlers(mocks.RemoveImageHandler(images))
c := dockerClient{api: docker}
resetLogrus, logbuf := captureLogrus(logrus.DebugLevel)
defer resetLogrus()
Expect(c.RemoveImageByID(t.ImageID(imageA))).To(Succeed())
shortA := t.ImageID(imageA).ShortID()
shortAParent := t.ImageID(imageAParent).ShortID()
Eventually(logbuf).Should(gbytes.Say(`deleted="%v, %v" untagged="?%v"?`, shortA, shortAParent, shortA))
})
})
When("image is not found", func() {
It("should return an error", func() {
image := util.GenerateRandomSHA256()
mockServer.AppendHandlers(mocks.RemoveImageHandler(nil))
c := dockerClient{api: docker}
err := c.RemoveImageByID(t.ImageID(image))
Expect(errdefs.IsNotFound(err)).To(BeTrue())
})
})
})
When("listing containers", func() { When("listing containers", func() {
When("no filter is provided", func() { When("no filter is provided", func() {
It("should return all available containers", func() { It("should return all available containers", func() {
@ -193,10 +226,8 @@ var _ = Describe("the client", func() {
} }
// Capture logrus output in buffer // Capture logrus output in buffer
logbuf := gbytes.NewBuffer() resetLogrus, logbuf := captureLogrus(logrus.DebugLevel)
origOut := logrus.StandardLogger().Out defer resetLogrus()
defer logrus.SetOutput(origOut)
logrus.SetOutput(logbuf)
user := "" user := ""
containerID := t.ContainerID("ex-cont-id") containerID := t.ContainerID("ex-cont-id")
@ -255,6 +286,23 @@ var _ = Describe("the client", func() {
}) })
}) })
// Capture logrus output in buffer
func captureLogrus(level logrus.Level) (func(), *gbytes.Buffer) {
logbuf := gbytes.NewBuffer()
origOut := logrus.StandardLogger().Out
logrus.SetOutput(logbuf)
origLev := logrus.StandardLogger().Level
logrus.SetLevel(level)
return func() {
logrus.SetOutput(origOut)
logrus.SetLevel(origLev)
}, logbuf
}
// Gomega matcher helpers // Gomega matcher helpers
func withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher { func withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher {

@ -7,6 +7,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"path/filepath" "path/filepath"
"strings"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
@ -190,3 +191,29 @@ const (
Found FoundStatus = true Found FoundStatus = true
Missing FoundStatus = false Missing FoundStatus = false
) )
// RemoveImageHandler mocks the DELETE images/ID endpoint, simulating removal of the given imagesWithParents
func RemoveImageHandler(imagesWithParents map[string][]string) http.HandlerFunc {
return ghttp.CombineHandlers(
ghttp.VerifyRequest("DELETE", O.MatchRegexp("/images/.*")),
func(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(r.URL.Path, `/`)
image := parts[len(parts)-1]
if parents, found := imagesWithParents[image]; found {
items := []types.ImageDeleteResponseItem{
{Untagged: image},
{Deleted: image},
}
for _, parent := range parents {
items = append(items, types.ImageDeleteResponseItem{Deleted: parent})
}
ghttp.RespondWithJSONEncoded(http.StatusOK, items)(w, r)
} else {
ghttp.RespondWithJSONEncoded(http.StatusNotFound, struct{ message string }{
message: "Something went wrong.",
})(w, r)
}
},
)
}

Loading…
Cancel
Save