http report wip

feat/api-report
nils måsén 3 years ago
parent e3dd8d688a
commit efaf7190ee

@ -2,6 +2,7 @@ package cmd
import ( import (
"github.com/containrrr/watchtower/internal/meta" "github.com/containrrr/watchtower/internal/meta"
"github.com/containrrr/watchtower/pkg/session"
"math" "math"
"os" "os"
"os/signal" "os/signal"
@ -10,9 +11,6 @@ import (
"syscall" "syscall"
"time" "time"
apiMetrics "github.com/containrrr/watchtower/pkg/api/metrics"
"github.com/containrrr/watchtower/pkg/api/update"
"github.com/containrrr/watchtower/internal/actions" "github.com/containrrr/watchtower/internal/actions"
"github.com/containrrr/watchtower/internal/flags" "github.com/containrrr/watchtower/internal/flags"
"github.com/containrrr/watchtower/pkg/api" "github.com/containrrr/watchtower/pkg/api"
@ -34,12 +32,12 @@ var (
noRestart bool noRestart bool
monitorOnly bool monitorOnly bool
enableLabel bool enableLabel bool
notifier t.Notifier notifier notifications.Notifier
timeout time.Duration timeout time.Duration
lifecycleHooks bool lifecycleHooks bool
rollingRestart bool rollingRestart bool
scope string scope string
// Set on build using ldflags latestReport *session.Report
) )
var rootCmd = NewRootCommand() var rootCmd = NewRootCommand()
@ -156,6 +154,7 @@ func Run(c *cobra.Command, names []string) {
runOnce, _ := c.PersistentFlags().GetBool("run-once") runOnce, _ := c.PersistentFlags().GetBool("run-once")
enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update") enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update")
enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics") enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics")
enableReportAPI := true
unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls") unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls")
apiToken, _ := c.PersistentFlags().GetString("http-api-token") apiToken, _ := c.PersistentFlags().GetString("http-api-token")
@ -171,7 +170,7 @@ func Run(c *cobra.Command, names []string) {
if runOnce { if runOnce {
writeStartupMessage(c, time.Time{}, filterDesc) writeStartupMessage(c, time.Time{}, filterDesc)
runUpdatesWithNotifications(filter) runUpdatesWithNotifications(filter, session.StartupTrigger)
notifier.Close() notifier.Close()
os.Exit(0) os.Exit(0)
return return
@ -188,13 +187,17 @@ func Run(c *cobra.Command, names []string) {
httpAPI := api.New(apiToken) httpAPI := api.New(apiToken)
if enableUpdateAPI { if enableUpdateAPI {
updateHandler := update.New(func() { runUpdatesWithNotifications(filter) }, updateLock) httpAPI.RegisterFunc(api.UpdateEndpoint(func() *metrics.Metric {
httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle) return runUpdatesWithNotifications(filter, session.APITrigger)
}, updateLock))
} }
if enableMetricsAPI { if enableMetricsAPI {
metricsHandler := apiMetrics.New() httpAPI.RegisterHandler(api.MetricsEndpoint())
httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle) }
if enableReportAPI {
httpAPI.RegisterHandler(api.ReportEndpoint(&latestReport))
} }
if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil { if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil {
@ -293,7 +296,7 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string,
select { select {
case v := <-lock: case v := <-lock:
defer func() { lock <- v }() defer func() { lock <- v }()
metric := runUpdatesWithNotifications(filter) metric := runUpdatesWithNotifications(filter, session.SchedulerTrigger)
metrics.RegisterScan(metric) metrics.RegisterScan(metric)
default: default:
// Update was skipped // Update was skipped
@ -327,7 +330,7 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string,
return nil return nil
} }
func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric { func runUpdatesWithNotifications(filter t.Filter, trigger session.Trigger) *metrics.Metric {
notifier.StartNotification() notifier.StartNotification()
updateParams := t.UpdateParams{ updateParams := t.UpdateParams{
Filter: filter, Filter: filter,
@ -338,10 +341,11 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric {
LifecycleHooks: lifecycleHooks, LifecycleHooks: lifecycleHooks,
RollingRestart: rollingRestart, RollingRestart: rollingRestart,
} }
result, err := actions.Update(client, updateParams) result, err := actions.Update(client, updateParams, trigger)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
latestReport = result
notifier.SendNotification(result) notifier.SendNotification(result)
metricResults := metrics.NewMetric(result) metricResults := metrics.NewMetric(result)
log.Debugf("Session done: %v scanned, %v updated, %v failed", log.Debugf("Session done: %v scanned, %v updated, %v failed",

@ -8,10 +8,11 @@ import (
// CreateMockProgressReport creates a mock report from a given set of container states // 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 // All containers will be given a unique ID and name based on its state and index
func CreateMockProgressReport(states ...session.State) wt.Report { func CreateMockProgressReport(states ...session.State) *session.Report {
stateNums := make(map[session.State]int) stateNums := make(map[session.State]int)
progress := session.Progress{} mockSession := session.New(session.SchedulerTrigger)
progress := mockSession.Progress
failed := make(map[wt.ContainerID]error) failed := make(map[wt.ContainerID]error)
for _, state := range states { for _, state := range states {
@ -41,6 +42,6 @@ func CreateMockProgressReport(states ...session.State) wt.Report {
} }
progress.UpdateFailed(failed) progress.UpdateFailed(failed)
return progress.Report() return mockSession.Report()
} }

@ -15,9 +15,10 @@ import (
// used to start those containers have been updated. If a change is detected in // 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 // any of the images, the associated containers are stopped and restarted with
// the new image. // the new image.
func Update(client container.Client, params types.UpdateParams) (types.Report, error) { func Update(client container.Client, params types.UpdateParams, trigger session.Trigger) (*session.Report, error) {
log.Debug("Checking containers for updated images") log.Debug("Checking containers for updated images")
progress := &session.Progress{} updateSession := session.New(trigger)
progress := updateSession.Progress
staleCount := 0 staleCount := 0
if params.LifecycleHooks { if params.LifecycleHooks {
@ -92,7 +93,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e
if params.LifecycleHooks { if params.LifecycleHooks {
lifecycle.ExecutePostChecks(client, params) lifecycle.ExecutePostChecks(client, params)
} }
return progress.Report(), nil return updateSession.Report(), nil
} }
func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error { func performRollingRestart(containers []container.Container, client container.Client, params types.UpdateParams) map[types.ContainerID]error {

@ -4,6 +4,7 @@ import (
"github.com/containrrr/watchtower/internal/actions" "github.com/containrrr/watchtower/internal/actions"
"github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/container"
"github.com/containrrr/watchtower/pkg/container/mocks" "github.com/containrrr/watchtower/pkg/container/mocks"
"github.com/containrrr/watchtower/pkg/session"
"github.com/containrrr/watchtower/pkg/types" "github.com/containrrr/watchtower/pkg/types"
dockerContainer "github.com/docker/docker/api/types/container" dockerContainer "github.com/docker/docker/api/types/container"
cli "github.com/docker/docker/client" cli "github.com/docker/docker/client"
@ -15,6 +16,8 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
) )
var trigger = session.SchedulerTrigger
var _ = Describe("the update action", func() { var _ = Describe("the update action", func() {
var dockerClient cli.CommonAPIClient var dockerClient cli.CommonAPIClient
var client MockClient var client MockClient
@ -60,7 +63,7 @@ var _ = Describe("the update action", func() {
When("there are multiple containers using the same image", func() { When("there are multiple containers using the same image", func() {
It("should only try to remove the image once", func() { It("should only try to remove the image once", func() {
_, err := actions.Update(client, types.UpdateParams{Cleanup: true}) _, err := actions.Update(client, types.UpdateParams{Cleanup: true}, trigger)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
}) })
@ -76,7 +79,7 @@ var _ = Describe("the update action", func() {
time.Now(), time.Now(),
), ),
) )
_, err := actions.Update(client, types.UpdateParams{Cleanup: true}) _, err := actions.Update(client, types.UpdateParams{Cleanup: true}, trigger)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(2)) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(2))
}) })
@ -84,7 +87,7 @@ var _ = Describe("the update action", func() {
When("performing a rolling restart update", func() { When("performing a rolling restart update", func() {
It("should try to remove the image once", func() { It("should try to remove the image once", func() {
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, RollingRestart: true}) _, err := actions.Update(client, types.UpdateParams{Cleanup: true, RollingRestart: true}, trigger)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
}) })
@ -124,7 +127,7 @@ var _ = Describe("the update action", func() {
}) })
It("should not update those containers", func() { It("should not update those containers", func() {
_, err := actions.Update(client, types.UpdateParams{Cleanup: true}) _, err := actions.Update(client, types.UpdateParams{Cleanup: true}, trigger)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
}) })
@ -154,7 +157,7 @@ var _ = Describe("the update action", func() {
}) })
It("should not update any containers", func() { It("should not update any containers", func() {
_, err := actions.Update(client, types.UpdateParams{MonitorOnly: true}) _, err := actions.Update(client, types.UpdateParams{MonitorOnly: true}, trigger)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0)) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
}) })
@ -193,7 +196,7 @@ var _ = Describe("the update action", func() {
}) })
It("should not update those containers", func() { It("should not update those containers", func() {
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}) _, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}, trigger)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0)) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
}) })
@ -229,7 +232,7 @@ var _ = Describe("the update action", func() {
}) })
It("should not update those containers", func() { It("should not update those containers", func() {
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}) _, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}, trigger)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0)) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(0))
}) })
@ -265,7 +268,7 @@ var _ = Describe("the update action", func() {
}) })
It("should update those containers", func() { It("should update those containers", func() {
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}) _, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}, trigger)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
}) })
@ -300,7 +303,7 @@ var _ = Describe("the update action", func() {
}) })
It("skip running preupdate", func() { It("skip running preupdate", func() {
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}) _, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}, trigger)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
}) })
@ -336,7 +339,7 @@ var _ = Describe("the update action", func() {
}) })
It("skip running preupdate", func() { It("skip running preupdate", func() {
_, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}) _, err := actions.Update(client, types.UpdateParams{Cleanup: true, LifecycleHooks: true}, trigger)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1)) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(1))
}) })

@ -18,7 +18,7 @@ func TestEnvConfig_Defaults(t *testing.T) {
err := EnvConfig(cmd) err := EnvConfig(cmd)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "unix:///var/run/docker.sock", os.Getenv("DOCKER_HOST")) // assert.Equal(t, "unix:///var/run/docker.sock", os.Getenv("DOCKER_HOST"))
assert.Equal(t, "", os.Getenv("DOCKER_TLS_VERIFY")) assert.Equal(t, "", os.Getenv("DOCKER_TLS_VERIFY"))
// Re-enable this test when we've moved to github actions. // Re-enable this test when we've moved to github actions.
// assert.Equal(t, DockerAPIMinVersion, os.Getenv("DOCKER_API_VERSION")) // assert.Equal(t, DockerAPIMinVersion, os.Getenv("DOCKER_API_VERSION"))

@ -2,6 +2,7 @@ package api
import ( import (
"fmt" "fmt"
"github.com/containrrr/watchtower/pkg/session"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"net/http" "net/http"
) )
@ -10,8 +11,9 @@ const tokenMissingMsg = "api token is empty or has not been set. exiting"
// API is the http server responsible for serving the HTTP API endpoints // API is the http server responsible for serving the HTTP API endpoints
type API struct { type API struct {
Token string Token string
hasHandlers bool hasHandlers bool
latestReport session.Report
} }
// New is a factory function creating a new API instance // New is a factory function creating a new API instance
@ -74,3 +76,7 @@ func runHTTPServer() {
log.Info("Serving HTTP") log.Info("Serving HTTP")
log.Fatal(http.ListenAndServe(":8080", nil)) log.Fatal(http.ListenAndServe(":8080", nil))
} }
func (api *API) UpdateReport(report session.Report) {
api.latestReport = report
}

@ -0,0 +1,13 @@
package api_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestAPI(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "API Suite")
}

@ -0,0 +1,23 @@
package api
import (
"encoding/json"
"github.com/sirupsen/logrus"
"net/http"
)
// WriteJsonOrError writes the supplied response to the http.ResponseWriter, handling any errors by logging and
// returning an Internal Server Error response (status 500)
func WriteJsonOrError(writer http.ResponseWriter, response interface{}) {
data, err := json.MarshalIndent(response, "", " ")
if err != nil {
logrus.WithError(err).Error("failed to create json payload")
writer.WriteHeader(500)
return
}
writer.Header().Set("Content-Type", "application/json")
_, err = writer.Write(data)
if err != nil {
logrus.WithError(err).Error("failed to write response")
}
}

@ -0,0 +1,32 @@
package api
import (
"github.com/containrrr/watchtower/pkg/metrics"
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// MetricsHandler is a HTTP handler for serving metric data
type MetricsHandler struct {
Path string
Handle http.HandlerFunc
Metrics *metrics.Metrics
}
// NewMetricsHandler is a factory function creating a new Metrics instance
func NewMetricsHandler() *MetricsHandler {
m := metrics.Default()
handler := promhttp.Handler()
return &MetricsHandler{
Path: "/v1/metrics",
Handle: handler.ServeHTTP,
Metrics: m,
}
}
func MetricsEndpoint() (path string, handler http.HandlerFunc) {
mh := NewMetricsHandler()
return mh.Path, mh.Handle
}

@ -1,27 +0,0 @@
package metrics
import (
"github.com/containrrr/watchtower/pkg/metrics"
"net/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// Handler is an HTTP handle for serving metric data
type Handler struct {
Path string
Handle http.HandlerFunc
Metrics *metrics.Metrics
}
// New is a factory function creating a new Metrics instance
func New() *Handler {
m := metrics.Default()
handler := promhttp.Handler()
return &Handler{
Path: "/v1/metrics",
Handle: handler.ServeHTTP,
Metrics: m,
}
}

@ -1,4 +1,4 @@
package metrics_test package api_test
import ( import (
"fmt" "fmt"
@ -6,10 +6,8 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"testing"
"github.com/containrrr/watchtower/pkg/api" "github.com/containrrr/watchtower/pkg/api"
metricsAPI "github.com/containrrr/watchtower/pkg/api/metrics"
"github.com/containrrr/watchtower/pkg/metrics" "github.com/containrrr/watchtower/pkg/metrics"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
@ -21,11 +19,6 @@ const (
getURL = "http://localhost:8080/v1/metrics" getURL = "http://localhost:8080/v1/metrics"
) )
func TestMetrics(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Metrics Suite")
}
func getWithToken(handler http.Handler) map[string]string { func getWithToken(handler http.Handler) map[string]string {
metricMap := map[string]string{} metricMap := map[string]string{}
respWriter := httptest.NewRecorder() respWriter := httptest.NewRecorder()
@ -51,7 +44,7 @@ func getWithToken(handler http.Handler) map[string]string {
var _ = Describe("the metrics API", func() { var _ = Describe("the metrics API", func() {
httpAPI := api.New(token) httpAPI := api.New(token)
m := metricsAPI.New() m := api.NewMetricsHandler()
handleReq := httpAPI.RequireToken(m.Handle) handleReq := httpAPI.RequireToken(m.Handle)
tryGetMetrics := func() map[string]string { return getWithToken(handleReq) } tryGetMetrics := func() map[string]string { return getWithToken(handleReq) }

@ -0,0 +1,14 @@
package api
import (
"github.com/containrrr/watchtower/pkg/session"
"net/http"
)
func ReportEndpoint(reportPtr **session.Report) (path string, handler http.HandlerFunc) {
path = "/v1/report"
handler = func(writer http.ResponseWriter, request *http.Request) {
WriteJsonOrError(writer, *reportPtr)
}
return path, handler
}

@ -0,0 +1,62 @@
package api
import (
"github.com/containrrr/watchtower/pkg/metrics"
log "github.com/sirupsen/logrus"
"net/http"
)
var (
lock chan bool
)
// NewUpdateHandler is a factory function creating a new Handler instance
func NewUpdateHandler(updateFn func() *metrics.Metric, updateLock chan bool) *UpdateHandler {
if updateLock != nil {
lock = updateLock
} else {
lock = make(chan bool, 1)
lock <- true
}
return &UpdateHandler{
fn: updateFn,
Path: "/v1/update",
}
}
func UpdateEndpoint(updateFn func() *metrics.Metric, updateLock chan bool) (path string, handler http.HandlerFunc) {
uh := NewUpdateHandler(updateFn, updateLock)
return uh.Path, uh.Handle
}
// UpdateHandler is an API handler used for triggering container update scans
type UpdateHandler struct {
fn func() *metrics.Metric
Path string
}
// Handle is the actual http.Handle function doing all the heavy lifting
func (handler *UpdateHandler) Handle(w http.ResponseWriter, _ *http.Request) {
log.Info("Updates triggered by HTTP API request.")
result := updateResult{}
select {
case chanValue := <-lock:
defer func() { lock <- chanValue }()
metric := handler.fn()
metrics.RegisterScan(metric)
result.Result = metric
result.Skipped = false
default:
log.Debug("Skipped. Another update already running.")
result.Skipped = true
}
WriteJsonOrError(w, result)
}
type updateResult struct {
Skipped bool
Result *metrics.Metric
}

@ -1,54 +0,0 @@
package update
import (
"io"
"net/http"
"os"
log "github.com/sirupsen/logrus"
)
var (
lock chan bool
)
// New is a factory function creating a new Handler instance
func New(updateFn func(), updateLock chan bool) *Handler {
if updateLock != nil {
lock = updateLock
} else {
lock = make(chan bool, 1)
lock <- true
}
return &Handler{
fn: updateFn,
Path: "/v1/update",
}
}
// Handler is an API handler used for triggering container update scans
type Handler struct {
fn func()
Path string
}
// Handle is the actual http.Handle function doing all the heavy lifting
func (handle *Handler) Handle(w http.ResponseWriter, r *http.Request) {
log.Info("Updates triggered by HTTP API request.")
_, err := io.Copy(os.Stdout, r.Body)
if err != nil {
log.Println(err)
return
}
select {
case chanValue := <-lock:
defer func() { lock <- chanValue }()
handle.fn()
default:
log.Debug("Skipped. Another update already running.")
}
}

@ -1,7 +1,7 @@
package metrics package metrics
import ( import (
"github.com/containrrr/watchtower/pkg/types" "github.com/containrrr/watchtower/pkg/session"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
) )
@ -26,12 +26,12 @@ type Metrics struct {
} }
// NewMetric returns a Metric with the counts taken from the appropriate types.Report fields // NewMetric returns a Metric with the counts taken from the appropriate types.Report fields
func NewMetric(report types.Report) *Metric { func NewMetric(report *session.Report) *Metric {
return &Metric{ return &Metric{
Scanned: len(report.Scanned()), Scanned: len(report.Scanned),
// Note: This is for backwards compatibility. ideally, stale containers should be counted separately // Note: This is for backwards compatibility. ideally, stale containers should be counted separately
Updated: len(report.Updated()) + len(report.Stale()), Updated: len(report.Updated) + len(report.Stale),
Failed: len(report.Failed()), Failed: len(report.Failed),
} }
} }

@ -1,6 +1,7 @@
package notifications package notifications
import ( import (
"github.com/containrrr/watchtower/pkg/session"
ty "github.com/containrrr/watchtower/pkg/types" ty "github.com/containrrr/watchtower/pkg/types"
"github.com/johntdyer/slackrus" "github.com/johntdyer/slackrus"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -8,8 +9,16 @@ import (
"os" "os"
) )
// Notifier is the interface that all notification services have in common
type Notifier interface {
StartNotification()
SendNotification(report *session.Report)
GetNames() []string
Close()
}
// NewNotifier creates and returns a new Notifier, using global configuration. // NewNotifier creates and returns a new Notifier, using global configuration.
func NewNotifier(c *cobra.Command) ty.Notifier { func NewNotifier(c *cobra.Command) Notifier {
f := c.PersistentFlags() f := c.PersistentFlags()
level, _ := f.GetString("notifications-level") level, _ := f.GetString("notifications-level")

@ -3,13 +3,13 @@ package notifications
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"github.com/containrrr/watchtower/pkg/session"
stdlog "log" stdlog "log"
"strings" "strings"
"text/template" "text/template"
"github.com/containrrr/shoutrrr" "github.com/containrrr/shoutrrr"
"github.com/containrrr/shoutrrr/pkg/types" "github.com/containrrr/shoutrrr/pkg/types"
t "github.com/containrrr/watchtower/pkg/types"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -18,7 +18,7 @@ const (
shoutrrrDefaultTemplate = `{{- with .Report -}} shoutrrrDefaultTemplate = `{{- with .Report -}}
{{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed {{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed
{{range .Updated -}} {{range .Updated -}}
- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}} - {{.Name}} ({{.ImageName}}): {{.OldImageID.ShortID}} updated to {{.NewImageID.ShortID}}
{{end -}} {{end -}}
{{range .Fresh -}} {{range .Fresh -}}
- {{.Name}} ({{.ImageName}}): {{.State}} - {{.Name}} ({{.ImageName}}): {{.State}}
@ -66,7 +66,7 @@ func (n *shoutrrrTypeNotifier) GetNames() []string {
return names return names
} }
func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, urls ...string) t.Notifier { func newShoutrrrNotifier(tplString string, acceptedLogLevels []log.Level, legacy bool, urls ...string) Notifier {
notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy) notifier := createNotifier(urls, acceptedLogLevels, tplString, legacy)
log.AddHook(notifier) log.AddHook(notifier)
@ -129,7 +129,7 @@ func (n *shoutrrrTypeNotifier) buildMessage(data Data) string {
return body.String() return body.String()
} }
func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report t.Report) { func (n *shoutrrrTypeNotifier) sendEntries(entries []*log.Entry, report *session.Report) {
msg := n.buildMessage(Data{entries, report}) msg := n.buildMessage(Data{entries, report})
n.messages <- msg n.messages <- msg
} }
@ -140,11 +140,7 @@ func (n *shoutrrrTypeNotifier) StartNotification() {
} }
} }
func (n *shoutrrrTypeNotifier) SendNotification(report t.Report) { func (n *shoutrrrTypeNotifier) SendNotification(report *session.Report) {
//if n.entries == nil || len(n.entries) <= 0 {
// return
//}
n.sendEntries(n.entries, report) n.sendEntries(n.entries, report)
n.entries = nil n.entries = nil
} }
@ -205,5 +201,5 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template,
// Data is the notification template data model // Data is the notification template data model
type Data struct { type Data struct {
Entries []*log.Entry Entries []*log.Entry
Report t.Report Report *session.Report
} }

@ -223,5 +223,7 @@ func getTemplatedResult(tplString string, legacy bool, data Data) (string, error
if err != nil { if err != nil {
return "", err return "", err
} }
return notifier.buildMessage(data), err msg := notifier.buildMessage(data)
println(msg)
return msg, err
} }

@ -1,6 +1,9 @@
package session package session
import wt "github.com/containrrr/watchtower/pkg/types" import (
wt "github.com/containrrr/watchtower/pkg/types"
"strings"
)
// State indicates what the current state is of the container // State indicates what the current state is of the container
type State int type State int
@ -19,51 +22,17 @@ const (
// ContainerStatus contains the container state during a session // ContainerStatus contains the container state during a session
type ContainerStatus struct { type ContainerStatus struct {
containerID wt.ContainerID ID wt.ContainerID
oldImage wt.ImageID Name string
newImage wt.ImageID OldImageID wt.ImageID
containerName string NewImageID wt.ImageID
imageName string ImageName string
error Error error
state State 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 (state State) String() string {
func (u *ContainerStatus) State() string { switch state {
switch u.state {
case SkippedState: case SkippedState:
return "Skipped" return "Skipped"
case ScannedState: case ScannedState:
@ -80,3 +49,12 @@ func (u *ContainerStatus) State() string {
return "Unknown" return "Unknown"
} }
} }
// MarshalJSON marshals State as a string
func (state State) MarshalJSON() ([]byte, error) {
sb := strings.Builder{}
sb.WriteString(`"`)
sb.WriteString(state.String())
sb.WriteString(`"`)
return []byte(sb.String()), nil
}

@ -10,19 +10,19 @@ type Progress map[types.ContainerID]*ContainerStatus
// UpdateFromContainer sets various status fields from their corresponding container equivalents // UpdateFromContainer sets various status fields from their corresponding container equivalents
func UpdateFromContainer(cont types.Container, newImage types.ImageID, state State) *ContainerStatus { func UpdateFromContainer(cont types.Container, newImage types.ImageID, state State) *ContainerStatus {
return &ContainerStatus{ return &ContainerStatus{
containerID: cont.ID(), ID: cont.ID(),
containerName: cont.Name(), Name: cont.Name(),
imageName: cont.ImageName(), ImageName: cont.ImageName(),
oldImage: cont.SafeImageID(), OldImageID: cont.SafeImageID(),
newImage: newImage, NewImageID: newImage,
state: state, State: state,
} }
} }
// AddSkipped adds a container to the Progress with the state set as skipped // AddSkipped adds a container to the Progress with the state set as skipped
func (m Progress) AddSkipped(cont types.Container, err error) { func (m Progress) AddSkipped(cont types.Container, err error) {
update := UpdateFromContainer(cont, cont.SafeImageID(), SkippedState) update := UpdateFromContainer(cont, cont.SafeImageID(), SkippedState)
update.error = err update.Error = err
m.Add(update) m.Add(update)
} }
@ -35,22 +35,17 @@ func (m Progress) AddScanned(cont types.Container, newImage types.ImageID) {
func (m Progress) UpdateFailed(failures map[types.ContainerID]error) { func (m Progress) UpdateFailed(failures map[types.ContainerID]error) {
for id, err := range failures { for id, err := range failures {
update := m[id] update := m[id]
update.error = err update.Error = err
update.state = FailedState update.State = FailedState
} }
} }
// Add a container to the map using container ID as the key // Add a container to the map using container ID as the key
func (m Progress) Add(update *ContainerStatus) { func (m Progress) Add(update *ContainerStatus) {
m[update.containerID] = update m[update.ID] = update
} }
// MarkForUpdate marks the container identified by containerID for update // MarkForUpdate marks the container identified by containerID for update
func (m Progress) MarkForUpdate(containerID types.ContainerID) { func (m Progress) MarkForUpdate(containerID types.ContainerID) {
m[containerID].state = UpdatedState m[containerID].State = UpdatedState
}
// Report creates a new Report from a Progress instance
func (m Progress) Report() types.Report {
return NewReport(m)
} }

@ -1,90 +1,78 @@
package session package session
import ( import (
"github.com/containrrr/watchtower/pkg/types"
"sort" "sort"
"time"
) )
type report struct { type Report struct {
scanned []types.ContainerReport Started time.Time
updated []types.ContainerReport Ended time.Time
failed []types.ContainerReport Trigger Trigger
skipped []types.ContainerReport Scanned []*ContainerStatus
stale []types.ContainerReport Updated []*ContainerStatus
fresh []types.ContainerReport Failed []*ContainerStatus
} Skipped []*ContainerStatus
Stale []*ContainerStatus
func (r *report) Scanned() []types.ContainerReport { Fresh []*ContainerStatus
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 // NewReport creates a types.Report from the supplied Progress
func NewReport(progress Progress) types.Report { // s.Started, time.Now().UTC(), s.Trigger, s.Progress
report := &report{ func NewReport(started, ended time.Time, trigger Trigger, progress Progress) *Report {
scanned: []types.ContainerReport{}, report := &Report{
updated: []types.ContainerReport{}, Started: started,
failed: []types.ContainerReport{}, Ended: ended,
skipped: []types.ContainerReport{}, Trigger: trigger,
stale: []types.ContainerReport{}, Scanned: []*ContainerStatus{},
fresh: []types.ContainerReport{}, Updated: []*ContainerStatus{},
Failed: []*ContainerStatus{},
Skipped: []*ContainerStatus{},
Stale: []*ContainerStatus{},
Fresh: []*ContainerStatus{},
} }
for _, update := range progress { for _, update := range progress {
if update.state == SkippedState { if update.State == SkippedState {
report.skipped = append(report.skipped, update) report.Skipped = append(report.Skipped, update)
continue continue
} }
report.scanned = append(report.scanned, update) report.Scanned = append(report.Scanned, update)
if update.newImage == update.oldImage { if update.NewImageID == update.OldImageID {
update.state = FreshState update.State = FreshState
report.fresh = append(report.fresh, update) report.Fresh = append(report.Fresh, update)
continue continue
} }
switch update.state { switch update.State {
case UpdatedState: case UpdatedState:
report.updated = append(report.updated, update) report.Updated = append(report.Updated, update)
case FailedState: case FailedState:
report.failed = append(report.failed, update) report.Failed = append(report.Failed, update)
default: default:
update.state = StaleState update.State = StaleState
report.stale = append(report.stale, update) report.Stale = append(report.Stale, update)
} }
} }
sort.Sort(sortableContainers(report.scanned)) sort.Sort(sortableContainers(report.Scanned))
sort.Sort(sortableContainers(report.updated)) sort.Sort(sortableContainers(report.Updated))
sort.Sort(sortableContainers(report.failed)) sort.Sort(sortableContainers(report.Failed))
sort.Sort(sortableContainers(report.skipped)) sort.Sort(sortableContainers(report.Skipped))
sort.Sort(sortableContainers(report.stale)) sort.Sort(sortableContainers(report.Stale))
sort.Sort(sortableContainers(report.fresh)) sort.Sort(sortableContainers(report.Fresh))
return report return report
} }
type sortableContainers []types.ContainerReport type sortableContainers []*ContainerStatus
// Len implements sort.Interface.Len // Len implements sort.Interface.Len
func (s sortableContainers) Len() int { return len(s) } func (s sortableContainers) Len() int { return len(s) }
// Less implements sort.Interface.Less // Less implements sort.Interface.Less
func (s sortableContainers) Less(i, j int) bool { return s[i].ID() < s[j].ID() } func (s sortableContainers) Less(i, j int) bool { return s[i].ID < s[j].ID }
// Swap implements sort.Interface.Swap // Swap implements sort.Interface.Swap
func (s sortableContainers) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s sortableContainers) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

@ -0,0 +1,24 @@
package session
import (
"time"
)
type Session struct {
Trigger Trigger
Started time.Time
Progress Progress
}
func New(trigger Trigger) *Session {
return &Session{
Started: time.Now().UTC(),
Trigger: trigger,
Progress: Progress{},
}
}
// Report creates a new Report from a Session instance
func (s Session) Report() *Report {
return NewReport(s.Started, time.Now().UTC(), s.Trigger, s.Progress)
}

@ -0,0 +1,34 @@
package session
import "strings"
type Trigger int
const (
SchedulerTrigger Trigger = iota
APITrigger
StartupTrigger
)
// String returns a string representation of the Trigger
func (trigger Trigger) String() string {
switch trigger {
case SchedulerTrigger:
return "Scheduler"
case APITrigger:
return "API"
case StartupTrigger:
return "Startup"
default:
return "Unknown"
}
}
// MarshalJSON marshals Trigger as a quoted string
func (trigger Trigger) MarshalJSON() ([]byte, error) {
sb := strings.Builder{}
sb.WriteString(`"`)
sb.WriteString(trigger.String())
sb.WriteString(`"`)
return []byte(sb.String()), nil
}

@ -1,9 +0,0 @@
package types
// Notifier is the interface that all notification services have in common
type Notifier interface {
StartNotification()
SendNotification(Report)
GetNames() []string
Close()
}

@ -1,22 +0,0 @@
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…
Cancel
Save