mirror of
https://github.com/containrrr/watchtower.git
synced 2025-12-13 21:56:38 +01:00
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:
parent
d0ecc23d72
commit
e3dd8d688a
32 changed files with 853 additions and 598 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue