From 547d033460ce13bc5c961ff8b83becefe7b29de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nils=20m=C3=A5s=C3=A9n?= Date: Sun, 29 Jan 2023 17:10:18 +0100 Subject: [PATCH] feat(notifications): add json template (#1542) --- pkg/notifications/common_templates.go | 3 +- pkg/notifications/json.go | 71 ++++++++++++++++ pkg/notifications/json_test.go | 118 ++++++++++++++++++++++++++ pkg/notifications/model.go | 19 +++++ pkg/notifications/shoutrrr.go | 14 +-- 5 files changed, 211 insertions(+), 14 deletions(-) create mode 100644 pkg/notifications/json.go create mode 100644 pkg/notifications/json_test.go create mode 100644 pkg/notifications/model.go diff --git a/pkg/notifications/common_templates.go b/pkg/notifications/common_templates.go index 64a53c0..84c0f54 100644 --- a/pkg/notifications/common_templates.go +++ b/pkg/notifications/common_templates.go @@ -35,5 +35,6 @@ var commonTemplates = map[string]string{ no containers matched filter {{- end -}} {{- end -}}`, -} + `json.v1`: `{{ . | ToJSON }}`, +} diff --git a/pkg/notifications/json.go b/pkg/notifications/json.go new file mode 100644 index 0000000..1bd304a --- /dev/null +++ b/pkg/notifications/json.go @@ -0,0 +1,71 @@ +package notifications + +import ( + "encoding/json" + + t "github.com/containrrr/watchtower/pkg/types" +) + +type jsonMap = map[string]interface{} + +// MarshalJSON implements json.Marshaler +func (d Data) MarshalJSON() ([]byte, error) { + var entries = make([]jsonMap, len(d.Entries)) + for i, entry := range d.Entries { + entries[i] = jsonMap{ + `level`: entry.Level, + `message`: entry.Message, + `data`: entry.Data, + `time`: entry.Time, + } + } + + var report jsonMap + if d.Report != nil { + report = jsonMap{ + `scanned`: marshalReports(d.Report.Scanned()), + `updated`: marshalReports(d.Report.Updated()), + `failed`: marshalReports(d.Report.Failed()), + `skipped`: marshalReports(d.Report.Skipped()), + `stale`: marshalReports(d.Report.Stale()), + `fresh`: marshalReports(d.Report.Fresh()), + } + } + + return json.Marshal(jsonMap{ + `report`: report, + `title`: d.Title, + `host`: d.Host, + `entries`: entries, + }) +} + +func marshalReports(reports []t.ContainerReport) []jsonMap { + jsonReports := make([]jsonMap, len(reports)) + for i, report := range reports { + jsonReports[i] = jsonMap{ + `id`: report.ID().ShortID(), + `name`: report.Name(), + `currentImageId`: report.CurrentImageID().ShortID(), + `latestImageId`: report.LatestImageID().ShortID(), + `imageName`: report.ImageName(), + `state`: report.State(), + } + if errorMessage := report.Error(); errorMessage != "" { + jsonReports[i][`error`] = errorMessage + } + } + return jsonReports +} + +var _ json.Marshaler = &Data{} + +func toJSON(v interface{}) string { + var bytes []byte + var err error + if bytes, err = json.MarshalIndent(v, "", " "); err != nil { + LocalLog.Errorf("failed to marshal JSON in notification template: %v", err) + return "" + } + return string(bytes) +} diff --git a/pkg/notifications/json_test.go b/pkg/notifications/json_test.go new file mode 100644 index 0000000..ef30c59 --- /dev/null +++ b/pkg/notifications/json_test.go @@ -0,0 +1,118 @@ +package notifications + +import ( + s "github.com/containrrr/watchtower/pkg/session" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("JSON template", func() { + When("using report templates", func() { + When("JSON template is used", func() { + It("should format the messages to the expected format", func() { + expected := `{ + "entries": [ + { + "data": null, + "level": "info", + "message": "foo Bar", + "time": "0001-01-01T00:00:00Z" + } + ], + "host": "Mock", + "report": { + "failed": [ + { + "currentImageId": "01d210000000", + "error": "accidentally the whole container", + "id": "c79210000000", + "imageName": "mock/fail1:latest", + "latestImageId": "d0a210000000", + "name": "fail1", + "state": "Failed" + } + ], + "fresh": [ + { + "currentImageId": "01d310000000", + "id": "c79310000000", + "imageName": "mock/frsh1:latest", + "latestImageId": "01d310000000", + "name": "frsh1", + "state": "Fresh" + } + ], + "scanned": [ + { + "currentImageId": "01d110000000", + "id": "c79110000000", + "imageName": "mock/updt1:latest", + "latestImageId": "d0a110000000", + "name": "updt1", + "state": "Updated" + }, + { + "currentImageId": "01d120000000", + "id": "c79120000000", + "imageName": "mock/updt2:latest", + "latestImageId": "d0a120000000", + "name": "updt2", + "state": "Updated" + }, + { + "currentImageId": "01d210000000", + "error": "accidentally the whole container", + "id": "c79210000000", + "imageName": "mock/fail1:latest", + "latestImageId": "d0a210000000", + "name": "fail1", + "state": "Failed" + }, + { + "currentImageId": "01d310000000", + "id": "c79310000000", + "imageName": "mock/frsh1:latest", + "latestImageId": "01d310000000", + "name": "frsh1", + "state": "Fresh" + } + ], + "skipped": [ + { + "currentImageId": "01d410000000", + "error": "unpossible", + "id": "c79410000000", + "imageName": "mock/skip1:latest", + "latestImageId": "01d410000000", + "name": "skip1", + "state": "Skipped" + } + ], + "stale": [], + "updated": [ + { + "currentImageId": "01d110000000", + "id": "c79110000000", + "imageName": "mock/updt1:latest", + "latestImageId": "d0a110000000", + "name": "updt1", + "state": "Updated" + }, + { + "currentImageId": "01d120000000", + "id": "c79120000000", + "imageName": "mock/updt2:latest", + "latestImageId": "d0a120000000", + "name": "updt2", + "state": "Updated" + } + ] + }, + "title": "Watchtower updates on Mock" +}` + data := mockDataFromStates(s.UpdatedState, s.FreshState, s.FailedState, s.SkippedState, s.UpdatedState) + Expect(getTemplatedResult(`json.v1`, false, data)).To(MatchJSON(expected)) + }) + }) + }) +}) diff --git a/pkg/notifications/model.go b/pkg/notifications/model.go new file mode 100644 index 0000000..83c97ba --- /dev/null +++ b/pkg/notifications/model.go @@ -0,0 +1,19 @@ +package notifications + +import ( + t "github.com/containrrr/watchtower/pkg/types" + log "github.com/sirupsen/logrus" +) + +// StaticData is the part of the notification template data model set upon initialization +type StaticData struct { + Title string + Host string +} + +// Data is the notification template data model +type Data struct { + StaticData + Entries []*log.Entry + Report t.Report +} diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index 47141e8..d6ce859 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -210,6 +210,7 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, funcs := template.FuncMap{ "ToUpper": strings.ToUpper, "ToLower": strings.ToLower, + "ToJSON": toJSON, "Title": cases.Title(language.AmericanEnglish).String, } tplBase := template.New("").Funcs(funcs) @@ -240,16 +241,3 @@ func getShoutrrrTemplate(tplString string, legacy bool) (tpl *template.Template, return } - -// StaticData is the part of the notification template data model set upon initialization -type StaticData struct { - Title string - Host string -} - -// Data is the notification template data model -type Data struct { - StaticData - Entries []*log.Entry - Report t.Report -}