From 77a46ab3bdc6dd9c86d64961d9377bd353c9ce87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sat, 12 Aug 2023 20:49:56 +0200 Subject: [PATCH] add support for opencontainers meta labels --- internal/actions/mocks/client.go | 7 ++++ internal/actions/update.go | 7 ++-- pkg/container/client.go | 9 ++++++ pkg/session/container_status.go | 14 +++++++- pkg/session/image_meta.go | 55 ++++++++++++++++++++++++++++++++ pkg/session/progress.go | 22 +++++++++++-- pkg/types/image_meta.go | 14 ++++++++ pkg/types/report.go | 2 ++ 8 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 pkg/session/image_meta.go create mode 100644 pkg/types/image_meta.go diff --git a/internal/actions/mocks/client.go b/internal/actions/mocks/client.go index 7b4162a..704bb36 100644 --- a/internal/actions/mocks/client.go +++ b/internal/actions/mocks/client.go @@ -5,6 +5,8 @@ import ( "fmt" "time" + dockerTypes "github.com/docker/docker/api/types" + t "github.com/containrrr/watchtower/pkg/types" ) @@ -66,6 +68,11 @@ func (client MockClient) RemoveImageByID(_ t.ImageID) error { return nil } +// GetImage is a mock method +func (client MockClient) GetImage(_ t.ImageID) (dockerTypes.ImageInspect, error) { + return dockerTypes.ImageInspect{}, nil +} + // GetContainer is a mock method func (client MockClient) GetContainer(_ t.ContainerID) (t.Container, error) { return client.TestData.Containers[0], nil diff --git a/internal/actions/update.go b/internal/actions/update.go index 9c97f27..f139833 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -33,7 +33,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e staleCheckFailed := 0 for i, targetContainer := range containers { - stale, newestImage, err := client.IsContainerStale(targetContainer) + stale, newestImageID, err := client.IsContainerStale(targetContainer) shouldUpdate := stale && !params.NoRestart && !params.MonitorOnly && !targetContainer.IsMonitorOnly() if err == nil && shouldUpdate { // Check to make sure we have all the necessary information for recreating the container @@ -55,12 +55,15 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e staleCheckFailed++ progress.AddSkipped(targetContainer, err) } else { - progress.AddScanned(targetContainer, newestImage) + progress.AddScanned(targetContainer, newestImageID) } containers[i].SetStale(stale) if stale { staleCount++ + if latestImage, err := client.GetImage(newestImageID); err == nil { + progress.UpdateLatestImage(targetContainer.ID(), latestImage) + } } } diff --git a/pkg/container/client.go b/pkg/container/client.go index 14ca237..02a7baf 100644 --- a/pkg/container/client.go +++ b/pkg/container/client.go @@ -34,6 +34,7 @@ type Client interface { ExecuteCommand(containerID t.ContainerID, command string, timeout int) (SkipUpdate bool, err error) RemoveImageByID(t.ImageID) error WarnOnHeadPullFailed(container t.Container) bool + GetImage(imageID t.ImageID) (types.ImageInspect, error) } // NewClient returns a new Client instance which can be used to interact with @@ -397,6 +398,14 @@ func (client dockerClient) PullImage(ctx context.Context, container t.Container) return nil } +func (client dockerClient) GetImage(id t.ImageID) (types.ImageInspect, error) { + imageInfo, _, err := client.api.ImageInspectWithRaw(context.Background(), string(id)) + if err != nil { + return types.ImageInspect{}, err + } + return imageInfo, nil +} + func (client dockerClient) RemoveImageByID(id t.ImageID) error { log.Infof("Removing image %s", id.ShortID()) diff --git a/pkg/session/container_status.go b/pkg/session/container_status.go index 8313da1..cd5d8f6 100644 --- a/pkg/session/container_status.go +++ b/pkg/session/container_status.go @@ -25,7 +25,9 @@ type ContainerStatus struct { containerName string imageName string error - state State + state State + beforeMeta imageMeta + afterMeta imageMeta } // ID returns the container ID @@ -80,3 +82,13 @@ func (u *ContainerStatus) State() string { return "Unknown" } } + +// Before returns the metadata for the image considered latest before the session +func (u *ContainerStatus) Before() wt.ImageMeta { + return u.beforeMeta +} + +// After returns the metadata for the image considered latest after the session +func (u *ContainerStatus) After() wt.ImageMeta { + return u.afterMeta +} diff --git a/pkg/session/image_meta.go b/pkg/session/image_meta.go new file mode 100644 index 0000000..3d12a2a --- /dev/null +++ b/pkg/session/image_meta.go @@ -0,0 +1,55 @@ +package session + +import "strings" + +type imageMeta map[string]string + +func imageMetaFromLabels(labels map[string]string) imageMeta { + im := make(imageMeta) + for key, value := range labels { + if suffix, found := strings.CutPrefix(key, "org.opencontainers.image."); found { + im[suffix] = value + } + } + return im +} + +func (im imageMeta) Authors() string { + return im["authors"] +} + +func (im imageMeta) Created() string { + return im["created"] +} + +func (im imageMeta) Description() string { + return im["description"] +} + +func (im imageMeta) Documentation() string { + return im["documentation"] +} + +func (im imageMeta) Licenses() string { + return im["licenses"] +} + +func (im imageMeta) Revision() string { + return im["revision"] +} + +func (im imageMeta) Source() string { + return im["source"] +} + +func (im imageMeta) Title() string { + return im["title"] +} + +func (im imageMeta) Url() string { + return im["url"] +} + +func (im imageMeta) Version() string { + return im["version"] +} diff --git a/pkg/session/progress.go b/pkg/session/progress.go index 57069be..1fb3ce7 100644 --- a/pkg/session/progress.go +++ b/pkg/session/progress.go @@ -2,6 +2,7 @@ package session import ( "github.com/containrrr/watchtower/pkg/types" + dockerTypes "github.com/docker/docker/api/types" ) // Progress contains the current session container status @@ -9,6 +10,14 @@ type Progress map[types.ContainerID]*ContainerStatus // UpdateFromContainer sets various status fields from their corresponding container equivalents func UpdateFromContainer(cont types.Container, newImage types.ImageID, state State) *ContainerStatus { + + var beforeMeta imageMeta + if imageInfo := cont.ImageInfo(); imageInfo != nil && imageInfo.Config != nil { + beforeMeta = imageMetaFromLabels(imageInfo.Config.Labels) + } else { + beforeMeta = make(imageMeta) + } + return &ContainerStatus{ containerID: cont.ID(), containerName: cont.Name(), @@ -16,6 +25,8 @@ func UpdateFromContainer(cont types.Container, newImage types.ImageID, state Sta oldImage: cont.SafeImageID(), newImage: newImage, state: state, + beforeMeta: beforeMeta, + afterMeta: beforeMeta, } } @@ -27,8 +38,9 @@ func (m Progress) AddSkipped(cont types.Container, err error) { } // AddScanned adds a container to the Progress with the state set as scanned -func (m Progress) AddScanned(cont types.Container, newImage types.ImageID) { - m.Add(UpdateFromContainer(cont, newImage, ScannedState)) +func (m Progress) AddScanned(cont types.Container, newImageID types.ImageID) { + m.Add(UpdateFromContainer(cont, newImageID, ScannedState)) + } // UpdateFailed updates the containers passed, setting their state as failed with the supplied error @@ -54,3 +66,9 @@ func (m Progress) MarkForUpdate(containerID types.ContainerID) { func (m Progress) Report() types.Report { return NewReport(m) } + +func (m Progress) UpdateLatestImage(containerID types.ContainerID, image dockerTypes.ImageInspect) { + if image.Config != nil { + m[containerID].afterMeta = imageMetaFromLabels(image.Config.Labels) + } +} diff --git a/pkg/types/image_meta.go b/pkg/types/image_meta.go new file mode 100644 index 0000000..332bccd --- /dev/null +++ b/pkg/types/image_meta.go @@ -0,0 +1,14 @@ +package types + +type ImageMeta interface { + Authors() string + Created() string + Description() string + Documentation() string + Licenses() string + Revision() string + Source() string + Title() string + Url() string + Version() string +} diff --git a/pkg/types/report.go b/pkg/types/report.go index f454fc6..a15c80d 100644 --- a/pkg/types/report.go +++ b/pkg/types/report.go @@ -20,4 +20,6 @@ type ContainerReport interface { ImageName() string Error() string State() string + Before() ImageMeta + After() ImageMeta }