Add option for Teams Webhook
Add option to most messages via Teams / Office365 Connectorspull/541/head
parent
9200f9a7da
commit
15b17f87e5
|
@ -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 | |
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue