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

pull/1630/head
nils måsén 1 year 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
import (
"github.com/stretchr/testify/assert"
"regexp"
"testing"
"github.com/stretchr/testify/assert"
)
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, "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
// the Docker API.
// The client reads its configuration from the following environment variables:
// * DOCKER_HOST the docker-engine host to send api requests to
// * DOCKER_TLS_VERIFY whether to verify tls certificates
// * DOCKER_API_VERSION the minimum docker api version to work with
// - DOCKER_HOST the docker-engine host to send api requests to
// - DOCKER_TLS_VERIFY whether to verify tls certificates
// - DOCKER_API_VERSION the minimum docker api version to work with
func NewClient(opts ClientOptions) Client {
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 {
log.Infof("Removing image %s", id.ShortID())
_, err := client.api.ImageRemove(
items, err := client.api.ImageRemove(
context.Background(),
string(id),
types.ImageRemoveOptions{
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
}

@ -3,6 +3,7 @@ package container
import (
"time"
"github.com/containrrr/watchtower/internal/util"
"github.com/containrrr/watchtower/pkg/container/mocks"
"github.com/containrrr/watchtower/pkg/filters"
t "github.com/containrrr/watchtower/pkg/types"
@ -10,6 +11,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/backend"
cli "github.com/docker/docker/client"
"github.com/docker/docker/errdefs"
"github.com/onsi/gomega/gbytes"
"github.com/onsi/gomega/ghttp"
"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("no filter is provided", func() {
It("should return all available containers", func() {
@ -193,10 +226,8 @@ var _ = Describe("the client", func() {
}
// Capture logrus output in buffer
logbuf := gbytes.NewBuffer()
origOut := logrus.StandardLogger().Out
defer logrus.SetOutput(origOut)
logrus.SetOutput(logbuf)
resetLogrus, logbuf := captureLogrus(logrus.DebugLevel)
defer resetLogrus()
user := ""
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
func withContainerImageName(matcher gt.GomegaMatcher) gt.GomegaMatcher {

@ -7,6 +7,7 @@ import (
"net/http"
"net/url"
"path/filepath"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
@ -190,3 +191,29 @@ const (
Found FoundStatus = true
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