From 2fde6548fcae5459f6d9aa4775405d16bf59bebe Mon Sep 17 00:00:00 2001
From: CrazyMax <1951866+crazy-max@users.noreply.github.com>
Date: Mon, 16 Dec 2024 02:28:25 +0100
Subject: [PATCH] notif: support webhook url as secret

---
 docs/notif/discord.md            |  4 +++-
 docs/notif/slack.md              |  4 +++-
 docs/notif/teams.md              |  4 +++-
 internal/model/notif_discord.go  | 11 ++++++-----
 internal/model/notif_slack.go    |  7 ++++---
 internal/model/notif_teams.go    |  7 ++++---
 internal/notif/discord/client.go |  8 +++++++-
 internal/notif/slack/client.go   |  9 ++++++++-
 internal/notif/teams/client.go   |  9 ++++++++-
 9 files changed, 46 insertions(+), 17 deletions(-)

diff --git a/docs/notif/discord.md b/docs/notif/discord.md
index 17ee5da2..b4f5afbe 100644
--- a/docs/notif/discord.md
+++ b/docs/notif/discord.md
@@ -23,7 +23,8 @@ Allow sending notifications to your Discord channel.
 
 | Name               | Default                            | Description                                                                                               |
 |--------------------|------------------------------------|-----------------------------------------------------------------------------------------------------------|
