diff --git a/cmd/root.go b/cmd/root.go index fb7c29b..23bf1ec 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( "github.com/containrrr/watchtower/internal/meta" + "github.com/containrrr/watchtower/pkg/session" "math" "os" "os/signal" @@ -10,9 +11,6 @@ import ( "syscall" "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/flags" "github.com/containrrr/watchtower/pkg/api" @@ -34,12 +32,12 @@ var ( noRestart bool monitorOnly bool enableLabel bool - notifier t.Notifier + notifier notifications.Notifier timeout time.Duration lifecycleHooks bool rollingRestart bool scope string - // Set on build using ldflags + latestReport *session.Report ) var rootCmd = NewRootCommand() @@ -156,6 +154,7 @@ func Run(c *cobra.Command, names []string) { runOnce, _ := c.PersistentFlags().GetBool("run-once") enableUpdateAPI, _ := c.PersistentFlags().GetBool("http-api-update") enableMetricsAPI, _ := c.PersistentFlags().GetBool("http-api-metrics") + enableReportAPI := true unblockHTTPAPI, _ := c.PersistentFlags().GetBool("http-api-periodic-polls") apiToken, _ := c.PersistentFlags().GetString("http-api-token") @@ -171,7 +170,7 @@ func Run(c *cobra.Command, names []string) { if runOnce { writeStartupMessage(c, time.Time{}, filterDesc) - runUpdatesWithNotifications(filter) + runUpdatesWithNotifications(filter, session.StartupTrigger) notifier.Close() os.Exit(0) return @@ -188,13 +187,17 @@ func Run(c *cobra.Command, names []string) { httpAPI := api.New(apiToken) if enableUpdateAPI { - updateHandler := update.New(func() { runUpdatesWithNotifications(filter) }, updateLock) - httpAPI.RegisterFunc(updateHandler.Path, updateHandler.Handle) + httpAPI.RegisterFunc(api.UpdateEndpoint(func() *metrics.Metric { + return runUpdatesWithNotifications(filter, session.APITrigger) + }, updateLock)) } if enableMetricsAPI { - metricsHandler := apiMetrics.New() - httpAPI.RegisterHandler(metricsHandler.Path, metricsHandler.Handle) + httpAPI.RegisterHandler(api.MetricsEndpoint()) + } + + if enableReportAPI { + httpAPI.RegisterHandler(api.ReportEndpoint(&latestReport)) } if err := httpAPI.Start(enableUpdateAPI && !unblockHTTPAPI); err != nil { @@ -293,7 +296,7 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string, select { case v := <-lock: defer func() { lock <- v }() - metric := runUpdatesWithNotifications(filter) + metric := runUpdatesWithNotifications(filter, session.SchedulerTrigger) metrics.RegisterScan(metric) default: // Update was skipped @@ -327,7 +330,7 @@ func runUpgradesOnSchedule(c *cobra.Command, filter t.Filter, filtering string, return nil } -func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric { +func runUpdatesWithNotifications(filter t.Filter, trigger session.Trigger) *metrics.Metric { notifier.StartNotification() updateParams := t.UpdateParams{ Filter: filter, @@ -338,10 +341,11 @@ func runUpdatesWithNotifications(filter t.Filter) *metrics.Metric { LifecycleHooks: lifecycleHooks, RollingRestart: rollingRestart, } - result, err := actions.Update(client, updateParams) + result, err := actions.Update(client, updateParams, trigger) if err != nil { log.Error(err) } + latestReport = result notifier.SendNotification(result) metricResults := metrics.NewMetric(result) log.Debugf("Session done: %v scanned, %v updated, %v failed", diff --git a/internal/actions/mocks/progress.go b/internal/actions/mocks/progress.go index 6883b48..94feb97 100644 --- a/internal/actions/mocks/progress.go +++ b/internal/actions/mocks/progress.go @@ -8,10 +8,11 @@ import ( // 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 { +func CreateMockProgressReport(states ...session.State) *session.Report { stateNums := make(map[session.State]int) - progress := session.Progress{} + mockSession := session.New(session.SchedulerTrigger) + progress := mockSession.Progress failed := make(map[wt.ContainerID]error) for _, state := range states { @@ -41,6 +42,6 @@ func CreateMockProgressReport(states ...session.State) wt.Report { } progress.UpdateFailed(failed) - return progress.Report() + return mockSession.Report() } diff --git a/internal/actions/update.go b/internal/actions/update.go index f7eee8e..fea0f96 100644 --- a/internal/actions/update.go +++ b/internal/actions/update.go @@ -15,9 +15,10 @@ 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) (types.Report, error) { +func Update(client container.Client, params types.UpdateParams, trigger session.Trigger) (*session.Report, error) { log.Debug("Checking containers for updated images") - progress := &session.Progress{} + updateSession := session.New(trigger) + progress := updateSession.Progress staleCount := 0 if params.LifecycleHooks { @@ -92,7 +93,7 @@ func Update(client container.Client, params types.UpdateParams) (types.Report, e if params.LifecycleHooks { 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 { diff --git a/internal/actions/update_test.go b/internal/actions/update_test.go index 8750253..d848750 100644 --- a/internal/actions/update_test.go +++ b/internal/actions/update_test.go @@ -4,6 +4,7 @@ import ( "github.com/containrrr/watchtower/internal/actions" "github.com/containrrr/watchtower/pkg/container" "github.com/containrrr/watchtower/pkg/container/mocks" + "github.com/containrrr/watchtower/pkg/session" "github.com/containrrr/watchtower/pkg/types" dockerContainer "github.com/docker/docker/api/types/container" cli "github.com/docker/docker/client" @@ -15,6 +16,8 @@ import ( . "github.com/onsi/gomega" ) +var trigger = session.SchedulerTrigger + var _ = Describe("the update action", func() { var dockerClient cli.CommonAPIClient var client MockClient @@ -60,7 +63,7 @@ var _ = Describe("the update action", func() { When("there are multiple containers using the same image", 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(client.TestData.TriedToRemoveImageCount).To(Equal(1)) }) @@ -76,7 +79,7 @@ var _ = Describe("the update action", func() { time.Now(), ), ) - _, err := actions.Update(client, types.UpdateParams{Cleanup: true}) + _, err := actions.Update(client, types.UpdateParams{Cleanup: true}, trigger) Expect(err).NotTo(HaveOccurred()) Expect(client.TestData.TriedToRemoveImageCount).To(Equal(2)) }) @@ -84,7 +87,7 @@ var _ = Describe("the update action", func() { When("performing a rolling restart update", 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(client.TestData.TriedToRemoveImageCount).To(Equal(1)) }) @@ -124,7 +127,7 @@ var _ = Describe("the update action", 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(client.TestData.TriedToRemoveImageCount).To(Equal(1)) }) @@ -154,7 +157,7 @@ var _ = Describe("the update action", 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(client.TestData.TriedToRemoveImageCount).To(Equal(0)) }) @@ -193,7 +196,7 @@ var _ = Describe("the update action", 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(client.TestData.TriedToRemoveImageCount).To(Equal(0)) }) @@ -229,7 +232,7 @@ var _ = Describe("the update action", 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(client.TestData.TriedToRemoveImageCount).To(Equal(0)) }) @@ -265,7 +268,7 @@ var _ = Describe("the update action", 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(client.TestData.TriedToRemoveImageCount).To(Equal(1)) }) @@ -300,7 +303,7 @@ var _ = Describe("the update action", 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(client.TestData.TriedToRemoveImageCount).To(Equal(1)) }) @@ -336,7 +339,7 @@ var _ = Describe("the update action", 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(client.TestData.TriedToRemoveImageCount).To(Equal(1)) }) diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index e298622..ffa3ed8 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -18,7 +18,7 @@ func TestEnvConfig_Defaults(t *testing.T) { err := EnvConfig(cmd) 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")) // Re-enable this test when we've moved to github actions. // assert.Equal(t, DockerAPIMinVersion, os.Getenv("DOCKER_API_VERSION")) diff --git a/pkg/api/api.go b/pkg/api/api.go index b2279e1..e813b39 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "github.com/containrrr/watchtower/pkg/session" log "github.com/sirupsen/logrus" "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 type API struct { - Token string - hasHandlers bool + Token string + hasHandlers bool + latestReport session.Report } // New is a factory function creating a new API instance @@ -74,3 +76,7 @@ func runHTTPServer() { log.Info("Serving HTTP") log.Fatal(http.ListenAndServe(":8080", nil)) } + +func (api *API) UpdateReport(report session.Report) { + api.latestReport = report +} diff --git a/pkg/api/api_suite_test.go b/pkg/api/api_suite_test.go new file mode 100644 index 0000000..2a607d3 --- /dev/null +++ b/pkg/api/api_suite_test.go @@ -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") +} diff --git a/pkg/api/json.go b/pkg/api/json.go new file mode 100644 index 0000000..2cfbccb --- /dev/null +++ b/pkg/api/json.go @@ -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") + } +} diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go new file mode 100644 index 0000000..8985f69 --- /dev/null +++ b/pkg/api/metrics.go @@ -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 +} diff --git a/pkg/api/metrics/metrics.go b/pkg/api/metrics/metrics.go deleted file mode 100644 index 4faad4a..0000000 --- a/pkg/api/metrics/metrics.go +++ /dev/null @@ -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, - } -} diff --git a/pkg/api/metrics/metrics_test.go b/pkg/api/metrics_test.go similarity index 90% rename from pkg/api/metrics/metrics_test.go rename to pkg/api/metrics_test.go index 5120f8d..5cca519 100644 --- a/pkg/api/metrics/metrics_test.go +++ b/pkg/api/metrics_test.go @@ -1,4 +1,4 @@ -package metrics_test +package api_test import ( "fmt" @@ -6,10 +6,8 @@ import ( "net/http" "net/http/httptest" "strings" - "testing" "github.com/containrrr/watchtower/pkg/api" - metricsAPI "github.com/containrrr/watchtower/pkg/api/metrics" "github.com/containrrr/watchtower/pkg/metrics" . "github.com/onsi/ginkgo" @@ -21,11 +19,6 @@ const ( 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 { metricMap := map[string]string{} respWriter := httptest.NewRecorder() @@ -51,7 +44,7 @@ func getWithToken(handler http.Handler) map[string]string { var _ = Describe("the metrics API", func() { httpAPI := api.New(token) - m := metricsAPI.New() + m := api.NewMetricsHandler() handleReq := httpAPI.RequireToken(m.Handle) tryGetMetrics := func() map[string]string { return getWithToken(handleReq) } diff --git a/pkg/api/report.go b/pkg/api/report.go new file mode 100644 index 0000000..5178341 --- /dev/null +++ b/pkg/api/report.go @@ -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 +} diff --git a/pkg/api/update.go b/pkg/api/update.go new file mode 100644 index 0000000..37de030 --- /dev/null +++ b/pkg/api/update.go @@ -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 +} diff --git a/pkg/api/update/update.go b/pkg/api/update/update.go deleted file mode 100644 index 4721e3e..0000000 --- a/pkg/api/update/update.go +++ /dev/null @@ -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.") - } - -} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index b681733..92163a6 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -1,7 +1,7 @@ package metrics 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/promauto" ) @@ -26,12 +26,12 @@ type Metrics struct { } // 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{ - Scanned: len(report.Scanned()), + 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()), + Updated: len(report.Updated) + len(report.Stale), + Failed: len(report.Failed), } } diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index e1cb5e7..d1958b0 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -1,6 +1,7 @@ package notifications import ( + "github.com/containrrr/watchtower/pkg/session" ty "github.com/containrrr/watchtower/pkg/types" "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" @@ -8,8 +9,16 @@ import ( "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. -func NewNotifier(c *cobra.Command) ty.Notifier { +func NewNotifier(c *cobra.Command) Notifier { f := c.PersistentFlags() level, _ := f.GetString("notifications-level") diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index 41ef126..84a67ec 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -3,13 +3,13 @@ package notifications import ( "bytes" "fmt" + "github.com/containrrr/watchtower/pkg/session" stdlog "log" "strings" "text/template" "github.com/containrrr/shoutrrr" "github.com/containrrr/shoutrrr/pkg/types" - t "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" ) @@ -18,7 +18,7 @@ const ( shoutrrrDefaultTemplate = `{{- with .Report -}} {{len .Scanned}} Scanned, {{len .Updated}} Updated, {{len .Failed}} Failed {{range .Updated -}} -- {{.Name}} ({{.ImageName}}): {{.CurrentImageID.ShortID}} updated to {{.LatestImageID.ShortID}} +- {{.Name}} ({{.ImageName}}): {{.OldImageID.ShortID}} updated to {{.NewImageID.ShortID}} {{end -}} {{range .Fresh -}} - {{.Name}} ({{.ImageName}}): {{.State}} @@ -66,7 +66,7 @@ func (n *shoutrrrTypeNotifier) GetNames() []string { 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) log.AddHook(notifier) @@ -129,7 +129,7 @@ func (n *shoutrrrTypeNotifier) buildMessage(data Data) 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}) n.messages <- msg } @@ -140,11 +140,7 @@ func (n *shoutrrrTypeNotifier) StartNotification() { } } -func (n *shoutrrrTypeNotifier) SendNotification(report t.Report) { - //if n.entries == nil || len(n.entries) <= 0 { - // return - //} - +func (n *shoutrrrTypeNotifier) SendNotification(report *session.Report) { n.sendEntries(n.entries, report) n.entries = nil } @@ -205,5 +201,5 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, // Data is the notification template data model type Data struct { Entries []*log.Entry - Report t.Report + Report *session.Report } diff --git a/pkg/notifications/shoutrrr_test.go b/pkg/notifications/shoutrrr_test.go index e92655c..3f3b0fd 100644 --- a/pkg/notifications/shoutrrr_test.go +++ b/pkg/notifications/shoutrrr_test.go @@ -223,5 +223,7 @@ func getTemplatedResult(tplString string, legacy bool, data Data) (string, error if err != nil { return "", err } - return notifier.buildMessage(data), err + msg := notifier.buildMessage(data) + println(msg) + return msg, err } diff --git a/pkg/session/container_status.go b/pkg/session/container_status.go index 8313da1..b23c0b9 100644 --- a/pkg/session/container_status.go +++ b/pkg/session/container_status.go @@ -1,6 +1,9 @@ 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 type State int @@ -19,51 +22,17 @@ const ( // 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() + ID wt.ContainerID + Name string + OldImageID wt.ImageID + NewImageID wt.ImageID + ImageName string + Error error + State State } -// State returns the current State that the container is in -func (u *ContainerStatus) State() string { - switch u.state { +func (state State) String() string { + switch state { case SkippedState: return "Skipped" case ScannedState: @@ -80,3 +49,12 @@ func (u *ContainerStatus) State() string { 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 +} diff --git a/pkg/session/progress.go b/pkg/session/progress.go index 57069be..4c6b013 100644 --- a/pkg/session/progress.go +++ b/pkg/session/progress.go @@ -10,19 +10,19 @@ 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, + ID: cont.ID(), + Name: cont.Name(), + ImageName: cont.ImageName(), + OldImageID: cont.SafeImageID(), + NewImageID: 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 + update.Error = err 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) { for id, err := range failures { update := m[id] - update.error = err - update.state = FailedState + 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 + m[update.ID] = 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) + m[containerID].State = UpdatedState } diff --git a/pkg/session/report.go b/pkg/session/report.go index 646a0c0..27b91a1 100644 --- a/pkg/session/report.go +++ b/pkg/session/report.go @@ -1,90 +1,78 @@ package session import ( - "github.com/containrrr/watchtower/pkg/types" "sort" + "time" ) -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 +type Report struct { + Started time.Time + Ended time.Time + Trigger Trigger + Scanned []*ContainerStatus + Updated []*ContainerStatus + Failed []*ContainerStatus + Skipped []*ContainerStatus + Stale []*ContainerStatus + Fresh []*ContainerStatus } // 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{}, +// s.Started, time.Now().UTC(), s.Trigger, s.Progress +func NewReport(started, ended time.Time, trigger Trigger, progress Progress) *Report { + report := &Report{ + Started: started, + Ended: ended, + Trigger: trigger, + Scanned: []*ContainerStatus{}, + Updated: []*ContainerStatus{}, + Failed: []*ContainerStatus{}, + Skipped: []*ContainerStatus{}, + Stale: []*ContainerStatus{}, + Fresh: []*ContainerStatus{}, } for _, update := range progress { - if update.state == SkippedState { - report.skipped = append(report.skipped, update) + 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) + report.Scanned = append(report.Scanned, update) + if update.NewImageID == update.OldImageID { + update.State = FreshState + report.Fresh = append(report.Fresh, update) continue } - switch update.state { + switch update.State { case UpdatedState: - report.updated = append(report.updated, update) + report.Updated = append(report.Updated, update) case FailedState: - report.failed = append(report.failed, update) + report.Failed = append(report.Failed, update) default: - update.state = StaleState - report.stale = append(report.stale, update) + 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)) + 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 +type sortableContainers []*ContainerStatus // 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() } +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] } diff --git a/pkg/session/session.go b/pkg/session/session.go new file mode 100644 index 0000000..d467a74 --- /dev/null +++ b/pkg/session/session.go @@ -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) +} diff --git a/pkg/session/trigger.go b/pkg/session/trigger.go new file mode 100644 index 0000000..58013d0 --- /dev/null +++ b/pkg/session/trigger.go @@ -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 +} diff --git a/pkg/types/notifier.go b/pkg/types/notifier.go deleted file mode 100644 index ccb2cb6..0000000 --- a/pkg/types/notifier.go +++ /dev/null @@ -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() -} diff --git a/pkg/types/report.go b/pkg/types/report.go deleted file mode 100644 index 8013b58..0000000 --- a/pkg/types/report.go +++ /dev/null @@ -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 -}