Session report collection and report templates (#981)

* wip: notification stats

* make report notifications optional

* linting/documentation fixes

* linting/documentation fixes

* merge types.Container and container.Interface

* smaller naming/format fixes

* use typed image/container IDs

* simplify notifier and update tests

* add missed doc comments

* lint fixes

* remove unused constructors

* rename old/new current/latest
This commit is contained in:
nils måsén 2021-06-27 09:05:01 +02:00 committed by GitHub
parent d0ecc23d72
commit e3dd8d688a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 853 additions and 598 deletions

View file

@ -26,13 +26,13 @@ const defaultStopSignal = "SIGTERM"
// Docker API.
type Client interface {
ListContainers(t.Filter) ([]Container, error)
GetContainer(containerID string) (Container, error)
GetContainer(containerID t.ContainerID) (Container, error)
StopContainer(Container, time.Duration) error
StartContainer(Container) (string, error)
StartContainer(Container) (t.ContainerID, error)
RenameContainer(Container, string) error
IsContainerStale(Container) (bool, error)
ExecuteCommand(containerID string, command string, timeout int) (SkipUpdate bool, err error)
RemoveImageByID(string) error
IsContainerStale(Container) (stale bool, latestImage t.ImageID, err error)
ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error)
RemoveImageByID(t.ImageID) error
WarnOnHeadPullFailed(container Container) bool
}
@ -108,7 +108,7 @@ func (client dockerClient) ListContainers(fn t.Filter) ([]Container, error) {
for _, runningContainer := range containers {
c, err := client.GetContainer(runningContainer.ID)
c, err := client.GetContainer(t.ContainerID(runningContainer.ID))
if err != nil {
return nil, err
}
@ -137,10 +137,10 @@ func (client dockerClient) createListFilter() filters.Args {
return filterArgs
}
func (client dockerClient) GetContainer(containerID string) (Container, error) {
func (client dockerClient) GetContainer(containerID t.ContainerID) (Container, error) {
bg := context.Background()
containerInfo, err := client.api.ContainerInspect(bg, containerID)
containerInfo, err := client.api.ContainerInspect(bg, string(containerID))
if err != nil {
return Container{}, err
}
@ -161,11 +161,12 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
signal = defaultStopSignal
}
shortID := ShortID(c.ID())
idStr := string(c.ID())
shortID := c.ID().ShortID()
if c.IsRunning() {
log.Infof("Stopping %s (%s) with %s", c.Name(), shortID, signal)
if err := client.api.ContainerKill(bg, c.ID(), signal); err != nil {
if err := client.api.ContainerKill(bg, idStr, signal); err != nil {
return err
}
}
@ -178,7 +179,7 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
} else {
log.Debugf("Removing container %s", shortID)
if err := client.api.ContainerRemove(bg, c.ID(), types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.removeVolumes}); err != nil {
if err := client.api.ContainerRemove(bg, idStr, types.ContainerRemoveOptions{Force: true, RemoveVolumes: client.removeVolumes}); err != nil {
return err
}
}
@ -191,7 +192,7 @@ func (client dockerClient) StopContainer(c Container, timeout time.Duration) err
return nil
}
func (client dockerClient) StartContainer(c Container) (string, error) {
func (client dockerClient) StartContainer(c Container) (t.ContainerID, error) {
bg := context.Background()
config := c.runtimeConfig()
hostConfig := c.hostConfig()
@ -234,18 +235,19 @@ func (client dockerClient) StartContainer(c Container) (string, error) {
}
createdContainerID := t.ContainerID(createdContainer.ID)
if !c.IsRunning() && !client.reviveStopped {
return createdContainer.ID, nil
return createdContainerID, nil
}
return createdContainer.ID, client.doStartContainer(bg, c, createdContainer)
return createdContainerID, client.doStartContainer(bg, c, createdContainer)
}
func (client dockerClient) doStartContainer(bg context.Context, c Container, creation container.ContainerCreateCreatedBody) error {
name := c.Name()
log.Debugf("Starting container %s (%s)", name, ShortID(creation.ID))
log.Debugf("Starting container %s (%s)", name, t.ContainerID(creation.ID).ShortID())
err := client.api.ContainerStart(bg, creation.ID, types.ContainerStartOptions{})
if err != nil {
return err
@ -255,38 +257,39 @@ func (client dockerClient) doStartContainer(bg context.Context, c Container, cre
func (client dockerClient) RenameContainer(c Container, newName string) error {
bg := context.Background()
log.Debugf("Renaming container %s (%s) to %s", c.Name(), ShortID(c.ID()), newName)
return client.api.ContainerRename(bg, c.ID(), newName)
log.Debugf("Renaming container %s (%s) to %s", c.Name(), c.ID().ShortID(), newName)
return client.api.ContainerRename(bg, string(c.ID()), newName)
}
func (client dockerClient) IsContainerStale(container Container) (bool, error) {
func (client dockerClient) IsContainerStale(container Container) (stale bool, latestImage t.ImageID, err error) {
ctx := context.Background()
if !client.pullImages {
log.Debugf("Skipping image pull.")
} else if err := client.PullImage(ctx, container); err != nil {
return false, err
return false, container.SafeImageID(), err
}
return client.HasNewImage(ctx, container)
}
func (client dockerClient) HasNewImage(ctx context.Context, container Container) (bool, error) {
oldImageID := container.containerInfo.ContainerJSONBase.Image
func (client dockerClient) HasNewImage(ctx context.Context, container Container) (hasNew bool, latestImage t.ImageID, err error) {
currentImageID := t.ImageID(container.containerInfo.ContainerJSONBase.Image)
imageName := container.ImageName()
newImageInfo, _, err := client.api.ImageInspectWithRaw(ctx, imageName)
if err != nil {
return false, err
return false, currentImageID, err
}
if newImageInfo.ID == oldImageID {
newImageID := t.ImageID(newImageInfo.ID)
if newImageID == currentImageID {
log.Debugf("No new images found for %s", container.Name())
return false, nil
return false, currentImageID, nil
}
log.Infof("Found new %s image (%s)", imageName, ShortID(newImageInfo.ID))
return true, nil
log.Infof("Found new %s image (%s)", imageName, newImageID.ShortID())
return true, newImageID, nil
}
// PullImage pulls the latest image for the supplied container, optionally skipping if it's digest can be confirmed
@ -343,12 +346,12 @@ func (client dockerClient) PullImage(ctx context.Context, container Container) e
return nil
}
func (client dockerClient) RemoveImageByID(id string) error {
log.Infof("Removing image %s", ShortID(id))
func (client dockerClient) RemoveImageByID(id t.ImageID) error {
log.Infof("Removing image %s", id.ShortID())
_, err := client.api.ImageRemove(
context.Background(),
id,
string(id),
types.ImageRemoveOptions{
Force: true,
})
@ -356,7 +359,7 @@ func (client dockerClient) RemoveImageByID(id string) error {
return err
}
func (client dockerClient) ExecuteCommand(containerID string, command string, timeout int) (SkipUpdate bool, err error) {
func (client dockerClient) ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error) {
bg := context.Background()
// Create the exec
@ -366,7 +369,7 @@ func (client dockerClient) ExecuteCommand(containerID string, command string, ti
Cmd: []string{"sh", "-c", command},
}
exec, err := client.api.ContainerExecCreate(bg, containerID, execConfig)
exec, err := client.api.ContainerExecCreate(bg, string(containerID), execConfig)
if err != nil {
return false, err
}
@ -462,7 +465,7 @@ func (client dockerClient) waitForStopOrTimeout(c Container, waitTime time.Durat
case <-timeout:
return nil
default:
if ci, err := client.api.ContainerInspect(bg, c.ID()); err != nil {
if ci, err := client.api.ContainerInspect(bg, string(c.ID())); err != nil {
return err
} else if !ci.State.Running {
return nil

View file

@ -6,6 +6,7 @@ import (
"strings"
"github.com/containrrr/watchtower/internal/util"
wt "github.com/containrrr/watchtower/pkg/types"
"github.com/docker/docker/api/types"
dockercontainer "github.com/docker/docker/api/types/container"
@ -35,8 +36,8 @@ func (c Container) ContainerInfo() *types.ContainerJSON {
}
// ID returns the Docker container ID.
func (c Container) ID() string {
return c.containerInfo.ID
func (c Container) ID() wt.ContainerID {
return wt.ContainerID(c.containerInfo.ID)
}
// IsRunning returns a boolean flag indicating whether or not the current
@ -59,9 +60,18 @@ func (c Container) Name() string {
}
// ImageID returns the ID of the Docker image that was used to start the
// container.
func (c Container) ImageID() string {
return c.imageInfo.ID
// container. May cause nil dereference if imageInfo is not set!
func (c Container) ImageID() wt.ImageID {
return wt.ImageID(c.imageInfo.ID)
}
// SafeImageID returns the ID of the Docker image that was used to start the container if available,
// otherwise returns an empty string
func (c Container) SafeImageID() wt.ImageID {
if c.imageInfo == nil {
return ""
}
return wt.ImageID(c.imageInfo.ID)
}
// ImageName returns the name of the Docker image that was used to start the

View file

@ -204,8 +204,8 @@ var _ = Describe("the container", func() {
It("should return its ID on calls to .ID()", func() {
id := c.ID()
Expect(id).To(Equal("container_id"))
Expect(id).NotTo(Equal("wrong-id"))
Expect(id).To(BeEquivalentTo("container_id"))
Expect(id).NotTo(BeEquivalentTo("wrong-id"))
})
It("should return true, true if enabled on calls to .Enabled()", func() {
enabled, exists := c.Enabled()

View file

@ -25,13 +25,13 @@ func NewMockAPIServer() *httptest.Server {
Filters := r.URL.Query().Get("filters")
var result map[string]interface{}
json.Unmarshal([]byte(Filters), &result)
_ = json.Unmarshal([]byte(Filters), &result)
status := result["status"].(map[string]interface{})
response = getMockJSONFromDisk("./mocks/data/containers.json")
var x2 []types.Container
var containers []types.Container
json.Unmarshal([]byte(response), &containers)
_ = json.Unmarshal([]byte(response), &containers)
for _, v := range containers {
for key := range status {
if v.State == key {
@ -56,7 +56,7 @@ func NewMockAPIServer() *httptest.Server {
} else if isRequestFor("sha256:4dbc5f9c07028a985e14d1393e849ea07f68804c4293050d5a641b138db72daa", r) {
response = getMockJSONFromDisk("./mocks/data/image02.json")
}
fmt.Fprintln(w, response)
_, _ = fmt.Fprintln(w, response)
},
))
}
@ -67,10 +67,9 @@ func isRequestFor(urlPart string, r *http.Request) bool {
func getMockJSONFromDisk(relPath string) string {
absPath, _ := filepath.Abs(relPath)
logrus.Error(absPath)
buf, err := ioutil.ReadFile(absPath)
if err != nil {
logrus.Error(err)
logrus.WithError(err).WithField("file", absPath).Error(err)
return ""
}
return string(buf)

View file

@ -1,23 +0,0 @@
package container
import "strings"
// ShortID returns the 12-character (hex) short version of an image ID hash, removing any "sha256:" prefix if present
func ShortID(imageID string) (short string) {
prefixSep := strings.IndexRune(imageID, ':')
offset := 0
length := 12
if prefixSep >= 0 {
if imageID[0:prefixSep] == "sha256" {
offset = prefixSep + 1
} else {
length += prefixSep + 1
}
}
if len(imageID) >= offset+length {
return imageID[offset : offset+length]
}
return imageID
}

View file

@ -1,10 +1,9 @@
package container_test
import (
wt "github.com/containrrr/watchtower/pkg/types"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
. "github.com/containrrr/watchtower/pkg/container"
)
var _ = Describe("container utils", func() {
@ -12,13 +11,13 @@ var _ = Describe("container utils", func() {
When("given a normal image ID", func() {
When("it contains a sha256 prefix", func() {
It("should return that ID in short version", func() {
actual := ShortID("sha256:0123456789abcd00000000001111111111222222222233333333334444444444")
actual := shortID("sha256:0123456789abcd00000000001111111111222222222233333333334444444444")
Expect(actual).To(Equal("0123456789ab"))
})
})
When("it doesn't contain a prefix", func() {
It("should return that ID in short version", func() {
actual := ShortID("0123456789abcd00000000001111111111222222222233333333334444444444")
actual := shortID("0123456789abcd00000000001111111111222222222233333333334444444444")
Expect(actual).To(Equal("0123456789ab"))
})
})
@ -26,21 +25,26 @@ var _ = Describe("container utils", func() {
When("given a short image ID", func() {
When("it contains no prefix", func() {
It("should return the same string", func() {
Expect(ShortID("0123456789ab")).To(Equal("0123456789ab"))
Expect(shortID("0123456789ab")).To(Equal("0123456789ab"))
})
})
When("it contains a the sha256 prefix", func() {
It("should return the ID without the prefix", func() {
Expect(ShortID("sha256:0123456789ab")).To(Equal("0123456789ab"))
Expect(shortID("sha256:0123456789ab")).To(Equal("0123456789ab"))
})
})
})
When("given an ID with an unknown prefix", func() {
It("should return a short version of that ID including the prefix", func() {
Expect(ShortID("md5:0123456789ab")).To(Equal("md5:0123456789ab"))
Expect(ShortID("md5:0123456789abcdefg")).To(Equal("md5:0123456789ab"))
Expect(ShortID("md5:01")).To(Equal("md5:01"))
Expect(shortID("md5:0123456789ab")).To(Equal("md5:0123456789ab"))
Expect(shortID("md5:0123456789abcdefg")).To(Equal("md5:0123456789ab"))
Expect(shortID("md5:01")).To(Equal("md5:01"))
})
})
})
})
func shortID(id string) string {
// Proxy to the types implementation, relocated due to package dependency resolution
return wt.ImageID(id).ShortID()
}