diff --git a/chart/keel/templates/secret.yaml b/chart/keel/templates/secret.yaml index 80fdde879..4151d9504 100644 --- a/chart/keel/templates/secret.yaml +++ b/chart/keel/templates/secret.yaml @@ -27,6 +27,9 @@ data: {{- if .Values.teams.enabled }} TEAMS_WEBHOOK_URL: {{ .Values.teams.webhookUrl | b64enc }} {{- end }} +{{- if .Values.discord.enabled }} + DISCORD_WEBHOOK_URL: {{ .Values.discord.webhookUrl | b64enc }} +{{- end }} {{- if and .Values.mail.enabled .Values.mail.smtp.pass }} MAIL_SMTP_PASS: {{ .Values.mail.smtp.pass | b64enc }} {{- end }} diff --git a/chart/keel/values.yaml b/chart/keel/values.yaml index bc028632d..79aa3d6f6 100644 --- a/chart/keel/values.yaml +++ b/chart/keel/values.yaml @@ -93,6 +93,11 @@ teams: enabled: false webhookUrl: "" +# Discord notifications +discord: + enabled: false + webhookUrl: "" + # Mail notifications mail: enabled: false diff --git a/cmd/keel/main.go b/cmd/keel/main.go index 2cbec24a7..dbfecde6b 100644 --- a/cmd/keel/main.go +++ b/cmd/keel/main.go @@ -39,6 +39,7 @@ import ( // notification extensions "github.com/keel-hq/keel/extension/notification/auditor" + _ "github.com/keel-hq/keel/extension/notification/discord" _ "github.com/keel-hq/keel/extension/notification/hipchat" _ "github.com/keel-hq/keel/extension/notification/mail" _ "github.com/keel-hq/keel/extension/notification/mattermost" diff --git a/constants/constants.go b/constants/constants.go index 12d95cff2..787b7cbba 100644 --- a/constants/constants.go +++ b/constants/constants.go @@ -34,6 +34,9 @@ const ( // MS Teams webhook url, see https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using#setting-up-a-custom-incoming-webhook EnvTeamsWebhookUrl = "TEAMS_WEBHOOK_URL" + // Discord webhook url, see https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks + EnvDiscordWebhookUrl = "DISCORD_WEBHOOK_URL" + // Mail notification settings EnvMailTo = "MAIL_TO" EnvMailFrom = "MAIL_FROM" diff --git a/extension/notification/discord/discord.go b/extension/notification/discord/discord.go new file mode 100644 index 000000000..5af00a068 --- /dev/null +++ b/extension/notification/discord/discord.go @@ -0,0 +1,112 @@ +package discord + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "time" + + "github.com/keel-hq/keel/constants" + "github.com/keel-hq/keel/extension/notification" + "github.com/keel-hq/keel/types" + + log "github.com/sirupsen/logrus" +) + +const timeout = 5 * time.Second + +type sender struct { + endpoint string + client *http.Client +} + +// Config represents the configuration of a Discord Webhook Sender. +type Config struct { + Endpoint string +} + +func init() { + log.Info(0) + notification.RegisterSender("discord", &sender{}) +} + +func (s *sender) Configure(config *notification.Config) (bool, error) { + log.Info(1) + // Get configuration + var httpConfig Config + + if os.Getenv(constants.EnvDiscordWebhookUrl) != "" { + httpConfig.Endpoint = os.Getenv(constants.EnvDiscordWebhookUrl) + } else { + return false, nil + } + // Validate endpoint URL. + if httpConfig.Endpoint == "" { + return false, nil + } + if _, err := url.ParseRequestURI(httpConfig.Endpoint); err != nil { + return false, fmt.Errorf("could not parse endpoint URL: %s", err) + } + s.endpoint = httpConfig.Endpoint + + // Setup HTTP client. + s.client = &http.Client{ + Transport: http.DefaultTransport, + Timeout: timeout, + } + + log.WithFields(log.Fields{ + "name": "discord", + "endpoint": s.endpoint, + }).Info("extension.notification.discord: sender configured") + log.Info(2) + return true, nil +} + +type DiscordMessage struct { + Username string `json:"username"` + Content string `json:"content"` + Embeds []Embed `json:"embeds"` +} + +type Embed struct { + Title string `json:"title"` + Description string `json:"description"` + Footer Footer `json:"footer"` +} + +type Footer struct { + Text string `json:"text"` +} + +func (s *sender) Send(event types.EventNotification) error { + discordMessage := DiscordMessage{ + Username: "Keel", + Embeds: []Embed{ + { + Title: fmt.Sprintf("%s: %s", event.Type.String(), event.Name), + Description: event.Message, + Footer: Footer{Text: event.Level.String()}, + }, + }, + } + + jsonMessage, err := json.Marshal(discordMessage) + if err != nil { + return fmt.Errorf("could not marshal: %s", err) + } + + resp, err := s.client.Post(s.endpoint, "application/json", bytes.NewBuffer(jsonMessage)) + if err != nil || resp == nil || (resp.StatusCode != 200 && resp.StatusCode != 204) { + if resp != nil { + return fmt.Errorf("got status %d, expected 200/204", resp.StatusCode) + } + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/extension/notification/discord/discord_test.go b/extension/notification/discord/discord_test.go new file mode 100644 index 000000000..0e05cb440 --- /dev/null +++ b/extension/notification/discord/discord_test.go @@ -0,0 +1,58 @@ +package discord + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/keel-hq/keel/types" +) + +func TestDiscordWebhookRequest(t *testing.T) { + currentTime := time.Now() + handler := func(resp http.ResponseWriter, req *http.Request) { + body, err := io.ReadAll(req.Body) + if err != nil { + t.Errorf("failed to parse body: %s", err) + } + + bodyStr := string(body) + + if !strings.Contains(bodyStr, types.NotificationPreDeploymentUpdate.String()) { + t.Errorf("missing deployment type") + } + + if !strings.Contains(bodyStr, "debug") { + t.Errorf("missing level") + } + + if !strings.Contains(bodyStr, "update deployment") { + t.Errorf("missing name") + } + if !strings.Contains(bodyStr, "message here") { + t.Errorf("missing message") + } + + t.Log(bodyStr) + } + + // create test server with handler + ts := httptest.NewServer(http.HandlerFunc(handler)) + defer ts.Close() + + s := &sender{ + endpoint: ts.URL, + client: &http.Client{}, + } + + s.Send(types.EventNotification{ + Name: "update deployment", + Message: "message here", + CreatedAt: currentTime, + Type: types.NotificationPreDeploymentUpdate, + Level: types.LevelDebug, + }) +}