Skip to content

Commit

Permalink
Add option for Teams Webhook
Browse files Browse the repository at this point in the history
Add option to most messages via Teams / Office365 Connectors
  • Loading branch information
Leif Segen committed Sep 8, 2020
1 parent 9200f9a commit 15b17f8
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 0 deletions.
2 changes: 2 additions & 0 deletions chart/keel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ The following table lists has the main configurable parameters (polling, trigger
| `slack.token` | Slack token | |
| `slack.channel` | Slack channel | |
| `slack.approvalsChannel` | Slack channel for approvals | |
| `teams.enabled` | Enable/disable MS Teams Notification | `false` |
| `teams.webhookUrl` | MS Teams Connector's webhook url | |
| `service.enabled` | Enable/disable Keel service | `false` |
| `service.type` | Keel service type | `LoadBalancer` |
| `service.externalIP` | Keel static IP | |
Expand Down
3 changes: 3 additions & 0 deletions chart/keel/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ data:
HIPCHAT_TOKEN: {{ .Values.hipchat.token | b64enc}}
HIPCHAT_APPROVALS_PASSWORT: {{ .Values.hipchat.password | b64enc }}
{{- end }}
{{- if .Values.teams.enabled }}
TEAMS_WEBHOOK_URL: {{ .Values.teams.webhookUrl | b64enc }}
{{- end }}
{{- if and .Values.mail.enabled .Values.mail.smtp.pass }}
MAIL_SMTP_PASS: {{ .Values.mail.smtp.pass | b64enc }}
{{- end }}
Expand Down
6 changes: 6 additions & 0 deletions chart/keel/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,16 @@ hipchat:
userName: ""
password: ""

# Mattermost notifications
mattermost:
enabled: false
endpoint: ""

# MS Teams notifications
teams:
enabled: false
webhookUrl: ""

# Mail notifications
mail:
enabled: false
Expand Down
3 changes: 3 additions & 0 deletions constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const (
EnvMattermostEndpoint = "MATTERMOST_ENDPOINT"
EnvMattermostName = "MATTERMOST_USERNAME"

// 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"

// Mail notification settings
EnvMailTo = "MAIL_TO"
EnvMailFrom = "MAIL_FROM"
Expand Down
3 changes: 3 additions & 0 deletions deployment/deployment-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ spec:
# Enable mattermost endpoint
- name: MATTERMOST_ENDPOINT
value: ""
# Enable MS Teams webhook endpoint
- name: TEAMS_WEBHOOK_URL
value: "{{ .teams_webhook_url }}" # Following same pattern as with adjacent examples. However, I can't see why this would work. (There is no top level "slack_channel" in the values.yaml. Nor is there the ".Values" prefix as with deployment.yaml.) Maybe my issue here is that I don't know how this depoyment-template.yaml file is used. I assume deployment.yaml would be the primary one in use.
- name: SLACK_TOKEN
value: "{{ .slack_token }}"
- name: SLACK_CHANNELS
Expand Down
35 changes: 35 additions & 0 deletions extension/notification/teams/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.7/MicrosoftTeams.schema.json",
"manifestVersion": "0.1",
"id": "e9343a03-0a5e-4c1f-95a8-263a565505a5", // get from microsoft teams?
"version": "1.0",
"packageName": "sh.keel",
"developer": {
"name": "Publisher",
"websiteUrl": "https://keel.sh",
"privacyUrl": "https://www.microsoft.com",
"termsOfUseUrl": "https://www.microsoft.com"
},
"description": {
"full": "This is a small sample app we made for you! This app has samples of all capabilities Microsoft Teams supports.",
"short": "This is a small sample app we made for you!"
},
"icons": {
"outline": "sampleapp-outline.png",
"color": "sampleapp-color.png"
},
"connectors": [
{
"connectorId": "e9343a03-0a5e-4c1f-95a8-263a565505a5", // get from microsoft teams?
"scopes": [
"team"
]
}
],
"name": {
"short": "Keel",
"full": "Keel"
},
"accentColor": "#46bd87",
"needsIdentity": "true"
}
145 changes: 145 additions & 0 deletions extension/notification/teams/teams.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package teams

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 Teams Webhook Sender.
type Config struct {
Endpoint string
}

func init() {
notification.RegisterSender("teams", &sender{})
}

func (s *sender) Configure(config *notification.Config) (bool, error) {
// Get configuration
var httpConfig Config

if os.Getenv(constants.EnvTeamsWebhookUrl) != "" {
httpConfig.Endpoint = os.Getenv(constants.EnvTeamsWebhookUrl)
} 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\n", err)
}
s.endpoint = httpConfig.Endpoint

// Setup HTTP client.
s.client = &http.Client{
Transport: http.DefaultTransport,
Timeout: timeout,
}

log.WithFields(log.Fields{
"name": "teams",
"webhook": s.endpoint,
}).Info("extension.notification.teams: sender configured")

return true, nil
}

