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/latestfeat/api-report
parent
d0ecc23d72
commit
e3dd8d688a
@ -0,0 +1,46 @@
|
|||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/containrrr/watchtower/pkg/session"
|
||||||
|
wt "github.com/containrrr/watchtower/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateMockProgressReport creates a mock report from a given set of container states
|
||||||
|
// All containers will be given a unique ID and name based on its state and index
|
||||||
|
func CreateMockProgressReport(states ...session.State) wt.Report {
|
||||||
|
|
||||||
|
stateNums := make(map[session.State]int)
|
||||||
|
progress := session.Progress{}
|
||||||
|
failed := make(map[wt.ContainerID]error)
|
||||||
|
|
||||||
|
for _, state := range states {
|
||||||
|
index := stateNums[state]
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case session.SkippedState:
|
||||||
|
c, _ := CreateContainerForProgress(index, 41, "skip%d")
|
||||||
|
progress.AddSkipped(c, errors.New("unpossible"))
|
||||||
|
break
|
||||||
|
case session.FreshState:
|
||||||
|
c, _ := CreateContainerForProgress(index, 31, "frsh%d")
|
||||||
|
progress.AddScanned(c, c.ImageID())
|
||||||
|
break
|
||||||
|
case session.UpdatedState:
|
||||||
|
c, newImage := CreateContainerForProgress(index, 11, "updt%d")
|
||||||
|
progress.AddScanned(c, newImage)
|
||||||
|
progress.MarkForUpdate(c.ID())
|
||||||
|
break
|
||||||
|
case session.FailedState:
|
||||||
|
c, newImage := CreateContainerForProgress(index, 21, "fail%d")
|
||||||
|
progress.AddScanned(c, newImage)
|
||||||
|
failed[c.ID()] = errors.New("accidentally the whole container")
|
||||||
|
}
|
||||||
|
|
||||||
|
stateNums[state] = index + 1
|
||||||
|
}
|
||||||
|
progress.UpdateFailed(failed)
|
||||||
|
|
||||||
|
return progress.Report()
|
||||||
|
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
@ -0,0 +1,13 @@
|
|||||||
|
package notifications_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNotifications(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "Notifications Suite")
|
||||||
|
}
|
@ -1,77 +0,0 @@
|
|||||||
// Package notifications ...
|
|
||||||
// Copyright 2010 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license.
|
|
||||||
package notifications
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
"net/smtp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SendMail connects to the server at addr, switches to TLS if
|
|
||||||
// possible, authenticates with the optional mechanism a if possible,
|
|
||||||
// and then sends an email from address from, to addresses to, with
|
|
||||||
// message msg.
|
|
||||||
// The addr must include a port, as in "mail.example.com:smtp".
|
|
||||||
//
|
|
||||||
// The addresses in the to parameter are the SMTP RCPT addresses.
|
|
||||||
//
|
|
||||||
// The msg parameter should be an RFC 822-style email with headers
|
|
||||||
// first, a blank line, and then the message body. The lines of msg
|
|
||||||
// should be CRLF terminated. The msg headers should usually include
|
|
||||||
// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc"
|
|
||||||
// messages is accomplished by including an email address in the to
|
|
||||||
// parameter but not including it in the msg headers.
|
|
||||||
//
|
|
||||||
// The SendMail function and the net/smtp package are low-level
|
|
||||||
// mechanisms and provide no support for DKIM signing, MIME
|
|
||||||
// attachments (see the mime/multipart package), or other mail
|
|
||||||
// functionality. Higher-level packages exist outside of the standard
|
|
||||||
// library.
|
|
||||||
func SendMail(addr string, insecureSkipVerify bool, a smtp.Auth, from string, to []string, msg []byte) error {
|
|
||||||
c, err := smtp.Dial(addr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer c.Close()
|
|
||||||
if err = c.Hello("localHost"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if ok, _ := c.Extension("STARTTLS"); ok {
|
|
||||||
serverName, _, _ := net.SplitHostPort(addr)
|
|
||||||
config := &tls.Config{ServerName: serverName, InsecureSkipVerify: insecureSkipVerify}
|
|
||||||
if err = c.StartTLS(config); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if a != nil {
|
|
||||||
if ok, _ := c.Extension("AUTH"); ok {
|
|
||||||
if err = c.Auth(a); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err = c.Mail(from); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, addr := range to {
|
|
||||||
if err = c.Rcpt(addr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w, err := c.Data()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = w.Write(msg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = w.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.Quit()
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
package notifications
|
|
||||||
|
|
||||||
import "bytes"
|
|
||||||
|
|
||||||
// SplitSubN splits a string into a list of string with each having
|
|
||||||
// a maximum number of characters n
|
|
||||||
func SplitSubN(s string, n int) []string {
|
|
||||||
sub := ""
|
|
||||||
subs := []string{}
|
|
||||||
|
|
||||||
runes := bytes.Runes([]byte(s))
|
|
||||||
l := len(runes)
|
|
||||||
for i, r := range runes {
|
|
||||||
sub = sub + string(r)
|
|
||||||
if (i+1)%n == 0 {
|
|
||||||
subs = append(subs, sub)
|
|
||||||
sub = ""
|
|
||||||
} else if (i + 1) == l {
|
|
||||||
subs = append(subs, sub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return subs
|
|
||||||
}
|
|
@ -0,0 +1,82 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import wt "github.com/containrrr/watchtower/pkg/types"
|
||||||
|
|
||||||
|
// State indicates what the current state is of the container
|
||||||
|
type State int
|
||||||
|
|
||||||
|
// State enum values
|
||||||
|
const (
|
||||||
|
// UnknownState is only used to represent an uninitialized State value
|
||||||
|
UnknownState State = iota
|
||||||
|
SkippedState
|
||||||
|
ScannedState
|
||||||
|
UpdatedState
|
||||||
|
FailedState
|
||||||
|
FreshState
|
||||||
|
StaleState
|
||||||
|
)
|
||||||
|
|
||||||
|
// ContainerStatus contains the container state during a session
|
||||||
|
type ContainerStatus struct {
|
||||||
|
containerID wt.ContainerID
|
||||||
|
oldImage wt.ImageID
|
||||||
|
newImage wt.ImageID
|
||||||
|
containerName string
|
||||||
|
imageName string
|
||||||
|
error
|
||||||
|
state State
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the container ID
|
||||||
|
func (u *ContainerStatus) ID() wt.ContainerID {
|
||||||
|
return u.containerID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the container name
|
||||||
|
func (u *ContainerStatus) Name() string {
|
||||||
|
return u.containerName
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentImageID returns the image ID that the container used when the session started
|
||||||
|
func (u *ContainerStatus) CurrentImageID() wt.ImageID {
|
||||||
|
return u.oldImage
|
||||||
|
}
|
||||||
|
|
||||||
|
// LatestImageID returns the newest image ID found during the session
|
||||||
|
func (u *ContainerStatus) LatestImageID() wt.ImageID {
|
||||||
|
return u.newImage
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageName returns the name:tag that the container uses
|
||||||
|
func (u *ContainerStatus) ImageName() string {
|
||||||
|
return u.imageName
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the error (if any) that was encountered for the container during a session
|
||||||
|
func (u *ContainerStatus) Error() string {
|
||||||
|
if u.error == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return u.error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// State returns the current State that the container is in
|
||||||
|
func (u *ContainerStatus) State() string {
|
||||||
|
switch u.state {
|
||||||
|
case SkippedState:
|
||||||
|
return "Skipped"
|
||||||
|
case ScannedState:
|
||||||
|
return "Scanned"
|
||||||
|
case UpdatedState:
|
||||||
|
return "Updated"
|
||||||
|
case FailedState:
|
||||||
|
return "Failed"
|
||||||
|
case FreshState:
|
||||||
|
return "Fresh"
|
||||||
|
case StaleState:
|
||||||
|
return "Stale"
|
||||||
|
default:
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Progress contains the current session container status
|
||||||
|
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 {
|
||||||
|
return &ContainerStatus{
|
||||||
|
containerID: cont.ID(),
|
||||||
|
containerName: cont.Name(),
|
||||||
|
imageName: cont.ImageName(),
|
||||||
|
oldImage: cont.SafeImageID(),
|
||||||
|
newImage: newImage,
|
||||||
|
state: state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSkipped adds a container to the Progress with the state set as skipped
|
||||||
|
func (m Progress) AddSkipped(cont types.Container, err error) {
|
||||||
|
update := UpdateFromContainer(cont, cont.SafeImageID(), SkippedState)
|
||||||
|
update.error = err
|
||||||
|
m.Add(update)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFailed updates the containers passed, setting their state as failed with the supplied error
|
||||||
|
func (m Progress) UpdateFailed(failures map[types.ContainerID]error) {
|
||||||
|
for id, err := range failures {
|
||||||
|
update := m[id]
|
||||||
|
update.error = err
|
||||||
|
update.state = FailedState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a container to the map using container ID as the key
|
||||||
|
func (m Progress) Add(update *ContainerStatus) {
|
||||||
|
m[update.containerID] = update
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkForUpdate marks the container identified by containerID for update
|
||||||
|
func (m Progress) MarkForUpdate(containerID types.ContainerID) {
|
||||||
|
m[containerID].state = UpdatedState
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report creates a new Report from a Progress instance
|
||||||
|
func (m Progress) Report() types.Report {
|
||||||
|
return NewReport(m)
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/containrrr/watchtower/pkg/types"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type report struct {
|
||||||
|
scanned []types.ContainerReport
|
||||||
|
updated []types.ContainerReport
|
||||||
|
failed []types.ContainerReport
|
||||||
|
skipped []types.ContainerReport
|
||||||
|
stale []types.ContainerReport
|
||||||
|
fresh []types.ContainerReport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *report) Scanned() []types.ContainerReport {
|
||||||
|
return r.scanned
|
||||||
|
}
|
||||||
|
func (r *report) Updated() []types.ContainerReport {
|
||||||
|
return r.updated
|
||||||
|
}
|
||||||
|
func (r *report) Failed() []types.ContainerReport {
|
||||||
|
return r.failed
|
||||||
|
}
|
||||||
|
func (r *report) Skipped() []types.ContainerReport {
|
||||||
|
return r.skipped
|
||||||
|
}
|
||||||
|
func (r *report) Stale() []types.ContainerReport {
|
||||||
|
return r.stale
|
||||||
|
}
|
||||||
|
func (r *report) Fresh() []types.ContainerReport {
|
||||||
|
return r.fresh
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReport creates a types.Report from the supplied Progress
|
||||||
|
func NewReport(progress Progress) types.Report {
|
||||||
|
report := &report{
|
||||||
|
scanned: []types.ContainerReport{},
|
||||||
|
updated: []types.ContainerReport{},
|
||||||
|
failed: []types.ContainerReport{},
|
||||||
|
skipped: []types.ContainerReport{},
|
||||||
|
stale: []types.ContainerReport{},
|
||||||
|
fresh: []types.ContainerReport{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, update := range progress {
|
||||||
|
if update.state == SkippedState {
|
||||||
|
report.skipped = append(report.skipped, update)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
report.scanned = append(report.scanned, update)
|
||||||
|
if update.newImage == update.oldImage {
|
||||||
|
update.state = FreshState
|
||||||
|
report.fresh = append(report.fresh, update)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch update.state {
|
||||||
|
case UpdatedState:
|
||||||
|
report.updated = append(report.updated, update)
|
||||||
|
case FailedState:
|
||||||
|
report.failed = append(report.failed, update)
|
||||||
|
default:
|
||||||
|
update.state = StaleState
|
||||||
|
report.stale = append(report.stale, update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(sortableContainers(report.scanned))
|
||||||
|
sort.Sort(sortableContainers(report.updated))
|
||||||
|
sort.Sort(sortableContainers(report.failed))
|
||||||
|
sort.Sort(sortableContainers(report.skipped))
|
||||||
|
sort.Sort(sortableContainers(report.stale))
|
||||||
|
sort.Sort(sortableContainers(report.fresh))
|
||||||
|
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
|
||||||
|
type sortableContainers []types.ContainerReport
|
||||||
|
|
||||||
|
// Len implements sort.Interface.Len
|
||||||
|
func (s sortableContainers) Len() int { return len(s) }
|
||||||
|
|
||||||
|
// Less implements sort.Interface.Less
|
||||||
|
func (s sortableContainers) Less(i, j int) bool { return s[i].ID() < s[j].ID() }
|
||||||
|
|
||||||
|
// Swap implements sort.Interface.Swap
|
||||||
|
func (s sortableContainers) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
@ -0,0 +1,22 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// Report contains reports for all the containers processed during a session
|
||||||
|
type Report interface {
|
||||||
|
Scanned() []ContainerReport
|
||||||
|
Updated() []ContainerReport
|
||||||
|
Failed() []ContainerReport
|
||||||
|
Skipped() []ContainerReport
|
||||||
|
Stale() []ContainerReport
|
||||||
|
Fresh() []ContainerReport
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerReport represents a container that was included in watchtower session
|
||||||
|
type ContainerReport interface {
|
||||||
|
ID() ContainerID
|
||||||
|
Name() string
|
||||||
|
CurrentImageID() ImageID
|
||||||
|
LatestImageID() ImageID
|
||||||
|
ImageName() string
|
||||||
|
Error() string
|
||||||
|
State() string
|
||||||
|
}
|
Loading…
Reference in New Issue