-| `webhookURL`[^1]   |                                    | Discord [incoming webhook URL](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) |
+| `webhookURL`       |                                    | Discord [incoming webhook URL](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks) |
+| `webhookURLFile`   |                                    | Use content of secret file as webhook URL if `webhookURL` is not defined                                  |
 | `mentions`         |                                    | List of users or roles to notify                                                                          |
 | `renderFields`     | `true`                             | Render [field objects](https://discordjs.guide/popular-topics/embeds.html)                                |
 | `timeout`          | `10s`                              | Timeout specifies a time limit for the request to be made                                                 |
@@ -31,6 +32,7 @@ Allow sending notifications to your Discord channel.
 
 !!! abstract "Environment variables"
     * `DIUN_NOTIF_DISCORD_WEBHOOKURL`
+    * `DIUN_NOTIF_DISCORD_WEBHOOKURLFILE`
     * `DIUN_NOTIF_DISCORD_MENTIONS` (comma separated)
     * `DIUN_NOTIF_DISCORD_RENDERFIELDS`
     * `DIUN_NOTIF_DISCORD_TIMEOUT`
diff --git a/docs/notif/slack.md b/docs/notif/slack.md
index ed8f9fc3..5aec92a3 100644
--- a/docs/notif/slack.md
+++ b/docs/notif/slack.md
@@ -19,12 +19,14 @@ You can send notifications to your Slack channel using an [incoming webhook URL]
 
 | Name               | Default                            | Description                                                                               |
 |--------------------|------------------------------------|-------------------------------------------------------------------------------------------|
-| `webhookURL`[^1]   |                                    | Slack [incoming webhook URL](https://api.slack.com/messaging/webhooks)                    |
+| `webhookURL`       |                                    | Slack [incoming webhook URL](https://api.slack.com/messaging/webhooks)                    |
+| `webhookURLFile`   |                                    | Use content of secret file as webhook URL if `webhookURL` is not defined                  |
 | `renderFields`     | `true`                             | Render [field objects](https://api.slack.com/messaging/composing/layouts#stack_of_blocks) |
 | `templateBody`[^1] | See [below](#default-templatebody) | [Notification template](../faq.md#notification-template) for message body                 |
 
 !!! abstract "Environment variables"
     * `DIUN_NOTIF_SLACK_WEBHOOKURL`
+    * `DIUN_NOTIF_SLACK_WEBHOOKURLFILE`
     * `DIUN_NOTIF_SLACK_RENDERFIELDS`
     * `DIUN_NOTIF_SLACK_TEMPLATEBODY`
 
diff --git a/docs/notif/teams.md b/docs/notif/teams.md
index 0a48217d..ae6044b7 100644
--- a/docs/notif/teams.md
+++ b/docs/notif/teams.md
@@ -16,12 +16,14 @@ You can send notifications to your Teams team-channel using an [incoming webhook
 
 | Name               | Default                            | Description                                                                                                                                     |
 |--------------------|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
-| `webhookURL`[^1]   |                                    | Teams [incoming webhook URL](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors) |
+| `webhookURL`       |                                    | Teams [incoming webhook URL](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors) |
+| `webhookURLFile`   |                                    | Use content of secret file as webhook URL if `webhookURL` is not defined                                                                        |
 | `renderFacts`      | `true`                             | Render fact objects                                                                                                                             |
 | `templateBody`[^1] | See [below](#default-templatebody) | [Notification template](../faq.md#notification-template) for message body                                                                       |
 
 !!! abstract "Environment variables"
     * `DIUN_NOTIF_TEAMS_WEBHOOKURL`
+    * `DIUN_NOTIF_TEAMS_WEBHOOKURLFILE`
     * `DIUN_NOTIF_TEAMS_RENDERFACTS`
     * `DIUN_NOTIF_TEAMS_TEMPLATEBODY`
 
diff --git a/internal/model/notif_discord.go b/internal/model/notif_discord.go
index 81627eef..6ecae20d 100644
--- a/internal/model/notif_discord.go
+++ b/internal/model/notif_discord.go
@@ -8,11 +8,12 @@ import (
 
 // NotifDiscord holds Discord notification configuration details
 type NotifDiscord struct {
-	WebhookURL   string         `yaml:"webhookURL,omitempty" json:"webhookURL,omitempty" validate:"required"`
-	Mentions     []string       `yaml:"mentions,omitempty" json:"mentions,omitempty"`
-	RenderFields *bool          `yaml:"renderFields,omitempty" json:"renderFields,omitempty" validate:"required"`
-	Timeout      *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"`
-	TemplateBody string         `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"`
+	WebhookURL     string         `yaml:"webhookURL,omitempty" json:"webhookURL,omitempty" validate:"omitempty"`
+	WebhookURLFile string         `yaml:"webhookURLFile,omitempty" json:"webhookURLFile,omitempty" validate:"omitempty,file"`
+	Mentions       []string       `yaml:"mentions,omitempty" json:"mentions,omitempty"`
+	RenderFields   *bool          `yaml:"renderFields,omitempty" json:"renderFields,omitempty" validate:"required"`
+	Timeout        *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"`
+	TemplateBody   string         `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"`
 }
 
 // GetDefaults gets the default values
diff --git a/internal/model/notif_slack.go b/internal/model/notif_slack.go
index 958f5c20..c1ba57b2 100644
--- a/internal/model/notif_slack.go
+++ b/internal/model/notif_slack.go
@@ -7,9 +7,10 @@ const NotifSlackDefaultTemplateBody = "<!channel> Docker tag {{ if .Entry.Image.
 
 // NotifSlack holds slack notification configuration details
 type NotifSlack struct {
-	WebhookURL   string `yaml:"webhookURL,omitempty" json:"webhookURL,omitempty" validate:"required"`
-	RenderFields *bool  `yaml:"renderFields,omitempty" json:"renderFields,omitempty" validate:"required"`
-	TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"`
+	WebhookURL     string `yaml:"webhookURL,omitempty" json:"webhookURL,omitempty" validate:"omitempty"`
+	WebhookURLFile string `yaml:"webhookURLFile,omitempty" json:"webhookURLFile,omitempty" validate:"omitempty,file"`
+	RenderFields   *bool  `yaml:"renderFields,omitempty" json:"renderFields,omitempty" validate:"required"`
+	TemplateBody   string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"`
 }
 
 // GetDefaults gets the default values
diff --git a/internal/model/notif_teams.go b/internal/model/notif_teams.go
index 1a047eb8..792f5528 100644
--- a/internal/model/notif_teams.go
+++ b/internal/model/notif_teams.go
@@ -7,9 +7,10 @@ const NotifTeamsDefaultTemplateBody = "Docker tag {{ if .Entry.Image.HubLink }}[
 
 // NotifTeams holds Teams notification configuration details
 type NotifTeams struct {
-	WebhookURL   string `yaml:"webhookURL,omitempty" json:"webhookURL,omitempty" validate:"required"`
-	RenderFacts  *bool  `yaml:"renderFacts,omitempty" json:"renderFacts,omitempty" validate:"required"`
-	TemplateBody string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"`
+	WebhookURL     string `yaml:"webhookURL,omitempty" json:"webhookURL,omitempty" validate:"omitempty"`
+	WebhookURLFile string `yaml:"webhookURLFile,omitempty" json:"webhookURLFile,omitempty" validate:"omitempty,file"`
+	RenderFacts    *bool  `yaml:"renderFacts,omitempty" json:"renderFacts,omitempty" validate:"required"`
+	TemplateBody   string `yaml:"templateBody,omitempty" json:"templateBody,omitempty" validate:"required"`
 }
 
 // GetDefaults gets the default values
diff --git a/internal/notif/discord/client.go b/internal/notif/discord/client.go
index bb07b5a8..5b5fd157 100644
--- a/internal/notif/discord/client.go
+++ b/internal/notif/discord/client.go
@@ -12,6 +12,7 @@ import (
 	"github.com/crazy-max/diun/v4/internal/model"
 	"github.com/crazy-max/diun/v4/internal/msg"
 	"github.com/crazy-max/diun/v4/internal/notif/notifier"
+	"github.com/crazy-max/diun/v4/pkg/utl"
 	"github.com/pkg/errors"
 )
 
@@ -42,6 +43,11 @@ func (c *Client) Name() string {
 func (c *Client) Send(entry model.NotifEntry) error {
 	var content bytes.Buffer
 
+	webhookURL, err := utl.GetSecret(c.cfg.WebhookURL, c.cfg.WebhookURLFile)
+	if err != nil {
+		return errors.Wrap(err, "cannot retrieve webhook URL for Discord notifier")
+	}
+
 	message, err := msg.New(msg.Options{
 		Meta:         c.meta,
 		Entry:        entry,
@@ -117,7 +123,7 @@ func (c *Client) Send(entry model.NotifEntry) error {
 		return err
 	}
 
-	u, err := url.Parse(c.cfg.WebhookURL)
+	u, err := url.Parse(webhookURL)
 	if err != nil {
 		return err
 	}
diff --git a/internal/notif/slack/client.go b/internal/notif/slack/client.go
index 31e22a53..13005d95 100644
--- a/internal/notif/slack/client.go
+++ b/internal/notif/slack/client.go
@@ -9,7 +9,9 @@ import (
 	"github.com/crazy-max/diun/v4/internal/model"
 	"github.com/crazy-max/diun/v4/internal/msg"
 	"github.com/crazy-max/diun/v4/internal/notif/notifier"
+	"github.com/crazy-max/diun/v4/pkg/utl"
 	"github.com/nlopes/slack"
+	"github.com/pkg/errors"
 )
 
 // Client represents an active slack notification object
@@ -36,6 +38,11 @@ func (c *Client) Name() string {
 
 // Send creates and sends a slack notification with an entry
 func (c *Client) Send(entry model.NotifEntry) error {
+	webhookURL, err := utl.GetSecret(c.cfg.WebhookURL, c.cfg.WebhookURLFile)
+	if err != nil {
+		return errors.Wrap(err, "cannot retrieve webhook URL for Slack notifier")
+	}
+
 	message, err := msg.New(msg.Options{
 		Meta:         c.meta,
 		Entry:        entry,
@@ -93,7 +100,7 @@ func (c *Client) Send(entry model.NotifEntry) error {
 		color = "#0054ca"
 	}
 
-	return slack.PostWebhook(c.cfg.WebhookURL, &slack.WebhookMessage{
+	return slack.PostWebhook(webhookURL, &slack.WebhookMessage{
 		Attachments: []slack.Attachment{
 			{
 				Color:         color,
diff --git a/internal/notif/teams/client.go b/internal/notif/teams/client.go
index bbaa5470..200bbec0 100644
--- a/internal/notif/teams/client.go
+++ b/internal/notif/teams/client.go
@@ -11,6 +11,8 @@ import (
 	"github.com/crazy-max/diun/v4/internal/model"
 	"github.com/crazy-max/diun/v4/internal/msg"
 	"github.com/crazy-max/diun/v4/internal/notif/notifier"
+	"github.com/crazy-max/diun/v4/pkg/utl"
+	"github.com/pkg/errors"
 )
 
 // Client represents an active webhook notification object
@@ -50,6 +52,11 @@ type Fact struct {
 
 // Send creates and sends a webhook notification with an entry
 func (c *Client) Send(entry model.NotifEntry) error {
+	webhookURL, err := utl.GetSecret(c.cfg.WebhookURL, c.cfg.WebhookURLFile)
+	if err != nil {
+		return errors.Wrap(err, "cannot retrieve webhook URL for Teams notifier")
+	}
+
 	message, err := msg.New(msg.Options{
 		Meta:         c.meta,
 		Entry:        entry,
@@ -107,7 +114,7 @@ func (c *Client) Send(entry model.NotifEntry) error {
 	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
 	defer cancel()
 
-	req, err := http.NewRequestWithContext(ctx, "POST", c.cfg.WebhookURL, bytes.NewBuffer(jsonBody))
+	req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(jsonBody))
 	if err != nil {
 		return err
 	}