type notificationEnvelope struct {
types.EventNotification
}

type SimpleTeamsMessageCard struct {
_Context string `json:"@context"`
_Type string `json:"@type"`
Sections []TeamsMessageSection `json:"sections"`
Summary string `json:"summary"`
ThemeColor string `json:"themeColor"`
}

type TeamsMessageSection struct {
ActivityImage string `json:"activityImage"`
ActivitySubtitle string `json:"activitySubtitle"`
ActivityText string `json:"activityText"`
ActivityTitle string `json:"activityTitle"`
Facts []TeamsFact `json:"facts"`
Markdown bool `json:"markdown"`
}

type TeamsFact struct {
Name string `json:"name"`
Value string `json:"value"`
}

// Microsoft Teams expects the hexidecimal formatted color to not have a "#" at the front
// Source: https://stackoverflow.com/a/48798875/2199949
func trimFirstChar(s string) string {
for i := range s {
if i > 0 {
// The value i is the index in s of the second
// character. Slice to remove the first character.
return s[i:]
}
}
// There are 0 or 1 characters in the string.
return ""
}

func (s *sender) Send(event types.EventNotification) error {
// Marshal notification.
jsonNotification, err := json.Marshal(simpleTeamsMessageCard{
_Type: "MessageCard",
_Context: "http://schema.org/extensions",
ThemeColor: trimFirstChar(event.Level.Color()),
Summary: event.Type.String(),
Sections: []TeamsMessageSection{
{
ActivityImage: constants.KeelLogoURL,
ActivityText: event.Message,
ActivityTitle: "**" + event.Type.String() + "**"
},
[]TeamsFact{
{
Name: "Version",
Value: fmt.Sprintf("https://keel.sh %s", version.GetKeelVersion().Version)
}
},
Markdown: true
}
})
if err != nil {
return fmt.Errorf("could not marshal: %s", err)
}

// Send notification via HTTP POST.
resp, err := s.client.Post(s.endpoint, "application/json", bytes.NewBuffer(jsonNotification))
if err != nil || resp == nil || (resp.StatusCode != 200 && resp.StatusCode != 201) {
if resp != nil {
return fmt.Errorf("got status %d, expected 200/201", resp.StatusCode)
}
return err
}
defer resp.Body.Close()

return nil
}
70 changes: 70 additions & 0 deletions extension/notification/teams/teams_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package teams

import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"fmt"

"github.com/keel-hq/keel/types"
"github.com/keel-hq/keel/extension/notification/teams"
)

func TestTrimLeftChar() {
fmt.Printf("%q\n", "Hello, 世界")
fmt.Printf("%q\n", teams.trimLeftChar(""))
fmt.Printf("%q\n", teams.trimLeftChar("H"))
fmt.Printf("%q\n", teams.trimLeftChar("世"))
fmt.Printf("%q\n", teams.trimLeftChar("Hello"))
fmt.Printf("%q\n", teams.trimLeftChar("世界"))
}

func TestTeamsRequest(t *testing.T) {
currentTime := time.Now()
handler := func(resp http.ResponseWriter, req *http.Request) {
body, err := ioutil.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{
webhook: ts.URL,
client: &http.Client{},
}

s.Send(types.EventNotification{
Name: "update deployment",
Message: "message here",
CreatedAt: currentTime,
Type: types.NotificationPreDeploymentUpdate,
Level: types.LevelDebug,
})
}

0 comments on commit 15b17f8

Please sign in to comment.