diff --git a/cmd/root.go b/cmd/root.go index 1e61308..4308dd1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -34,15 +34,20 @@ var ( scope string ) -var rootCmd = &cobra.Command{ - Use: "watchtower", - Short: "Automatically updates running Docker containers", - Long: ` -Watchtower automatically updates running Docker containers whenever a new image is released. -More information available at https://github.com/containrrr/watchtower/. -`, - Run: Run, - PreRun: PreRun, +var rootCmd = NewRootCommand() + +// NewRootCommand creates the root command for watchtower +func NewRootCommand() *cobra.Command { + return &cobra.Command{ + Use: "watchtower", + Short: "Automatically updates running Docker containers", + Long: ` + Watchtower automatically updates running Docker containers whenever a new image is released. + More information available at https://github.com/containrrr/watchtower/. + `, + Run: Run, + PreRun: PreRun, + } } func init() { diff --git a/pkg/notifications/email.go b/pkg/notifications/email.go index 6079de7..2356978 100644 --- a/pkg/notifications/email.go +++ b/pkg/notifications/email.go @@ -1,29 +1,22 @@ package notifications import ( - "encoding/base64" - "fmt" - "github.com/spf13/cobra" - "net/smtp" "os" - "strings" "time" + "github.com/spf13/cobra" + + shoutrrrSmtp "github.com/containrrr/shoutrrr/pkg/services/smtp" t "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" - "strconv" ) const ( emailType = "email" ) -// Implements Notifier, logrus.Hook -// The default logrus email integration would have several issues: -// - It would send one email per log output -// - It would only send errors -// We work around that by holding on to log entries until the update cycle is done. type emailTypeNotifier struct { + url string From, To string Server, User, Password, SubjectTag string Port int @@ -33,7 +26,12 @@ type emailTypeNotifier struct { delay time.Duration } -func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +// NewEmailNotifier is a factory method creating a new email notifier instance +func NewEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { + return newEmailNotifier(c, acceptedLogLevels) +} + +func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { flags := c.PersistentFlags() from, _ := flags.GetString("notification-email-from") @@ -47,6 +45,7 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie subjecttag, _ := flags.GetString("notification-email-subjecttag") n := &emailTypeNotifier{ + entries: []*log.Entry{}, From: from, To: to, Server: server, @@ -59,12 +58,33 @@ func newEmailNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie SubjectTag: subjecttag, } - log.AddHook(n) - return n } -func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte { +func (e *emailTypeNotifier) GetURL() string { + conf := &shoutrrrSmtp.Config{ + FromAddress: e.From, + FromName: "Watchtower", + ToAddresses: []string{e.To}, + Port: uint16(e.Port), + Host: e.Server, + Subject: e.getSubject(), + Username: e.User, + Password: e.Password, + UseStartTLS: true, + UseHTML: false, + } + + if len(e.User) > 0 { + conf.Set("auth", "Plain") + } else { + conf.Set("auth", "None") + } + + return conf.GetURL().String() +} + +func (e *emailTypeNotifier) getSubject() string { var emailSubject string if e.SubjectTag == "" { @@ -75,83 +95,13 @@ func (e *emailTypeNotifier) buildMessage(entries []*log.Entry) []byte { if hostname, err := os.Hostname(); err == nil { emailSubject += " on " + hostname } - body := "" - for _, entry := range entries { - body += entry.Time.Format("2006-01-02 15:04:05") + " (" + entry.Level.String() + "): " + entry.Message + "\r\n" - // We don't use fields in watchtower, so don't bother sending them. - } - - t := time.Now() - - header := make(map[string]string) - header["From"] = e.From - header["To"] = e.To - header["Subject"] = emailSubject - header["Date"] = t.Format(time.RFC1123Z) - header["MIME-Version"] = "1.0" - header["Content-Type"] = "text/plain; charset=\"utf-8\"" - header["Content-Transfer-Encoding"] = "base64" - - message := "" - for k, v := range header { - message += fmt.Sprintf("%s: %s\r\n", k, v) - } - - encodedBody := base64.StdEncoding.EncodeToString([]byte(body)) - //RFC 2045 base64 encoding demands line no longer than 76 characters. - for _, line := range SplitSubN(encodedBody, 76) { - message += "\r\n" + line - } - - return []byte(message) -} - -func (e *emailTypeNotifier) sendEntries(entries []*log.Entry) { - // Do the sending in a separate goroutine so we don't block the main process. - msg := e.buildMessage(entries) - go func() { - if e.delay > 0 { - time.Sleep(e.delay) - } - - var auth smtp.Auth - if e.User != "" { - auth = smtp.PlainAuth("", e.User, e.Password, e.Server) - } - err := SendMail(e.Server+":"+strconv.Itoa(e.Port), e.tlsSkipVerify, auth, e.From, strings.Split(e.To, ","), msg) - if err != nil { - // Use fmt so it doesn't trigger another email. - fmt.Println("Failed to send notification email: ", err) - } - }() + return emailSubject } -func (e *emailTypeNotifier) StartNotification() { - if e.entries == nil { - e.entries = make([]*log.Entry, 0, 10) - } -} - -func (e *emailTypeNotifier) SendNotification() { - if e.entries == nil || len(e.entries) <= 0 { - return - } - - e.sendEntries(e.entries) - e.entries = nil -} - -func (e *emailTypeNotifier) Levels() []log.Level { - return e.logLevels -} - -func (e *emailTypeNotifier) Fire(entry *log.Entry) error { - if e.entries != nil { - e.entries = append(e.entries, entry) - } else { - e.sendEntries([]*log.Entry{entry}) - } - return nil -} +// TODO: Delete these once all notifiers have been converted to shoutrrr +func (e *emailTypeNotifier) StartNotification() {} +func (e *emailTypeNotifier) SendNotification() {} +func (e *emailTypeNotifier) Levels() []log.Level { return nil } +func (e *emailTypeNotifier) Fire(entry *log.Entry) error { return nil } func (e *emailTypeNotifier) Close() {} diff --git a/pkg/notifications/gotify.go b/pkg/notifications/gotify.go index 789f778..47bab40 100644 --- a/pkg/notifications/gotify.go +++ b/pkg/notifications/gotify.go @@ -1,16 +1,13 @@ package notifications import ( - "bytes" - "crypto/tls" - "encoding/json" - "fmt" - "net/http" "strings" + shoutrrrGotify "github.com/containrrr/shoutrrr/pkg/services/gotify" t "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) const ( @@ -24,94 +21,71 @@ type gotifyTypeNotifier struct { logLevels []log.Level } -func newGotifyNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +// NewGotifyNotifier is a factory method creating a new gotify notifier instance +func NewGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertableNotifier { + return newGotifyNotifier(c, levels) +} + +func newGotifyNotifier(c *cobra.Command, levels []log.Level) t.ConvertableNotifier { flags := c.PersistentFlags() - gotifyURL, _ := flags.GetString("notification-gotify-url") - if len(gotifyURL) < 1 { - log.Fatal("Required argument --notification-gotify-url(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_URL(env) is empty.") - } else if !(strings.HasPrefix(gotifyURL, "http://") || strings.HasPrefix(gotifyURL, "https://")) { - log.Fatal("Gotify URL must start with \"http://\" or \"https://\"") - } else if strings.HasPrefix(gotifyURL, "http://") { - log.Warn("Using an HTTP url for Gotify is insecure") + url := getGotifyURL(flags) + token := getGotifyToken(flags) + + skipVerify, _ := flags.GetBool("notification-gotify-tls-skip-verify") + + n := &gotifyTypeNotifier{ + gotifyURL: url, + gotifyAppToken: token, + gotifyInsecureSkipVerify: skipVerify, + logLevels: levels, } + return n +} + +func getGotifyToken(flags *pflag.FlagSet) string { gotifyToken, _ := flags.GetString("notification-gotify-token") if len(gotifyToken) < 1 { log.Fatal("Required argument --notification-gotify-token(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_TOKEN(env) is empty.") } + return gotifyToken +} - gotifyInsecureSkipVerify, _ := flags.GetBool("notification-gotify-tls-skip-verify") +func getGotifyURL(flags *pflag.FlagSet) string { + gotifyURL, _ := flags.GetString("notification-gotify-url") - n := &gotifyTypeNotifier{ - gotifyURL: gotifyURL, - gotifyAppToken: gotifyToken, - gotifyInsecureSkipVerify: gotifyInsecureSkipVerify, - logLevels: acceptedLogLevels, + if len(gotifyURL) < 1 { + log.Fatal("Required argument --notification-gotify-url(cli) or WATCHTOWER_NOTIFICATION_GOTIFY_URL(env) is empty.") + } else if !(strings.HasPrefix(gotifyURL, "http://") || strings.HasPrefix(gotifyURL, "https://")) { + log.Fatal("Gotify URL must start with \"http://\" or \"https://\"") + } else if strings.HasPrefix(gotifyURL, "http://") { + log.Warn("Using an HTTP url for Gotify is insecure") } - log.AddHook(n) - - return n + return gotifyURL } -func (n *gotifyTypeNotifier) StartNotification() {} - -func (n *gotifyTypeNotifier) SendNotification() {} +func (n *gotifyTypeNotifier) GetURL() string { + url := n.gotifyURL -func (n *gotifyTypeNotifier) Close() {} + if strings.HasPrefix(url, "https://") { + url = strings.TrimPrefix(url, "https://") + } else { + url = strings.TrimPrefix(url, "http://") + } -func (n *gotifyTypeNotifier) Levels() []log.Level { - return n.logLevels -} + url = strings.TrimSuffix(url, "/") -func (n *gotifyTypeNotifier) getURL() string { - url := n.gotifyURL - if !strings.HasSuffix(url, "/") { - url += "/" + config := &shoutrrrGotify.Config{ + Host: url, + Token: n.gotifyAppToken, } - return url + "message?token=" + n.gotifyAppToken -} -func (n *gotifyTypeNotifier) Fire(entry *log.Entry) error { - - go func() { - jsonBody, err := json.Marshal(gotifyMessage{ - Message: "(" + entry.Level.String() + "): " + entry.Message, - Title: "Watchtower", - Priority: 0, - }) - if err != nil { - fmt.Println("Failed to create JSON body for Gotify notification: ", err) - return - } - - // Explicitly define the client so we can set InsecureSkipVerify to the desired value. - client := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: n.gotifyInsecureSkipVerify, - }, - }, - } - jsonBodyBuffer := bytes.NewBuffer([]byte(jsonBody)) - resp, err := client.Post(n.getURL(), "application/json", jsonBodyBuffer) - if err != nil { - fmt.Println("Failed to send Gotify notification: ", err) - return - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - fmt.Printf("Gotify notification returned %d HTTP status code", resp.StatusCode) - } - - }() - return nil + return config.GetURL().String() } -type gotifyMessage struct { - Message string `json:"message"` - Title string `json:"title"` - Priority int `json:"priority"` -} +func (n *gotifyTypeNotifier) StartNotification() {} +func (n *gotifyTypeNotifier) SendNotification() {} +func (n *gotifyTypeNotifier) Close() {} +func (n *gotifyTypeNotifier) Levels() []log.Level { return nil } diff --git a/pkg/notifications/msteams.go b/pkg/notifications/msteams.go index ab33966..0c99072 100644 --- a/pkg/notifications/msteams.go +++ b/pkg/notifications/msteams.go @@ -1,15 +1,12 @@ package notifications import ( - "bytes" - "encoding/json" - "fmt" - "github.com/spf13/cobra" - "net/http" + "strings" + shoutrrrTeams "github.com/containrrr/shoutrrr/pkg/services/teams" t "github.com/containrrr/watchtower/pkg/types" log "github.com/sirupsen/logrus" - "io/ioutil" + "github.com/spf13/cobra" ) const ( @@ -22,7 +19,12 @@ type msTeamsTypeNotifier struct { data bool } -func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +// NewMsTeamsNotifier is a factory method creating a new teams notifier instance +func NewMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { + return newMsTeamsNotifier(cmd, acceptedLogLevels) +} + +func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { flags := cmd.PersistentFlags() @@ -38,103 +40,29 @@ func newMsTeamsNotifier(cmd *cobra.Command, acceptedLogLevels []log.Level) t.Not data: withData, } - log.AddHook(n) - return n } -func (n *msTeamsTypeNotifier) StartNotification() {} - -func (n *msTeamsTypeNotifier) SendNotification() {} - -func (n *msTeamsTypeNotifier) Close() {} - -func (n *msTeamsTypeNotifier) Levels() []log.Level { - return n.levels -} - -func (n *msTeamsTypeNotifier) Fire(entry *log.Entry) error { - - message := "(" + entry.Level.String() + "): " + entry.Message - - go func() { - webHookBody := messageCard{ - CardType: "MessageCard", - Context: "http://schema.org/extensions", - Markdown: true, - Text: message, - } +func (n *msTeamsTypeNotifier) GetURL() string { - if n.data && entry.Data != nil && len(entry.Data) > 0 { - section := messageCardSection{ - Facts: make([]messageCardSectionFact, len(entry.Data)), - Text: "", - } + baseURL := "https://outlook.office.com/webhook/" - index := 0 - for k, v := range entry.Data { - section.Facts[index] = messageCardSectionFact{ - Name: k, - Value: fmt.Sprint(v), - } - index++ - } - - webHookBody.Sections = []messageCardSection{section} - } - - jsonBody, err := json.Marshal(webHookBody) - if err != nil { - fmt.Println("Failed to build JSON body for MSTeams notificattion: ", err) - return - } - - resp, err := http.Post(n.webHookURL, "application/json", bytes.NewBuffer([]byte(jsonBody))) - if err != nil { - fmt.Println("Failed to send MSTeams notificattion: ", err) - } - - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode > 299 { - fmt.Println("Failed to send MSTeams notificattion. HTTP RESPONSE STATUS: ", resp.StatusCode) - if resp.Body != nil { - bodyBytes, err := ioutil.ReadAll(resp.Body) - if err == nil { - bodyString := string(bodyBytes) - fmt.Println(bodyString) - } - } - } - }() - - return nil -} - -type messageCard struct { - CardType string `json:"@type"` - Context string `json:"@context"` - CorrelationID string `json:"correlationId,omitempty"` - ThemeColor string `json:"themeColor,omitempty"` - Summary string `json:"summary,omitempty"` - Title string `json:"title,omitempty"` - Text string `json:"text,omitempty"` - Markdown bool `json:"markdown,bool"` - Sections []messageCardSection `json:"sections,omitempty"` -} + path := strings.Replace(n.webHookURL, baseURL, "", 1) + rawToken := strings.Replace(path, "/IncomingWebhook", "", 1) + token := strings.Split(rawToken, "/") + config := &shoutrrrTeams.Config{ + Token: shoutrrrTeams.Token{ + A: token[0], + B: token[1], + C: token[2], + }, + } -type messageCardSection struct { - Title string `json:"title,omitempty"` - Text string `json:"text,omitempty"` - ActivityTitle string `json:"activityTitle,omitempty"` - ActivitySubtitle string `json:"activitySubtitle,omitempty"` - ActivityImage string `json:"activityImage,omitempty"` - ActivityText string `json:"activityText,omitempty"` - HeroImage string `json:"heroImage,omitempty"` - Facts []messageCardSectionFact `json:"facts,omitempty"` + return config.GetURL().String() } -type messageCardSectionFact struct { - Name string `json:"name,omitempty"` - Value string `json:"value,omitempty"` -} +func (n *msTeamsTypeNotifier) StartNotification() {} +func (n *msTeamsTypeNotifier) SendNotification() {} +func (n *msTeamsTypeNotifier) Close() {} +func (n *msTeamsTypeNotifier) Levels() []log.Level { return nil } +func (n *msTeamsTypeNotifier) Fire(entry *log.Entry) error { return nil } diff --git a/pkg/notifications/notifier.go b/pkg/notifications/notifier.go index dedb21a..dea0fc8 100644 --- a/pkg/notifications/notifier.go +++ b/pkg/notifications/notifier.go @@ -31,26 +31,48 @@ func NewNotifier(c *cobra.Command) *Notifier { if err != nil { log.WithField("could not read notifications argument", log.Fields{"Error": err}).Fatal() } + + n.types = n.GetNotificationTypes(c, acceptedLogLevels, types) + + return n +} + +// GetNotificationTypes produces an array of notifiers from a list of types +func (n *Notifier) GetNotificationTypes(cmd *cobra.Command, levels []log.Level, types []string) []ty.Notifier { + output := make([]ty.Notifier, 0) + for _, t := range types { - var tn ty.Notifier + + if t == shoutrrrType { + output = append(output, newShoutrrrNotifier(cmd, levels)) + continue + } + + var legacyNotifier ty.ConvertableNotifier + switch t { case emailType: - tn = newEmailNotifier(c, acceptedLogLevels) + legacyNotifier = newEmailNotifier(cmd, []log.Level{}) case slackType: - tn = newSlackNotifier(c, acceptedLogLevels) + legacyNotifier = newSlackNotifier(cmd, []log.Level{}) case msTeamsType: - tn = newMsTeamsNotifier(c, acceptedLogLevels) + legacyNotifier = newMsTeamsNotifier(cmd, levels) case gotifyType: - tn = newGotifyNotifier(c, acceptedLogLevels) - case shoutrrrType: - tn = newShoutrrrNotifier(c, acceptedLogLevels) + legacyNotifier = newGotifyNotifier(cmd, []log.Level{}) default: log.Fatalf("Unknown notification type %q", t) } - n.types = append(n.types, tn) + + notifier := newShoutrrrNotifierFromURL( + cmd, + legacyNotifier.GetURL(), + levels, + ) + + output = append(output, notifier) } - return n + return output } // StartNotification starts a log batch. Notifications will be accumulated after this point and only sent when SendNotification() is called. diff --git a/pkg/notifications/notifier_test.go b/pkg/notifications/notifier_test.go new file mode 100644 index 0000000..6440bbc --- /dev/null +++ b/pkg/notifications/notifier_test.go @@ -0,0 +1,163 @@ +package notifications_test + +import ( + "fmt" + "os" + "testing" + + "github.com/containrrr/watchtower/cmd" + "github.com/containrrr/watchtower/internal/flags" + "github.com/containrrr/watchtower/pkg/notifications" + "github.com/containrrr/watchtower/pkg/types" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func TestActions(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Notifier Suite") +} + +var _ = Describe("notifications", func() { + // TODO: Either, we delete this test or we need to pass it valid URLs in the cobra command. + // --- + // When("getting notifiers from a types array", func() { + // It("should return the same amount of notifiers a string entries", func() { + + // notifier := ¬ifications.Notifier{} + // notifiers := notifier.GetNotificationTypes(&cobra.Command{}, []log.Level{}, []string{"slack", "email"}) + // Expect(len(notifiers)).To(Equal(2)) + // }) + // }) + Describe("the slack notifier", func() { + When("converting a slack service config into a shoutrrr url", func() { + builderFn := notifications.NewSlackNotifier + + It("should return the expected URL", func() { + + username := "containrrrbot" + tokenA := "aaa" + tokenB := "bbb" + tokenC := "ccc" + + password := fmt.Sprintf("%s-%s-%s", tokenA, tokenB, tokenC) + hookURL := fmt.Sprintf("https://hooks.slack.com/services/%s/%s/%s", tokenA, tokenB, tokenC) + expectedOutput := fmt.Sprintf("slack://%s:%s@%s/%s/%s", username, password, tokenA, tokenB, tokenC) + + args := []string{ + "--notification-slack-hook-url", + hookURL, + "--notification-slack-identifier", + username, + } + + testURL(builderFn, args, expectedOutput) + }) + }) + }) + + Describe("the gotify notifier", func() { + When("converting a gotify service config into a shoutrrr url", func() { + builderFn := notifications.NewGotifyNotifier + + It("should return the expected URL", func() { + token := "aaa" + host := "shoutrrr.local" + + expectedOutput := fmt.Sprintf("gotify://%s/%s", host, token) + + args := []string{ + "--notification-gotify-url", + fmt.Sprintf("https://%s", host), + "--notification-gotify-token", + token, + } + + testURL(builderFn, args, expectedOutput) + }) + }) + }) + + Describe("the teams notifier", func() { + When("converting a teams service config into a shoutrrr url", func() { + builderFn := notifications.NewMsTeamsNotifier + + It("should return the expected URL", func() { + + tokenA := "aaa" + tokenB := "bbb" + tokenC := "ccc" + + hookURL := fmt.Sprintf("https://outlook.office.com/webhook/%s/IncomingWebhook/%s/%s", tokenA, tokenB, tokenC) + expectedOutput := fmt.Sprintf("teams://%s:%s@%s", tokenA, tokenB, tokenC) + + args := []string{ + "--notification-msteams-hook", + hookURL, + } + + testURL(builderFn, args, expectedOutput) + }) + }) + }) + + Describe("the email notifier", func() { + + builderFn := notifications.NewEmailNotifier + + When("converting an email service config into a shoutrrr url", func() { + It("should set the from address in the URL", func() { + fromAddress := "lala@example.com" + expectedOutput := buildExpectedURL("", "", "", 25, fromAddress, "", "None") + args := []string{ + "--notification-email-from", + fromAddress, + } + testURL(builderFn, args, expectedOutput) + }) + + It("should return the expected URL", func() { + + fromAddress := "sender@example.com" + toAddress := "receiver@example.com" + expectedOutput := buildExpectedURL("", "", "", 25, fromAddress, toAddress, "None") + + args := []string{ + "--notification-email-from", + fromAddress, + "--notification-email-to", + toAddress, + } + + testURL(builderFn, args, expectedOutput) + }) + }) + }) +}) + +func buildExpectedURL(username string, password string, host string, port int, from string, to string, auth string) string { + hostname, err := os.Hostname() + Expect(err).NotTo(HaveOccurred()) + + subject := fmt.Sprintf("Watchtower updates on %s", hostname) + + var template = "smtp://%s:%s@%s:%d/?fromAddress=%s&fromName=Watchtower&toAddresses=%s&auth=%s&subject=%s&startTls=Yes&useHTML=No" + return fmt.Sprintf(template, username, password, host, port, from, to, auth, subject) +} + +type builderFn = func(c *cobra.Command, acceptedLogLevels []log.Level) types.ConvertableNotifier + +func testURL(builder builderFn, args []string, expectedURL string) { + + command := cmd.NewRootCommand() + flags.RegisterNotificationFlags(command) + command.ParseFlags(args) + + notifier := builder(command, []log.Level{}) + actualURL := notifier.GetURL() + + Expect(actualURL).To(Equal(expectedURL)) +} diff --git a/pkg/notifications/shoutrrr.go b/pkg/notifications/shoutrrr.go index d16808d..2715711 100644 --- a/pkg/notifications/shoutrrr.go +++ b/pkg/notifications/shoutrrr.go @@ -35,8 +35,17 @@ type shoutrrrTypeNotifier struct { func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { flags := c.PersistentFlags() - urls, _ := flags.GetStringArray("notification-url") + template := getShoutrrrTemplate(c) + return createSender(urls, acceptedLogLevels, template) +} + +func newShoutrrrNotifierFromURL(c *cobra.Command, url string, levels []log.Level) t.Notifier { + template := getShoutrrrTemplate(c) + return createSender([]string{url}, levels, template) +} + +func createSender(urls []string, levels []log.Level, template *template.Template) t.Notifier { r, err := shoutrrr.CreateSender(urls...) if err != nil { log.Fatalf("Failed to initialize Shoutrrr notifications: %s\n", err.Error()) @@ -45,10 +54,10 @@ func newShoutrrrNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Noti n := &shoutrrrTypeNotifier{ Urls: urls, Router: r, - logLevels: acceptedLogLevels, - template: getShoutrrrTemplate(c), messages: make(chan string, 1), done: make(chan bool), + logLevels: levels, + template: template, } log.AddHook(n) diff --git a/pkg/notifications/slack.go b/pkg/notifications/slack.go index 5f96390..f8cbd45 100644 --- a/pkg/notifications/slack.go +++ b/pkg/notifications/slack.go @@ -1,6 +1,9 @@ package notifications import ( + "strings" + + shoutrrrSlack "github.com/containrrr/shoutrrr/pkg/services/slack" t "github.com/containrrr/watchtower/pkg/types" "github.com/johntdyer/slackrus" log "github.com/sirupsen/logrus" @@ -15,7 +18,12 @@ type slackTypeNotifier struct { slackrus.SlackrusHook } -func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifier { +// NewSlackNotifier is a factory function used to generate new instance of the slack notifier type +func NewSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { + return newSlackNotifier(c, acceptedLogLevels) +} + +func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.ConvertableNotifier { flags := c.PersistentFlags() hookURL, _ := flags.GetString("notification-slack-hook-url") @@ -23,7 +31,6 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie channel, _ := flags.GetString("notification-slack-channel") emoji, _ := flags.GetString("notification-slack-icon-emoji") iconURL, _ := flags.GetString("notification-slack-icon-url") - n := &slackTypeNotifier{ SlackrusHook: slackrus.SlackrusHook{ HookURL: hookURL, @@ -34,12 +41,27 @@ func newSlackNotifier(c *cobra.Command, acceptedLogLevels []log.Level) t.Notifie AcceptedLevels: acceptedLogLevels, }, } - - log.AddHook(n) return n } -func (s *slackTypeNotifier) StartNotification() {} +func (s *slackTypeNotifier) GetURL() string { + rawTokens := strings.Replace(s.HookURL, "https://hooks.slack.com/services/", "", 1) + tokens := strings.Split(rawTokens, "/") + + conf := &shoutrrrSlack.Config{ + BotName: s.Username, + Token: shoutrrrSlack.Token{ + A: tokens[0], + B: tokens[1], + C: tokens[2], + }, + } + + return conf.GetURL().String() +} + +func (s *slackTypeNotifier) StartNotification() { +} func (s *slackTypeNotifier) SendNotification() {} diff --git a/pkg/types/convertable_notifier.go b/pkg/types/convertable_notifier.go new file mode 100644 index 0000000..3d7ac82 --- /dev/null +++ b/pkg/types/convertable_notifier.go @@ -0,0 +1,7 @@ +package types + +// ConvertableNotifier is a notifier capable of creating a shoutrrr URL +type ConvertableNotifier interface { + Notifier + GetURL() string +}