Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added Microsoft Teams Notifier #2636

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7f9b1ed
Updates
abdegenius Oct 27, 2024
217116f
Updates
abdegenius Oct 30, 2024
8f68cdd
Updates
abdegenius Nov 5, 2024
efef4d0
Updates
abdegenius Nov 5, 2024
2ed3fb1
Updates
abdegenius Nov 5, 2024
9f2394f
Merge pull request #3 from abdegenius/main
martinvibes Nov 5, 2024
2d74ae7
Updates
abdegenius Nov 6, 2024
3a5a80d
Updates
abdegenius Nov 6, 2024
45fb74e
Merge pull request #4 from abdegenius/main
martinvibes Nov 6, 2024
919ff67
Updates
abdegenius Nov 6, 2024
ddfab8a
Merge pull request #5 from abdegenius/main
martinvibes Nov 6, 2024
9006fdf
Merge branch 'thomaspoignant:main' into main
martinvibes Nov 6, 2024
9cc0cdd
Updates
abdegenius Nov 6, 2024
3da8e50
Merge pull request #6 from abdegenius/main
martinvibes Nov 6, 2024
254c763
Merge branch 'thomaspoignant:main' into main
martinvibes Nov 6, 2024
e189156
Updates
abdegenius Nov 6, 2024
d7a5a07
Merge pull request #7 from abdegenius/main
martinvibes Nov 6, 2024
01bf518
updates
martinvibes Nov 6, 2024
4ceff4d
Merge branch 'main' into notify_microsoft
martinvibes Nov 6, 2024
6e7f59c
Updates
abdegenius Nov 6, 2024
7058977
Merge pull request #8 from abdegenius/main
martinvibes Nov 6, 2024
44ce9e1
Merge branch 'main' into notify_microsoft
martinvibes Nov 6, 2024
7a59dd1
updates
martinvibes Nov 6, 2024
90db4e0
Updates
abdegenius Nov 6, 2024
5f2eeab
Merge pull request #9 from abdegenius/main
martinvibes Nov 6, 2024
46eea19
Merge branch 'main' into notify_microsoft
martinvibes Nov 6, 2024
b9a8d50
Updates
abdegenius Nov 6, 2024
c6c1e26
Merge pull request #10 from abdegenius/main
martinvibes Nov 6, 2024
b7e4992
Merge branch 'main' into notify_microsoft
martinvibes Nov 6, 2024
b10e670
Updates
abdegenius Nov 6, 2024
5e1ba71
Merge pull request #11 from abdegenius/main
martinvibes Nov 6, 2024
cb33cc1
Merge branch 'main' into notify_microsoft
martinvibes Nov 6, 2024
0ac6b73
updates
martinvibes Nov 6, 2024
124aab7
fix: Remove changes for contributing.md
thomaspoignant Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ Available notifiers are:
- **Slack**
- **Webhook**
- **Discord**
- **Microsoft Teams**

## Export data
**GO Feature Flag** allows you to export data about the usage of your flags.
Expand Down
12 changes: 8 additions & 4 deletions cmd/relayproxy/config/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
if c.Kind == SlackNotifier && (c.SlackWebhookURL == "" && c.WebhookURL == "") {
return fmt.Errorf("invalid notifier: no \"slackWebhookUrl\" property found for kind \"%s\"", c.Kind)
}
if c.Kind == MicrosoftTeamsNotifier && c.WebhookURL == "" {
return fmt.Errorf("invalid notifier: no \"WebhookURL\" property found for kind \"%s\"", c.Kind)
}

Check warning on line 25 in cmd/relayproxy/config/notifier.go

View check run for this annotation

Codecov / codecov/patch

cmd/relayproxy/config/notifier.go#L24-L25

Added lines #L24 - L25 were not covered by tests
if c.Kind == WebhookNotifier && c.EndpointURL == "" {
return fmt.Errorf("invalid notifier: no \"endpointUrl\" property found for kind \"%s\"", c.Kind)
}
Expand All @@ -32,15 +35,16 @@
type NotifierKind string

const (
SlackNotifier NotifierKind = "slack"
WebhookNotifier NotifierKind = "webhook"
DiscordNotifier NotifierKind = "discord"
SlackNotifier NotifierKind = "slack"
MicrosoftTeamsNotifier NotifierKind = "microsoftteams"
WebhookNotifier NotifierKind = "webhook"
DiscordNotifier NotifierKind = "discord"
)

// IsValid is checking if the value is part of the enum
func (r NotifierKind) IsValid() error {
switch r {
case SlackNotifier, WebhookNotifier, DiscordNotifier:
case SlackNotifier, WebhookNotifier, DiscordNotifier, MicrosoftTeamsNotifier:
return nil
}
return fmt.Errorf("invalid notifier: kind \"%s\" is not supported", r)
Expand Down
9 changes: 8 additions & 1 deletion cmd/relayproxy/service/gofeatureflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"github.com/thomaspoignant/go-feature-flag/exporter/webhookexporter"
"github.com/thomaspoignant/go-feature-flag/notifier"
"github.com/thomaspoignant/go-feature-flag/notifier/discordnotifier"
"github.com/thomaspoignant/go-feature-flag/notifier/microsoftteamsnotifier"
"github.com/thomaspoignant/go-feature-flag/notifier/slacknotifier"
"github.com/thomaspoignant/go-feature-flag/notifier/webhooknotifier"
"github.com/thomaspoignant/go-feature-flag/retriever"
Expand Down Expand Up @@ -322,7 +323,13 @@
cNotif.WebhookURL = cNotif.SlackWebhookURL // nolint
}
notifiers = append(notifiers, &slacknotifier.Notifier{SlackWebhookURL: cNotif.WebhookURL})

case config.MicrosoftTeamsNotifier:
notifiers = append(
notifiers,
&microsoftteamsnotifier.Notifier{
MicrosoftTeamsWebhookURL: cNotif.WebhookURL,
},
)

Check warning on line 332 in cmd/relayproxy/service/gofeatureflag.go

View check run for this annotation

Codecov / codecov/patch

cmd/relayproxy/service/gofeatureflag.go#L326-L332

Added lines #L326 - L332 were not covered by tests
case config.WebhookNotifier:
notifiers = append(notifiers,
&webhooknotifier.Notifier{
Expand Down
212 changes: 212 additions & 0 deletions notifier/microsoftteamsnotifier/notifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package microsoftteamsnotifier

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"sort"
"strings"
"sync"

"github.com/luci/go-render/render"
"github.com/r3labs/diff/v3"
"github.com/thomaspoignant/go-feature-flag/internal"
"github.com/thomaspoignant/go-feature-flag/notifier"
)

const (
colorDeleted = "#FF0000"
colorUpdated = "#FFA500"
colorAdded = "#008000"
goFFLogo = "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png"
microsoftteamsFooter = "go-feature-flag"
longMicrosoftTeamsAttachment = 100
)

type Notifier struct {
MicrosoftTeamsWebhookURL string

httpClient internal.HTTPClient
init sync.Once
}

func (c *Notifier) Notify(diff notifier.DiffCache) error {
if c.MicrosoftTeamsWebhookURL == "" {
return fmt.Errorf("error: (Microsoft Teams Notifier) invalid notifier configuration, no " +
"MicrosoftTeamsWebhookURL provided for the microsoft teams notifier")
}

// init the notifier
c.init.Do(func() {
if c.httpClient == nil {
c.httpClient = internal.DefaultHTTPClient()
}

Check warning on line 47 in notifier/microsoftteamsnotifier/notifier.go

View check run for this annotation

Codecov / codecov/patch

notifier/microsoftteamsnotifier/notifier.go#L46-L47

Added lines #L46 - L47 were not covered by tests
})

microsoftteamsURL, err := url.Parse(c.MicrosoftTeamsWebhookURL)
if err != nil {
return fmt.Errorf("error: (Microsoft Teams Notifier) invalid MicrosoftTeamsWebhookURL: %v",
c.MicrosoftTeamsWebhookURL)
}

reqBody := convertToMicrosoftTeamsMessage(diff)
payload, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("error: (Microsoft Teams Notifier) impossible to read differences; %v", err)
}

Check warning on line 60 in notifier/microsoftteamsnotifier/notifier.go

View check run for this annotation

Codecov / codecov/patch

notifier/microsoftteamsnotifier/notifier.go#L59-L60

Added lines #L59 - L60 were not covered by tests
microsoftTeamAccessToken := os.Getenv("MICROSOFT_TEAMS_ACCESS_TOKEN")
request := http.Request{
Method: http.MethodPost,
URL: microsoftteamsURL,
Body: io.NopCloser(bytes.NewReader(payload)),
Header: map[string][]string{
"Content-Type": {"application/json"},
"Authorization": {"Bearer " + microsoftTeamAccessToken},
},
}
response, err := c.httpClient.Do(&request)
if err != nil {
return fmt.Errorf("error: (Microsoft Teams Notifier) error: while calling webhook: %v", err)
}

defer func() { _ = response.Body.Close() }()
if response.StatusCode > 399 {
return fmt.Errorf("error: (Microsoft Teams Notifier) while calling microsoft teams webhook, statusCode = %d",
response.StatusCode)
}

return nil
}

func convertToMicrosoftTeamsMessage(diffCache notifier.DiffCache) AdaptiveCard {
hostname, _ := os.Hostname()
attachments := convertDeletedFlagsToMicrosoftTeamsMessage(diffCache)
attachments = append(attachments, convertUpdatedFlagsToMicrosoftTeamsMessage(diffCache)...)
attachments = append(attachments, convertAddedFlagsToMicrosoftTeamsMessage(diffCache)...)
sections := attachmentsToSections(attachments)
res := AdaptiveCard{
Type: "MessageCard",
Context: "https://schema.org/extensions",
Summary: fmt.Sprintf("Changes detected in your feature flag file on: *%s*", hostname),
Sections: sections,
}
return res
}

func convertDeletedFlagsToMicrosoftTeamsMessage(diffCache notifier.DiffCache) []attachment {
attachments := make([]attachment, 0)
for key := range diffCache.Deleted {
attachment := attachment{
Title: fmt.Sprintf("❌ Flag \"%s\" deleted", key),
Color: colorDeleted,
FooterIcon: goFFLogo,
Footer: microsoftteamsFooter,
}
attachments = append(attachments, attachment)
}
return attachments
}

func convertUpdatedFlagsToMicrosoftTeamsMessage(diffCache notifier.DiffCache) []attachment {
attachments := make([]attachment, 0)
for key, value := range diffCache.Updated {
attachment := attachment{
Title: fmt.Sprintf("✏️ Flag \"%s\" updated", key),
Color: colorUpdated,
FooterIcon: goFFLogo,
Footer: microsoftteamsFooter,
Fields: []Field{},
}

changelog, _ := diff.Diff(value.Before, value.After, diff.AllowTypeMismatch(true))
for _, change := range changelog {
if change.Type == "update" {
value := fmt.Sprintf("%s => %s", render.Render(change.From), render.Render(change.To))
short := len(value) < longMicrosoftTeamsAttachment
attachment.Fields = append(
attachment.Fields,
Field{Title: strings.Join(change.Path, "."), Short: short, Value: value},
)
}
}

sort.Sort(byTitle(attachment.Fields))

attachments = append(attachments, attachment)
}
return attachments
}

func convertAddedFlagsToMicrosoftTeamsMessage(diff notifier.DiffCache) []attachment {
attachments := make([]attachment, 0)
for key := range diff.Added {
attachment := attachment{
Title: fmt.Sprintf("🆕 Flag \"%s\" created", key),
Color: colorAdded,
FooterIcon: goFFLogo,
Footer: microsoftteamsFooter,
}
attachments = append(attachments, attachment)
}
return attachments
}

func attachmentsToSections(attachments []attachment) []Section {
sections := make([]Section, len(attachments))
for i, att := range attachments {
facts := make([]Fact, len(att.Fields))
for j, field := range att.Fields {
facts[j] = Fact(field)
}

sections[i] = Section{
Title: att.Title,
Color: att.Color,
Facts: facts,
}
}
return sections
}

type AdaptiveCard struct {
Type string `json:"@type"`
Context string `json:"@context"`
Summary string `json:"summary"`
Sections []Section `json:"sections"`
}

type Section struct {
Title string `json:"title,omitempty"`
Text string `json:"text,omitempty"`
Color string `json:"color,omitempty"`
Facts []Fact `json:"facts,omitempty"`
}

type Fact struct {
Title string `json:"title"`
Value string `json:"value"`
Short bool `json:"short"`
}
type attachment struct {
Title string `json:"title"`
Color string `json:"color"`
FooterIcon string `json:"footer_icon"`
Footer string `json:"footer"`
Fields []Field `json:"fields,omitempty"`
}

type Field struct {
Title string `json:"title"`
Value string `json:"value"`
Short bool `json:"short"`
}

type byTitle []Field

func (a byTitle) Len() int { return len(a) }
func (a byTitle) Less(i, j int) bool { return a[i].Title < a[j].Title }
func (a byTitle) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
Loading
Loading