Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Session report collection and report templates #981

Merged
merged 13 commits into from
Jun 27, 2021
5 changes: 3 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,11 +338,12 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
LifecycleHooks: lifecycleHooks,
RollingRestart: rollingRestart,
}
metricResults, err := actions.Update(client, updateParams)
result, err := actions.Update(client, updateParams)
if err != nil {
log.Error(err)
}
notifier.SendNotification()
notifier.SendNotification(result)
metricResults := metrics.NewMetric(result)
log.Debugf("Session done: %v scanned, %v updated, %v failed",
metricResults.Scanned, metricResults.Updated, metricResults.Failed)
return metricResults
Expand Down
20 changes: 10 additions & 10 deletions internal/actions/mocks/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,50 +40,50 @@ func CreateMockClient(data *TestData, api cli.CommonAPIClient, pullImages bool,
}

// ListContainers is a mock method returning the provided container testdata
func (client MockClient) ListContainers(f t.Filter) ([]container.Container, error) {
func (client MockClient) ListContainers(_ t.Filter) ([]container.Container, error) {
return client.TestData.Containers, nil
}

// StopContainer is a mock method
func (client MockClient) StopContainer(c container.Container, d time.Duration) error {
func (client MockClient) StopContainer(c container.Container, _ time.Duration) error {
if c.Name() == client.TestData.NameOfContainerToKeep {
return errors.New("tried to stop the instance we want to keep")
}
return nil
}

// StartContainer is a mock method
func (client MockClient) StartContainer(c container.Container) (string, error) {
func (client MockClient) StartContainer(_ container.Container) (string, error) {
return "", nil
}

// RenameContainer is a mock method
func (client MockClient) RenameContainer(c container.Container, s string) error {
func (client MockClient) RenameContainer(_ container.Container, _ string) error {
return nil
}

// RemoveImageByID increments the TriedToRemoveImageCount on being called
func (client MockClient) RemoveImageByID(id string) error {
func (client MockClient) RemoveImageByID(_ string) error {
client.TestData.TriedToRemoveImageCount++
return nil
}

// GetContainer is a mock method
func (client MockClient) GetContainer(containerID string) (container.Container, error) {
func (client MockClient) GetContainer(_ string) (container.Container, error) {
return container.Container{}, nil
}

// ExecuteCommand is a mock method
func (client MockClient) ExecuteCommand(containerID string, command string, timeout int) error {
func (client MockClient) ExecuteCommand(_ string, _ string, _ int) error {
return nil
}

// IsContainerStale is always true for the mock client
func (client MockClient) IsContainerStale(c container.Container) (bool, error) {
return true, nil
func (client MockClient) IsContainerStale(_ container.Container) (bool, string, error) {
return true, "", nil
}

// WarnOnHeadPullFailed is always true for the mock client
func (client MockClient) WarnOnHeadPullFailed(c container.Container) bool {
func (client MockClient) WarnOnHeadPullFailed(_ container.Container) bool {
return true
}
54 changes: 26 additions & 28 deletions internal/actions/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"github.com/containrrr/watchtower/internal/util"
"github.com/containrrr/watchtower/pkg/container"
"github.com/containrrr/watchtower/pkg/lifecycle"
metrics2 "github.com/containrrr/watchtower/pkg/metrics"
"github.com/containrrr/watchtower/pkg/session"
"github.com/containrrr/watchtower/pkg/sorter"
"github.com/containrrr/watchtower/pkg/types"
log "github.com/sirupsen/logrus"
Expand All @@ -14,9 +14,9 @@ import (
// used to start those containers have been updated. If a change is detected in
// any of the images, the associated containers are stopped and restarted with
// the new image.
func Update(client container.Client, params types.UpdateParams) (*metrics2.Metric, error) {
func Update(client container.Client, params types.UpdateParams) (types.Report, error) {
log.Debug("Checking containers for updated images")
metric := &metrics2.Metric{}
progress := &session.Progress{}
staleCount := 0

if params.LifecycleHooks {
Expand All @@ -31,7 +31,7 @@ func Update(client container.Client, params types.UpdateParams) (*metrics2.Metri
staleCheckFailed := 0

for i, targetContainer := range containers {
stale, err := client.IsContainerStale(targetContainer)
stale, newestImage, 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
Expand All @@ -51,7 +51,9 @@ func Update(client container.Client, params types.UpdateParams) (*metrics2.Metri
log.Infof("Unable to update container %q: %v. Proceeding to next.", targetContainer.Name(), err)
stale = false
staleCheckFailed++
metric.Failed++
progress.AddSkipped(targetContainer, err)
} else {
progress.AddScanned(targetContainer, newestImage)
}
containers[i].Stale = stale

Expand All @@ -61,8 +63,6 @@ func Update(client container.Client, params types.UpdateParams) (*metrics2.Metri
}

containers, err = sorter.SortByDependencies(containers)

metric.Scanned = len(containers)
if err != nil {
return nil, err
}
Expand All @@ -74,36 +74,35 @@ func Update(client container.Client, params types.UpdateParams) (*metrics2.Metri
for _, c := range containers {
if !c.IsMonitorOnly() {
containersToUpdate = append(containersToUpdate, c)
progress.MarkForUpdate(c.ID())
}
}
}

if params.RollingRestart {
metric.Failed += performRollingRestart(containersToUpdate, client, params)
progress.UpdateFailed(performRollingRestart(containersToUpdate, client, params))
} else {
metric.Failed += stopContainersInReversedOrder(containersToUpdate, client, params)
metric.Failed += restartContainersInSortedOrder(containersToUpdate, client, params)
progress.UpdateFailed(stopContainersInReversedOrder(containersToUpdate, client, params))
progress.UpdateFailed(restartContainersInSortedOrder(containersToUpdate, client, params))
}

metric.Updated = staleCount - (metric.Failed - staleCheckFailed)

if params.LifecycleHooks {
lifecycle.ExecutePostChecks(client, params)
}
return metric, nil
return progress.Report(), nil
}

func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) int {
cleanupImageIDs := make(map[string]bool)
failed := 0
func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) map[string]error {
cleanupImageIDs := make(map[string]bool, len(containers))
failed := make(map[string]error, len(containers))

for i := len(containers) - 1; i >= 0; i-- {
if containers[i].ToRestart() {
if err := stopStaleContainer(containers[i], client, params); err != nil {
failed++
failed[containers[i].ID()] = err
}
if err := restartStaleContainer(containers[i], client, params); err != nil {
failed++
failed[containers[i].ID()] = err
}
cleanupImageIDs[containers[i].ImageID()] = true
}
Expand All @@ -115,11 +114,11 @@ func performRollingRestart(containers []container.Container, client container.Cl
return failed
}

func stopContainersInReversedOrder(containers []container.Container, client container.Client, params types.UpdateParams) int {
failed := 0
func stopContainersInReversedOrder(containers []container.Container, client container.Client, params types.UpdateParams) map[string]error {
failed := make(map[string]error, len(containers))
for i := len(containers) - 1; i >= 0; i-- {
if err := stopStaleContainer(containers[i], client, params); err != nil {
failed++
failed[containers[i].ID()] = err
}
}
return failed
Expand Down Expand Up @@ -149,23 +148,22 @@ func stopStaleContainer(container container.Container, client container.Client,
return nil
}

func restartContainersInSortedOrder(containers []container.Container, client container.Client, params types.UpdateParams) int {
imageIDs := make(map[string]bool)

failed := 0
func restartContainersInSortedOrder(containers []container.Container, client container.Client, params types.UpdateParams) map[string]error {
cleanupImageIDs := make(map[string]bool, len(containers))
failed := make(map[string]error, len(containers))

for _, c := range containers {
if !c.ToRestart() {
continue
}
if err := restartStaleContainer(c, client, params); err != nil {
failed++
failed[c.ID()] = err
}
imageIDs[c.ImageID()] = true
cleanupImageIDs[c.ImageID()] = true
}

if params.Cleanup {
cleanupImages(client, imageIDs)
cleanupImages(client, cleanupImageIDs)
}

return failed
Expand Down
13 changes: 7 additions & 6 deletions internal/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,8 @@ func RegisterNotificationFlags(rootCmd *cobra.Command) {
viper.GetStringSlice("WATCHTOWER_NOTIFICATIONS"),
" notification types to send (valid: email, slack, msteams, gotify, shoutrrr)")

flags.StringP(
flags.String(
"notifications-level",
"",
viper.GetString("WATCHTOWER_NOTIFICATIONS_LEVEL"),
"The log level used for sending notifications. Possible values: panic, fatal, error, warn, info or debug")

Expand Down Expand Up @@ -301,18 +300,20 @@ Should only be used for testing.`)
`Controls whether watchtower verifies the Gotify server's certificate chain and host name.
Should only be used for testing.`)

flags.StringP(
flags.String(
"notification-template",
"",
viper.GetString("WATCHTOWER_NOTIFICATION_TEMPLATE"),
"The shoutrrr text/template for the messages")

flags.StringArrayP(
flags.StringArray(
"notification-url",
"",
viper.GetStringSlice("WATCHTOWER_NOTIFICATION_URL"),
"The shoutrrr URL to send notifications to")

flags.Bool("notification-report",
viper.GetBool("WATCHTOWER_NOTIFICATION_REPORT"),
"Use the session report as the notification template data")

flags.String(
"warn-on-head-failure",
viper.GetString("WATCHTOWER_WARN_ON_HEAD_FAILURE"),
Expand Down
14 changes: 7 additions & 7 deletions pkg/container/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type Client interface {
StopContainer(Container, time.Duration) error
StartContainer(Container) (string, error)
RenameContainer(Container, string) error
IsContainerStale(Container) (bool, error)
IsContainerStale(Container) (stale bool, newestImage string, err error)
ExecuteCommand(containerID string, command string, timeout int) error
RemoveImageByID(string) error
WarnOnHeadPullFailed(container Container) bool
Expand Down Expand Up @@ -259,34 +259,34 @@ func (client dockerClient) RenameContainer(c Container, newName string) error {
return client.api.ContainerRename(bg, c.ID(), newName)
}

func (client dockerClient) IsContainerStale(container Container) (bool, error) {
func (client dockerClient) IsContainerStale(container Container) (stale bool, newestImage string, 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) {
func (client dockerClient) HasNewImage(ctx context.Context, container Container) (hasNew bool, newestImage string, err error) {
oldImageID := container.containerInfo.ContainerJSONBase.Image
imageName := container.ImageName()

newImageInfo, _, err := client.api.ImageInspectWithRaw(ctx, imageName)
if err != nil {
return false, err
return false, oldImageID, err
}

if newImageInfo.ID == oldImageID {
log.Debugf("No new images found for %s", container.Name())
return false, nil
return false, oldImageID, nil
}

log.Infof("Found new %s image (%s)", imageName, ShortID(newImageInfo.ID))
return true, nil
return true, newImageInfo.ID, nil
}

// PullImage pulls the latest image for the supplied container, optionally skipping if it's digest can be confirmed
Expand Down
11 changes: 10 additions & 1 deletion pkg/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,20 @@ func (c Container) Name() string {
}

// ImageID returns the ID of the Docker image that was used to start the
// container.
// container. May cause nil dereference if imageInfo is not set!
func (c Container) ImageID() string {
return 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() string {
if c.imageInfo == nil {
return ""
}
return c.imageInfo.ID
}

// ImageName returns the name of the Docker image that was used to start the
// container. If the original image was specified without a particular tag, the
// "latest" tag is assumed.
Expand Down
9 changes: 9 additions & 0 deletions pkg/container/interface.go.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package container

// Interface is the minimum common Container interface
type Interface interface {
ID() string
Name() string
SafeImageID() string
ImageName() string
}
piksel marked this conversation as resolved.
Show resolved Hide resolved
11 changes: 11 additions & 0 deletions pkg/metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package metrics

import (
"github.com/containrrr/watchtower/pkg/types"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
Expand All @@ -24,6 +25,16 @@ type Metrics struct {
skipped prometheus.Counter
}

// NewMetric returns a Metric with the counts taken from the appropriate types.Report fields
func NewMetric(report types.Report) *Metric {
return &Metric{
Scanned: len(report.Scanned()),
// Note: This is for backwards compatibility. ideally, stale containers should be counted separately
Updated: len(report.Updated()) + len(report.Stale()),
Failed: len(report.Failed()),
}
}

// QueueIsEmpty checks whether any messages are enqueued in the channel
func (metrics *Metrics) QueueIsEmpty() bool {
return len(metrics.channel) == 0
Expand Down
Loading