From 7f9b1edfa17b3ae7cfb7bc7719d9875019fd53a7 Mon Sep 17 00:00:00 2001 From: abdegenius Date: Sun, 27 Oct 2024 22:11:46 +0100 Subject: [PATCH 01/13] Updates --- cmd/relayproxy/config/notifier.go | 7 +- cmd/relayproxy/service/gofeatureflag.go | 3 + cmd/relayproxy/service/gofeatureflag_test.go | 6 + notifier/microsoftteamsnotifier/notifier.go | 190 ++++++++++++++++ .../microsoftteamsnotifier/notifier_test.go | 214 ++++++++++++++++++ ...d_call_webhook_and_have_valid_results.json | 78 +++++++ 6 files changed, 497 insertions(+), 1 deletion(-) create mode 100644 notifier/microsoftteamsnotifier/notifier.go create mode 100644 notifier/microsoftteamsnotifier/notifier_test.go create mode 100644 notifier/microsoftteamsnotifier/testdata/should_call_webhook_and_have_valid_results.json diff --git a/cmd/relayproxy/config/notifier.go b/cmd/relayproxy/config/notifier.go index 584d8a6dda9..c456ff2dfa7 100644 --- a/cmd/relayproxy/config/notifier.go +++ b/cmd/relayproxy/config/notifier.go @@ -5,6 +5,7 @@ import "fmt" type NotifierConf struct { Kind NotifierKind `mapstructure:"kind" koanf:"kind"` SlackWebhookURL string `mapstructure:"slackWebhookUrl" koanf:"slackWebhookUrl"` + MicrosoftTeamsWebhookURL string `mapstructure:"microsoftteamsWebhookUrl" koanf:"microsoftteamsWebhookUrl"` EndpointURL string `mapstructure:"endpointUrl" koanf:"endpointUrl"` Secret string `mapstructure:"secret" koanf:"secret"` Meta map[string]string `mapstructure:"meta" koanf:"meta"` @@ -18,6 +19,9 @@ func (c *NotifierConf) IsValid() error { if c.Kind == SlackNotifier && c.SlackWebhookURL == "" { return fmt.Errorf("invalid notifier: no \"slackWebhookUrl\" property found for kind \"%s\"", c.Kind) } + if c.Kind == MicrosoftTeamsNotifier && c.MicrosoftTeamsWebhookURL == "" { + return fmt.Errorf("invalid notifier: no \"microsoftteamsWebhookUrl\" property found for kind \"%s\"", c.Kind) + } if c.Kind == WebhookNotifier && c.EndpointURL == "" { return fmt.Errorf("invalid notifier: no \"endpointUrl\" property found for kind \"%s\"", c.Kind) } @@ -28,13 +32,14 @@ type NotifierKind string const ( SlackNotifier NotifierKind = "slack" + MicrosoftTeamsNotifier NotifierKind = "microsoftteams" WebhookNotifier NotifierKind = "webhook" ) // IsValid is checking if the value is part of the enum func (r NotifierKind) IsValid() error { switch r { - case SlackNotifier, WebhookNotifier: + case SlackNotifier, MicrosoftTeamsNotifier, WebhookNotifier: return nil } return fmt.Errorf("invalid notifier: kind \"%s\" is not supported", r) diff --git a/cmd/relayproxy/service/gofeatureflag.go b/cmd/relayproxy/service/gofeatureflag.go index f8bd96d8218..0006ab5246a 100644 --- a/cmd/relayproxy/service/gofeatureflag.go +++ b/cmd/relayproxy/service/gofeatureflag.go @@ -20,6 +20,7 @@ import ( "github.com/thomaspoignant/go-feature-flag/exporter/webhookexporter" "github.com/thomaspoignant/go-feature-flag/notifier" "github.com/thomaspoignant/go-feature-flag/notifier/slacknotifier" + "github.com/thomaspoignant/go-feature-flag/notifier/microsoftteamsnotifier" "github.com/thomaspoignant/go-feature-flag/notifier/webhooknotifier" "github.com/thomaspoignant/go-feature-flag/retriever" "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" @@ -304,6 +305,8 @@ func initNotifier(c []config.NotifierConf) ([]notifier.Notifier, error) { switch cNotif.Kind { case config.SlackNotifier: notifiers = append(notifiers, &slacknotifier.Notifier{SlackWebhookURL: cNotif.SlackWebhookURL}) + case config.MicrosoftTeamsNotifier: + notifiers = append(notifiers, µsoftteamsnotifier.Notifier{MicrosoftTeamsWebhookURL: cNotif.MicrosoftTeamsWebhookURL}) case config.WebhookNotifier: notifiers = append(notifiers, diff --git a/cmd/relayproxy/service/gofeatureflag_test.go b/cmd/relayproxy/service/gofeatureflag_test.go index 597080ff042..ce499bd5f94 100644 --- a/cmd/relayproxy/service/gofeatureflag_test.go +++ b/cmd/relayproxy/service/gofeatureflag_test.go @@ -20,6 +20,7 @@ import ( "github.com/thomaspoignant/go-feature-flag/exporter/webhookexporter" "github.com/thomaspoignant/go-feature-flag/notifier" "github.com/thomaspoignant/go-feature-flag/notifier/slacknotifier" + "github.com/thomaspoignant/go-feature-flag/notifier/microsoftteamsnotifier" "github.com/thomaspoignant/go-feature-flag/notifier/webhooknotifier" "github.com/thomaspoignant/go-feature-flag/retriever" "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" @@ -403,6 +404,10 @@ func Test_initNotifier(t *testing.T) { Kind: config.SlackNotifier, SlackWebhookURL: "http:xxxx.xxx", }, + { + Kind: config.MicrosoftTeamsNotifier, + MicrosoftTeamsWebhookURL: "http:zzzz.zzz", + }, { Kind: config.WebhookNotifier, EndpointURL: "http:yyyy.yyy", @@ -411,6 +416,7 @@ func Test_initNotifier(t *testing.T) { }, want: []notifier.Notifier{ &slacknotifier.Notifier{SlackWebhookURL: "http:xxxx.xxx"}, + µsoftteamsnotifier.Notifier{MicrosoftTeamsWebhookURL: "http:zzzz.zzz"}, &webhooknotifier.Notifier{EndpointURL: "http:yyyy.yyy"}, }, wantErr: assert.NoError, diff --git a/notifier/microsoftteamsnotifier/notifier.go b/notifier/microsoftteamsnotifier/notifier.go new file mode 100644 index 00000000000..48f3a45a5f4 --- /dev/null +++ b/notifier/microsoftteamsnotifier/notifier.go @@ -0,0 +1,190 @@ +package microsoftteamsnotifier + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "sort" + "strings" + "sync" + "time" + + "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 ( + goFFLogo = "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png" + microsoftteamsFooter = "go-feature-flag" + colorDeleted = "#FF0000" + colorUpdated = "#FFA500" + colorAdded = "#008000" + longMicrosoftTeamsAttachment = 35 +) + +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() + } + }) + + 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) + } + request := http.Request{ + Method: http.MethodPost, + URL: microsoftteamsURL, + Body: io.NopCloser(bytes.NewReader(payload)), + Header: map[string][]string{"Content-type": {"application/json"}}, + } + 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) microsoftteamsMessage { + hostname, _ := os.Hostname() + attachments := convertDeletedFlagsToMicrosoftTeamsMessage(diffCache) + attachments = append(attachments, convertUpdatedFlagsToMicrosoftTeamsMessage(diffCache)...) + attachments = append(attachments, convertAddedFlagsToMicrosoftTeamsMessage(diffCache)...) + currentDate := time.Now().Format("YYYY-MM-DD") + res := []map[string]interface{}{ + "@type": "MessageCard", + "@context": "https://schema.org/extensions", + "summary": "Changes detected in your feature flag", + "sections": []map[string]interface{}{ + { + "activityTitle": "Changes detected in your feature flag", + "activitySubtitle": fmt.Sprintf("On flag file: *%s*", hostname), + "activityImage": goFFLogo, + "text": fmt.Sprintf("Changes detected in your feature flag file on: *%s*", hostname), + "facts": []map[string]string{ + {"name": "Date", "value": currentDate}, + }, + }, + }, + "attachments": []Attachment{attachments}, + // "attachments": attachments, + } + 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 +} + +type microsoftteamsMessage struct { + IconURL string `json:"icon_url"` + Text string `json:"text"` + Attachments []attachment `json:"attachments"` +} + +type attachment struct { + Color string `json:"color"` + Title string `json:"title"` + Fields []Field `json:"fields"` + FooterIcon string `json:"footer_icon,omitempty"` + Footer string `json:"footer,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] } diff --git a/notifier/microsoftteamsnotifier/notifier_test.go b/notifier/microsoftteamsnotifier/notifier_test.go new file mode 100644 index 00000000000..f0687c4b4c4 --- /dev/null +++ b/notifier/microsoftteamsnotifier/notifier_test.go @@ -0,0 +1,214 @@ +package microsoftteamsnotifier + +import ( + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/thomaspoignant/go-feature-flag/internal/flag" + "github.com/thomaspoignant/go-feature-flag/notifier" + "github.com/thomaspoignant/go-feature-flag/testutils" + "github.com/thomaspoignant/go-feature-flag/testutils/testconvert" +) + +func TestMicrosoftTeamsNotifier_Notify(t *testing.T) { + type args struct { + diff notifier.DiffCache + statusCode int + forceError bool + url string + } + type expected struct { + err bool + errMsg string + bodyPath string + signature string + } + tests := []struct { + name string + args args + expected expected + }{ + { + name: "should call webhook and have valid results", + expected: expected{ + bodyPath: "./testdata/should_call_webhook_and_have_valid_results.json", + }, + args: args{ + url: "https://outlook.office.com/webhook/ZZZZ", + statusCode: http.StatusOK, + diff: notifier.DiffCache{ + Added: map[string]flag.Flag{ + "test-flag3": &flag.InternalFlag{ + Rules: &[]flag.Rule{ + { + Name: testconvert.String("rule1"), + Query: testconvert.String("key eq \"random-key\""), + Percentages: &map[string]float64{ + "False": 95, + "True": 5, + }, + }, + }, + Variations: &map[string]*interface{}{ + "Default": testconvert.Interface("default"), + "False": testconvert.Interface("false"), + "True": testconvert.Interface("test"), + }, + DefaultRule: &flag.Rule{ + Name: testconvert.String("defaultRule"), + VariationResult: testconvert.String("Default"), + }, + TrackEvents: testconvert.Bool(true), + Disable: testconvert.Bool(false), + Version: testconvert.String("1.1"), + }, + }, + Deleted: map[string]flag.Flag{ + "test-flag": &flag.InternalFlag{ + Rules: &[]flag.Rule{ + { + Name: testconvert.String("rule1"), + Query: testconvert.String("key eq \"random-key\""), + Percentages: &map[string]float64{ + "False": 0, + "True": 100, + }, + }, + }, + Variations: &map[string]*interface{}{ + "Default": testconvert.Interface(false), + "False": testconvert.Interface(false), + "True": testconvert.Interface(true), + }, + DefaultRule: &flag.Rule{ + Name: testconvert.String("defaultRule"), + VariationResult: testconvert.String("Default"), + }, + }, + }, + Updated: map[string]notifier.DiffUpdated{ + "test-flag2": { + Before: &flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "Default": testconvert.Interface(false), + "False": testconvert.Interface(false), + "True": testconvert.Interface(true), + }, + DefaultRule: &flag.Rule{ + Name: testconvert.String("defaultRule"), + Percentages: &map[string]float64{ + "False": 0, + "True": 100, + }, + }, + Experimentation: &flag.ExperimentationRollout{ + Start: testconvert.Time(time.Unix(1095379400, 0)), + End: testconvert.Time(time.Unix(1095371000, 0)), + }, + }, + After: &flag.InternalFlag{ + Variations: &map[string]*interface{}{ + "Default": testconvert.Interface("strDefault"), + "False": testconvert.Interface("strFalse"), + "True": testconvert.Interface("strTrue"), + }, + Rules: &[]flag.Rule{ + { + Name: testconvert.String("rule1"), + Query: testconvert.String("key eq \"not-a-ke\""), + Percentages: &map[string]float64{ + "False": 20, + "True": 80, + }, + }, + }, + DefaultRule: &flag.Rule{ + Name: testconvert.String("defaultRule"), + VariationResult: testconvert.String("Default"), + }, + Disable: testconvert.Bool(true), + TrackEvents: testconvert.Bool(false), + Version: testconvert.String("1.1"), + }, + }, + }, + }, + }, + }, + { + name: "should err if http code is superior to 399", + expected: expected{ + err: true, + errMsg: "error: (Microsoft Teams Notifier) while calling microsoft teams webhook, statusCode = 400", + }, + args: args{ + url: "https://outlook.office.com/webhook/ZZZZ", + statusCode: http.StatusBadRequest, + diff: notifier.DiffCache{}, + }, + }, + { + name: "should err if error while calling webhook", + expected: expected{ + err: true, + errMsg: "error: (Microsoft Teams Notifier) error: while calling webhook: random error", + }, + args: args{ + url: "https://outlook.office.com/webhook/ZZZZ", + statusCode: http.StatusOK, + diff: notifier.DiffCache{}, + forceError: true, + }, + }, + { + name: "missing microsoft teams url", + expected: expected{ + err: true, + errMsg: "error: (Microsoft Teams Notifier) invalid notifier configuration, no MicrosoftTeamsWebhookURL provided for the microsoft teams notifier", + }, + args: args{ + statusCode: http.StatusOK, + diff: notifier.DiffCache{}, + }, + }, + { + name: "invalid microsoft teams url", + expected: expected{ + err: true, + errMsg: "error: (Microsoft Teams Notifier) invalid MicrosoftTeamsWebhookURL: https://{}outlook.office.com/webhook/ZZZZ", + }, + args: args{ + url: "https://{}outlook.office.com/webhook/ZZZZ", + statusCode: http.StatusOK, + diff: notifier.DiffCache{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockHTTPClient := &testutils.HTTPClientMock{StatusCode: tt.args.statusCode, ForceError: tt.args.forceError} + + c := Notifier{ + MicrosoftTeamsWebhookURL: tt.args.url, + httpClient: mockHTTPClient, + } + + err := c.Notify(tt.args.diff) + + if tt.expected.err { + assert.ErrorContains(t, err, tt.expected.errMsg) + } else { + assert.NoError(t, err) + hostname, _ := os.Hostname() + content, _ := os.ReadFile(tt.expected.bodyPath) + expectedContent := strings.ReplaceAll(string(content), "{{hostname}}", hostname) + assert.JSONEq(t, expectedContent, mockHTTPClient.Body) + assert.Equal(t, tt.expected.signature, mockHTTPClient.Signature) + } + }) + } +} diff --git a/notifier/microsoftteamsnotifier/testdata/should_call_webhook_and_have_valid_results.json b/notifier/microsoftteamsnotifier/testdata/should_call_webhook_and_have_valid_results.json new file mode 100644 index 00000000000..0803b3a8f8f --- /dev/null +++ b/notifier/microsoftteamsnotifier/testdata/should_call_webhook_and_have_valid_results.json @@ -0,0 +1,78 @@ +{ + "icon_url": "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png", + "text": "Changes detected in your feature flag file on: *{{hostname}}*", + "attachments": [ + { + "color": "#FF0000", + "title": "❌ Flag \"test-flag\" deleted", + "fields": null, + "footer_icon": "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png", + "footer": "go-feature-flag" + }, + { + "color": "#FFA500", + "title": "✏️ Flag \"test-flag2\" updated", + "fields": [ + { + "title": "DefaultRule.Percentages", + "value": "(*map[string]float64){\"False\":0, \"True\":100} =\u003e nil", + "short": false + }, + { + "title": "DefaultRule.VariationResult", + "value": "nil =\u003e (*string)(\"Default\")", + "short": true + }, + { + "title": "Disable", + "value": "nil =\u003e (*bool)(true)", + "short": true + }, + { + "title": "Experimentation", + "value": "(*flag.ExperimentationRollout){Start:(*time.Time){wall:0, ext:63230976200, loc:(*time.Location){name:\"\", zone:[]time.zone(nil), tx:[]time.zoneTrans(nil), extend:\"\", cacheStart:0, cacheEnd:0, cacheZone:(*time.zone)(nil)}}, End:(*time.Time){wall:0, ext:63230967800, loc:(*time.Location){name:\"\", zone:[]time.zone(nil), tx:[]time.zoneTrans(nil), extend:\"\", cacheStart:0, cacheEnd:0, cacheZone:(*time.zone)(nil)}}} =\u003e nil", + "short": false + }, + { + "title": "Rules", + "value": "nil =\u003e (*[]flag.Rule){flag.Rule{Name:(*string)(\"rule1\"), Query:(*string)(\"key eq \\\"not-a-ke\\\"\"), VariationResult:(*string)(nil), Percentages:(*map[string]float64){\"False\":20, \"True\":80}, ProgressiveRollout:(*flag.ProgressiveRollout)(nil), Disable:(*bool)(nil)}}", + "short": false + }, + { + "title": "TrackEvents", + "value": "nil =\u003e (*bool)(false)", + "short": true + }, + { + "title": "Variations.Default", + "value": "false =\u003e \"strDefault\"", + "short": true + }, + { + "title": "Variations.False", + "value": "false =\u003e \"strFalse\"", + "short": true + }, + { + "title": "Variations.True", + "value": "true =\u003e \"strTrue\"", + "short": true + }, + { + "title": "Version", + "value": "nil =\u003e (*string)(\"1.1\")", + "short": true + } + ], + "footer_icon": "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png", + "footer": "go-feature-flag" + }, + { + "color": "#008000", + "title": "🆕 Flag \"test-flag3\" created", + "fields": null, + "footer_icon": "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png", + "footer": "go-feature-flag" + } + ] +} From 217116f25838f90a999d50b8a96fd8b7f316fa67 Mon Sep 17 00:00:00 2001 From: abdegenius Date: Wed, 30 Oct 2024 08:17:36 +0100 Subject: [PATCH 02/13] Updates --- notifier/microsoftteamsnotifier/notifier.go | 43 ++++++++----------- .../microsoftteamsnotifier/notifier_test.go | 10 ++--- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/notifier/microsoftteamsnotifier/notifier.go b/notifier/microsoftteamsnotifier/notifier.go index 48f3a45a5f4..e3bce961307 100644 --- a/notifier/microsoftteamsnotifier/notifier.go +++ b/notifier/microsoftteamsnotifier/notifier.go @@ -58,11 +58,15 @@ func (c *Notifier) Notify(diff notifier.DiffCache) error { if err != nil { return fmt.Errorf("error: (Microsoft Teams Notifier) impossible to read differences; %v", err) } + 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"}}, + Header: map[string][]string{ + "Content-Type": {"application/json"}, + "Authorization": {"Bearer " + microsoftTeamAccessToken}, + }, } response, err := c.httpClient.Do(&request) if err != nil { @@ -83,26 +87,13 @@ func convertToMicrosoftTeamsMessage(diffCache notifier.DiffCache) microsoftteams attachments := convertDeletedFlagsToMicrosoftTeamsMessage(diffCache) attachments = append(attachments, convertUpdatedFlagsToMicrosoftTeamsMessage(diffCache)...) attachments = append(attachments, convertAddedFlagsToMicrosoftTeamsMessage(diffCache)...) - currentDate := time.Now().Format("YYYY-MM-DD") - res := []map[string]interface{}{ - "@type": "MessageCard", - "@context": "https://schema.org/extensions", - "summary": "Changes detected in your feature flag", - "sections": []map[string]interface{}{ - { - "activityTitle": "Changes detected in your feature flag", - "activitySubtitle": fmt.Sprintf("On flag file: *%s*", hostname), - "activityImage": goFFLogo, - "text": fmt.Sprintf("Changes detected in your feature flag file on: *%s*", hostname), - "facts": []map[string]string{ - {"name": "Date", "value": currentDate}, - }, - }, - }, - "attachments": []Attachment{attachments}, - // "attachments": attachments, + res := microsoftteamsMessage{ + IconURL: goFFLogo, + Body: MessageBody{Content: fmt.Sprintf("Changes detected in your feature flag file on: *%s*", hostname)}, + Attachments: attachments } return res + } func convertDeletedFlagsToMicrosoftTeamsMessage(diffCache notifier.DiffCache) []attachment { @@ -163,12 +154,6 @@ func convertAddedFlagsToMicrosoftTeamsMessage(diff notifier.DiffCache) []attachm return attachments } -type microsoftteamsMessage struct { - IconURL string `json:"icon_url"` - Text string `json:"text"` - Attachments []attachment `json:"attachments"` -} - type attachment struct { Color string `json:"color"` Title string `json:"title"` @@ -182,6 +167,14 @@ type Field struct { Value string `json:"value"` Short bool `json:"short"` } +type MessageBody struct { + Content string `json:"content"` +} +type microsoftteamsMessage struct { + IconURL string `json:"icon_url"` + Body MessageBody `json:"body"` + Attachments []attachment `json:"attachments"` +} type ByTitle []Field diff --git a/notifier/microsoftteamsnotifier/notifier_test.go b/notifier/microsoftteamsnotifier/notifier_test.go index f0687c4b4c4..d1982c98089 100644 --- a/notifier/microsoftteamsnotifier/notifier_test.go +++ b/notifier/microsoftteamsnotifier/notifier_test.go @@ -38,7 +38,7 @@ func TestMicrosoftTeamsNotifier_Notify(t *testing.T) { bodyPath: "./testdata/should_call_webhook_and_have_valid_results.json", }, args: args{ - url: "https://outlook.office.com/webhook/ZZZZ", + url: "https://graph.microsoft.com/teams/XXXXXX/channels/YYYYYY/messages", statusCode: http.StatusOK, diff: notifier.DiffCache{ Added: map[string]flag.Flag{ @@ -146,7 +146,7 @@ func TestMicrosoftTeamsNotifier_Notify(t *testing.T) { errMsg: "error: (Microsoft Teams Notifier) while calling microsoft teams webhook, statusCode = 400", }, args: args{ - url: "https://outlook.office.com/webhook/ZZZZ", + url: "https://graph.microsoft.com/teams/XXXXXX/channels/YYYYYY/messages", statusCode: http.StatusBadRequest, diff: notifier.DiffCache{}, }, @@ -158,7 +158,7 @@ func TestMicrosoftTeamsNotifier_Notify(t *testing.T) { errMsg: "error: (Microsoft Teams Notifier) error: while calling webhook: random error", }, args: args{ - url: "https://outlook.office.com/webhook/ZZZZ", + url: "https://graph.microsoft.com/teams/XXXXXX/channels/YYYYYY/messages", statusCode: http.StatusOK, diff: notifier.DiffCache{}, forceError: true, @@ -179,10 +179,10 @@ func TestMicrosoftTeamsNotifier_Notify(t *testing.T) { name: "invalid microsoft teams url", expected: expected{ err: true, - errMsg: "error: (Microsoft Teams Notifier) invalid MicrosoftTeamsWebhookURL: https://{}outlook.office.com/webhook/ZZZZ", + errMsg: "error: (Microsoft Teams Notifier) invalid MicrosoftTeamsWebhookURL: https://{}graph.microsoft.com/teams/XXXXXX/channels/YYYYYY/messages", }, args: args{ - url: "https://{}outlook.office.com/webhook/ZZZZ", + url: "https://{}graph.microsoft.com/teams/XXXXXX/channels/YYYYYY/messages", statusCode: http.StatusOK, diff: notifier.DiffCache{}, }, From 8f68cddc4ca104dc0ab91900cc493716cd2cf0a0 Mon Sep 17 00:00:00 2001 From: abdegenius Date: Tue, 5 Nov 2024 22:49:19 +0100 Subject: [PATCH 03/13] Updates --- notifier/microsoftteamsnotifier/notifier.go | 5 ++--- .../should_call_webhook_and_have_valid_results.json | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/notifier/microsoftteamsnotifier/notifier.go b/notifier/microsoftteamsnotifier/notifier.go index e3bce961307..72fac1e41cb 100644 --- a/notifier/microsoftteamsnotifier/notifier.go +++ b/notifier/microsoftteamsnotifier/notifier.go @@ -11,8 +11,7 @@ import ( "sort" "strings" "sync" - "time" - + "github.com/luci/go-render/render" "github.com/r3labs/diff/v3" "github.com/thomaspoignant/go-feature-flag/internal" @@ -90,7 +89,7 @@ func convertToMicrosoftTeamsMessage(diffCache notifier.DiffCache) microsoftteams res := microsoftteamsMessage{ IconURL: goFFLogo, Body: MessageBody{Content: fmt.Sprintf("Changes detected in your feature flag file on: *%s*", hostname)}, - Attachments: attachments + Attachments: attachments, } return res diff --git a/notifier/microsoftteamsnotifier/testdata/should_call_webhook_and_have_valid_results.json b/notifier/microsoftteamsnotifier/testdata/should_call_webhook_and_have_valid_results.json index 0803b3a8f8f..222b8a1174a 100644 --- a/notifier/microsoftteamsnotifier/testdata/should_call_webhook_and_have_valid_results.json +++ b/notifier/microsoftteamsnotifier/testdata/should_call_webhook_and_have_valid_results.json @@ -1,6 +1,8 @@ { "icon_url": "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png", - "text": "Changes detected in your feature flag file on: *{{hostname}}*", + "body": { + "content": "Changes detected in your feature flag file on: *{{hostname}}*" + }, "attachments": [ { "color": "#FF0000", @@ -75,4 +77,4 @@ "footer": "go-feature-flag" } ] -} +} \ No newline at end of file From 2ed3fb17ee78dd192843216252c12faeb419d0a6 Mon Sep 17 00:00:00 2001 From: abdegenius Date: Tue, 5 Nov 2024 23:26:37 +0100 Subject: [PATCH 04/13] Updates --- README.md | 1 + .../go_module/notifier/microsoft-teams.md | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 website/docs/go_module/notifier/microsoft-teams.md diff --git a/README.md b/README.md index bb2322b3555..0943774f120 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/website/docs/go_module/notifier/microsoft-teams.md b/website/docs/go_module/notifier/microsoft-teams.md new file mode 100644 index 00000000000..760130fa395 --- /dev/null +++ b/website/docs/go_module/notifier/microsoft-teams.md @@ -0,0 +1,40 @@ +--- +sidebar_position: 11 +--- + +# Microsoft Teams Notifier + +The microsoft teams notifier allows to get notified on your favorite microsoft teams channel when an instance of `go-feature-flag` is +detecting changes in the configuration of your flags. + +![Microsoft Teams Notification](/docs/notifier/microsoftteams1.png) + +## Configure Microsoft Teams Notifications + +https://learn.microsoft.com/en-us/graph/api/chatmessage-post + +1. First, get your microsoft team access token. + *Need a hand?*[Click here to see how it's done](https://learn.microsoft.com/en-us/graph/auth/auth-concepts?view=graph-rest-1.0#access-tokens) +2. Add the access token in your env file with the key `MICROSOFT_TEAMS_ACCESS_TOKEN` +3. Copy your Team ID (e.g XXXXXX) as well as well as the Channel ID (e.g YYYYYY) +4. Now, generate/copy your webhook URL. + It should look like: `https://graph.microsoft.com/teams/XXXXXX/channels/YYYYYY/messages`. +5. In your init method add a microsoft teams notifier + +```go +err := ffclient.Init(ffclient.Config +{ + // ... + Notifiers: []notifier.Notifier{ + µsoftteamsnotifier.Notifier{ + MicrosoftTeamsWebhookURL: "https://graph.microsoft.com/teams/XXXXXX/channels/YYYYYY/messages", + }, + }, +}) +``` + +## **Configuration fields** + +| **Field** | **Description** | +|---------------------|-------------------------------------------| +| `MicrosoftTeamsWebhookURL` | The complete URL of your microsoft teams webhook. | From eef74a0d363423b1af0310eb74f674232ab1bb2f Mon Sep 17 00:00:00 2001 From: martin machiebe Date: Tue, 5 Nov 2024 23:48:06 +0100 Subject: [PATCH 05/13] updates --- notifier/microsoftteamsnotifier/notifier.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/notifier/microsoftteamsnotifier/notifier.go b/notifier/microsoftteamsnotifier/notifier.go index 72fac1e41cb..3998afc43d9 100644 --- a/notifier/microsoftteamsnotifier/notifier.go +++ b/notifier/microsoftteamsnotifier/notifier.go @@ -91,8 +91,7 @@ func convertToMicrosoftTeamsMessage(diffCache notifier.DiffCache) microsoftteams Body: MessageBody{Content: fmt.Sprintf("Changes detected in your feature flag file on: *%s*", hostname)}, Attachments: attachments, } - return res - + return res } func convertDeletedFlagsToMicrosoftTeamsMessage(diffCache notifier.DiffCache) []attachment { From 9be4247c4f79ebb7e04efca18a292572566c8c9d Mon Sep 17 00:00:00 2001 From: martin machiebe Date: Wed, 6 Nov 2024 00:01:43 +0100 Subject: [PATCH 06/13] updates --- cmd/relayproxy/config/notifier.go | 22 +++++++++---------- cmd/relayproxy/service/gofeatureflag.go | 2 +- notifier/microsoftteamsnotifier/notifier.go | 14 ++++++------ .../microsoftteamsnotifier/notifier_test.go | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/cmd/relayproxy/config/notifier.go b/cmd/relayproxy/config/notifier.go index 379cd4f37c3..730d354ba27 100644 --- a/cmd/relayproxy/config/notifier.go +++ b/cmd/relayproxy/config/notifier.go @@ -3,14 +3,14 @@ package config import "fmt" type NotifierConf struct { - Kind NotifierKind `mapstructure:"kind" koanf:"kind"` - SlackWebhookURL string `mapstructure:"slackWebhookUrl" koanf:"slackWebhookUrl"` + Kind NotifierKind `mapstructure:"kind" koanf:"kind"` + SlackWebhookURL string `mapstructure:"slackWebhookUrl" koanf:"slackWebhookUrl"` MicrosoftTeamsWebhookURL string `mapstructure:"microsoftteamsWebhookUrl" koanf:"microsoftteamsWebhookUrl"` - EndpointURL string `mapstructure:"endpointUrl" koanf:"endpointUrl"` - Secret string `mapstructure:"secret" koanf:"secret"` - Meta map[string]string `mapstructure:"meta" koanf:"meta"` - Headers map[string][]string `mapstructure:"headers" koanf:"headers"` - WebhookURL string `mapstructure:"webhookUrl" koanf:"webhookurl"` + EndpointURL string `mapstructure:"endpointUrl" koanf:"endpointUrl"` + Secret string `mapstructure:"secret" koanf:"secret"` + Meta map[string]string `mapstructure:"meta" koanf:"meta"` + Headers map[string][]string `mapstructure:"headers" koanf:"headers"` + WebhookURL string `mapstructure:"webhookUrl" koanf:"webhookurl"` } func (c *NotifierConf) IsValid() error { @@ -35,10 +35,10 @@ func (c *NotifierConf) IsValid() error { type NotifierKind string const ( - SlackNotifier NotifierKind = "slack" - MicrosoftTeamsNotifier NotifierKind = "microsoftteams" - 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 diff --git a/cmd/relayproxy/service/gofeatureflag.go b/cmd/relayproxy/service/gofeatureflag.go index e03e0215918..184a668f58f 100644 --- a/cmd/relayproxy/service/gofeatureflag.go +++ b/cmd/relayproxy/service/gofeatureflag.go @@ -21,8 +21,8 @@ import ( "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/slacknotifier" "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" "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" diff --git a/notifier/microsoftteamsnotifier/notifier.go b/notifier/microsoftteamsnotifier/notifier.go index 3998afc43d9..ea478837b93 100644 --- a/notifier/microsoftteamsnotifier/notifier.go +++ b/notifier/microsoftteamsnotifier/notifier.go @@ -11,7 +11,7 @@ import ( "sort" "strings" "sync" - + "github.com/luci/go-render/render" "github.com/r3labs/diff/v3" "github.com/thomaspoignant/go-feature-flag/internal" @@ -19,11 +19,11 @@ import ( ) const ( - goFFLogo = "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png" + goFFLogo = "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png" microsoftteamsFooter = "go-feature-flag" - colorDeleted = "#FF0000" - colorUpdated = "#FFA500" - colorAdded = "#008000" + colorDeleted = "#FF0000" + colorUpdated = "#FFA500" + colorAdded = "#008000" longMicrosoftTeamsAttachment = 35 ) @@ -88,10 +88,10 @@ func convertToMicrosoftTeamsMessage(diffCache notifier.DiffCache) microsoftteams attachments = append(attachments, convertAddedFlagsToMicrosoftTeamsMessage(diffCache)...) res := microsoftteamsMessage{ IconURL: goFFLogo, - Body: MessageBody{Content: fmt.Sprintf("Changes detected in your feature flag file on: *%s*", hostname)}, + Body: MessageBody{Content: fmt.Sprintf("Changes detected in your feature flag file on: *%s*", hostname)}, Attachments: attachments, } - return res + return res } func convertDeletedFlagsToMicrosoftTeamsMessage(diffCache notifier.DiffCache) []attachment { diff --git a/notifier/microsoftteamsnotifier/notifier_test.go b/notifier/microsoftteamsnotifier/notifier_test.go index d1982c98089..5b03d69b758 100644 --- a/notifier/microsoftteamsnotifier/notifier_test.go +++ b/notifier/microsoftteamsnotifier/notifier_test.go @@ -194,7 +194,7 @@ func TestMicrosoftTeamsNotifier_Notify(t *testing.T) { c := Notifier{ MicrosoftTeamsWebhookURL: tt.args.url, - httpClient: mockHTTPClient, + httpClient: mockHTTPClient, } err := c.Notify(tt.args.diff) From b1d2d741d60d509fb99392affe2a5636210ff863 Mon Sep 17 00:00:00 2001 From: martin machiebe Date: Wed, 6 Nov 2024 00:10:26 +0100 Subject: [PATCH 07/13] updates --- cmd/relayproxy/service/gofeatureflag_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/relayproxy/service/gofeatureflag_test.go b/cmd/relayproxy/service/gofeatureflag_test.go index ea8721a87ee..3edfee28745 100644 --- a/cmd/relayproxy/service/gofeatureflag_test.go +++ b/cmd/relayproxy/service/gofeatureflag_test.go @@ -20,8 +20,8 @@ import ( "github.com/thomaspoignant/go-feature-flag/exporter/sqsexporter" "github.com/thomaspoignant/go-feature-flag/exporter/webhookexporter" "github.com/thomaspoignant/go-feature-flag/notifier" - "github.com/thomaspoignant/go-feature-flag/notifier/slacknotifier" "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" "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" @@ -426,7 +426,7 @@ func Test_initNotifier(t *testing.T) { SlackWebhookURL: "http:xxxx.xxx", }, { - Kind: config.MicrosoftTeamsNotifier, + Kind: config.MicrosoftTeamsNotifier, MicrosoftTeamsWebhookURL: "http:zzzz.zzz", }, { From 94d229ec3415fbbc8b488cfad66d5440138834bb Mon Sep 17 00:00:00 2001 From: martin machiebe Date: Wed, 6 Nov 2024 00:24:51 +0100 Subject: [PATCH 08/13] updates --- cmd/relayproxy/service/gofeatureflag.go | 3 ++- notifier/microsoftteamsnotifier/notifier.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/relayproxy/service/gofeatureflag.go b/cmd/relayproxy/service/gofeatureflag.go index 184a668f58f..2eec6fffc91 100644 --- a/cmd/relayproxy/service/gofeatureflag.go +++ b/cmd/relayproxy/service/gofeatureflag.go @@ -323,7 +323,8 @@ func initNotifier(c []config.NotifierConf) ([]notifier.Notifier, error) { } notifiers = append(notifiers, &slacknotifier.Notifier{SlackWebhookURL: cNotif.WebhookURL}) case config.MicrosoftTeamsNotifier: - notifiers = append(notifiers, µsoftteamsnotifier.Notifier{MicrosoftTeamsWebhookURL: cNotif.MicrosoftTeamsWebhookURL}) + notifiers = append(notifiers, µsoftteamsnotifier.Notifier{MicrosoftTeamsWebhookURL: + cNotif.MicrosoftTeamsWebhookURL}) case config.WebhookNotifier: notifiers = append(notifiers, &webhooknotifier.Notifier{ diff --git a/notifier/microsoftteamsnotifier/notifier.go b/notifier/microsoftteamsnotifier/notifier.go index ea478837b93..bdefce26fbd 100644 --- a/notifier/microsoftteamsnotifier/notifier.go +++ b/notifier/microsoftteamsnotifier/notifier.go @@ -49,7 +49,8 @@ func (c *Notifier) Notify(diff notifier.DiffCache) error { microsoftteamsURL, err := url.Parse(c.MicrosoftTeamsWebhookURL) if err != nil { - return fmt.Errorf("error: (Microsoft Teams Notifier) invalid MicrosoftTeamsWebhookURL: %v", c.MicrosoftTeamsWebhookURL) + return fmt.Errorf("error: (Microsoft Teams Notifier) invalid MicrosoftTeamsWebhookURL: %v", + c.MicrosoftTeamsWebhookURL) } reqBody := convertToMicrosoftTeamsMessage(diff) From 2d74ae7becea068009e1f9b73c5e85b571a1e5d0 Mon Sep 17 00:00:00 2001 From: abdegenius Date: Wed, 6 Nov 2024 07:46:35 +0100 Subject: [PATCH 09/13] Updates --- website/docs/go_module/notifier/microsoft-teams.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/website/docs/go_module/notifier/microsoft-teams.md b/website/docs/go_module/notifier/microsoft-teams.md index 760130fa395..8c70893b437 100644 --- a/website/docs/go_module/notifier/microsoft-teams.md +++ b/website/docs/go_module/notifier/microsoft-teams.md @@ -7,8 +7,6 @@ sidebar_position: 11 The microsoft teams notifier allows to get notified on your favorite microsoft teams channel when an instance of `go-feature-flag` is detecting changes in the configuration of your flags. -![Microsoft Teams Notification](/docs/notifier/microsoftteams1.png) - ## Configure Microsoft Teams Notifications https://learn.microsoft.com/en-us/graph/api/chatmessage-post From 3a5a80da8275e081be5d92c2a7e97dbaf95da296 Mon Sep 17 00:00:00 2001 From: abdegenius Date: Wed, 6 Nov 2024 09:20:34 +0100 Subject: [PATCH 10/13] Updates --- notifier/microsoftteamsnotifier/notifier.go | 76 +++++++++++++------ ...d_call_webhook_and_have_valid_results.json | 27 +++---- 2 files changed, 62 insertions(+), 41 deletions(-) diff --git a/notifier/microsoftteamsnotifier/notifier.go b/notifier/microsoftteamsnotifier/notifier.go index 72fac1e41cb..2602727573e 100644 --- a/notifier/microsoftteamsnotifier/notifier.go +++ b/notifier/microsoftteamsnotifier/notifier.go @@ -19,12 +19,12 @@ import ( ) const ( - goFFLogo = "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png" - microsoftteamsFooter = "go-feature-flag" - colorDeleted = "#FF0000" - colorUpdated = "#FFA500" - colorAdded = "#008000" - longMicrosoftTeamsAttachment = 35 + 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 { @@ -81,15 +81,17 @@ func (c *Notifier) Notify(diff notifier.DiffCache) error { return nil } -func convertToMicrosoftTeamsMessage(diffCache notifier.DiffCache) microsoftteamsMessage { +func convertToMicrosoftTeamsMessage(diffCache notifier.DiffCache) AdaptiveCard { hostname, _ := os.Hostname() attachments := convertDeletedFlagsToMicrosoftTeamsMessage(diffCache) attachments = append(attachments, convertUpdatedFlagsToMicrosoftTeamsMessage(diffCache)...) attachments = append(attachments, convertAddedFlagsToMicrosoftTeamsMessage(diffCache)...) - res := microsoftteamsMessage{ - IconURL: goFFLogo, - Body: MessageBody{Content: fmt.Sprintf("Changes detected in your feature flag file on: *%s*", hostname)}, - Attachments: attachments, + 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 @@ -153,12 +155,48 @@ func convertAddedFlagsToMicrosoftTeamsMessage(diff notifier.DiffCache) []attachm 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 { - Color string `json:"color"` Title string `json:"title"` - Fields []Field `json:"fields"` - FooterIcon string `json:"footer_icon,omitempty"` - Footer string `json:"footer,omitempty"` + Color string `json:"color"` + FooterIcon string `json:"footer_icon"` + Footer string `json:"footer"` + Fields []Field `json:"fields,omitempty"` } type Field struct { @@ -166,14 +204,6 @@ type Field struct { Value string `json:"value"` Short bool `json:"short"` } -type MessageBody struct { - Content string `json:"content"` -} -type microsoftteamsMessage struct { - IconURL string `json:"icon_url"` - Body MessageBody `json:"body"` - Attachments []attachment `json:"attachments"` -} type ByTitle []Field diff --git a/notifier/microsoftteamsnotifier/testdata/should_call_webhook_and_have_valid_results.json b/notifier/microsoftteamsnotifier/testdata/should_call_webhook_and_have_valid_results.json index 222b8a1174a..8e7ec8c269d 100644 --- a/notifier/microsoftteamsnotifier/testdata/should_call_webhook_and_have_valid_results.json +++ b/notifier/microsoftteamsnotifier/testdata/should_call_webhook_and_have_valid_results.json @@ -1,24 +1,20 @@ { - "icon_url": "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png", - "body": { - "content": "Changes detected in your feature flag file on: *{{hostname}}*" - }, - "attachments": [ + "@context": "https://schema.org/extensions", + "@type": "MessageCard", + "summary": "Changes detected in your feature flag file on: *{{hostname}}*", + "sections": [ { "color": "#FF0000", - "title": "❌ Flag \"test-flag\" deleted", - "fields": null, - "footer_icon": "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png", - "footer": "go-feature-flag" + "title": "❌ Flag \"test-flag\" deleted" }, { "color": "#FFA500", "title": "✏️ Flag \"test-flag2\" updated", - "fields": [ + "facts": [ { "title": "DefaultRule.Percentages", "value": "(*map[string]float64){\"False\":0, \"True\":100} =\u003e nil", - "short": false + "short": true }, { "title": "DefaultRule.VariationResult", @@ -65,16 +61,11 @@ "value": "nil =\u003e (*string)(\"1.1\")", "short": true } - ], - "footer_icon": "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png", - "footer": "go-feature-flag" + ] }, { "color": "#008000", - "title": "🆕 Flag \"test-flag3\" created", - "fields": null, - "footer_icon": "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png", - "footer": "go-feature-flag" + "title": "🆕 Flag \"test-flag3\" created" } ] } \ No newline at end of file From 2ae2c9df9a9a47c9b54494c53b8ef33411d6ae15 Mon Sep 17 00:00:00 2001 From: martin machiebe Date: Wed, 6 Nov 2024 10:04:49 +0100 Subject: [PATCH 11/13] updates --- notifier/microsoftteamsnotifier/notifier_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notifier/microsoftteamsnotifier/notifier_test.go b/notifier/microsoftteamsnotifier/notifier_test.go index 5b03d69b758..4ec03923629 100644 --- a/notifier/microsoftteamsnotifier/notifier_test.go +++ b/notifier/microsoftteamsnotifier/notifier_test.go @@ -33,7 +33,7 @@ func TestMicrosoftTeamsNotifier_Notify(t *testing.T) { expected expected }{ { - name: "should call webhook and have valid results", + name: "should call webhook and have valid result", expected: expected{ bodyPath: "./testdata/should_call_webhook_and_have_valid_results.json", }, From 919ff6783ae4edbc740f623344f6ec6999afffae Mon Sep 17 00:00:00 2001 From: abdegenius Date: Wed, 6 Nov 2024 11:02:24 +0100 Subject: [PATCH 12/13] Updates --- cmd/relayproxy/config/notifier.go | 23 ++++++------- cmd/relayproxy/service/gofeatureflag.go | 9 ++++-- cmd/relayproxy/service/gofeatureflag_test.go | 4 +-- notifier/microsoftteamsnotifier/notifier.go | 32 +++++++++---------- .../microsoftteamsnotifier/notifier_test.go | 2 +- 5 files changed, 38 insertions(+), 32 deletions(-) diff --git a/cmd/relayproxy/config/notifier.go b/cmd/relayproxy/config/notifier.go index 379cd4f37c3..1852760fc64 100644 --- a/cmd/relayproxy/config/notifier.go +++ b/cmd/relayproxy/config/notifier.go @@ -3,14 +3,15 @@ package config import "fmt" type NotifierConf struct { - Kind NotifierKind `mapstructure:"kind" koanf:"kind"` - SlackWebhookURL string `mapstructure:"slackWebhookUrl" koanf:"slackWebhookUrl"` + Kind NotifierKind `mapstructure:"kind" koanf:"kind"` + // Deprecated: Use WebhookURL instead + SlackWebhookURL string `mapstructure:"slackWebhookUrl" koanf:"slackWebhookUrl"` MicrosoftTeamsWebhookURL string `mapstructure:"microsoftteamsWebhookUrl" koanf:"microsoftteamsWebhookUrl"` - EndpointURL string `mapstructure:"endpointUrl" koanf:"endpointUrl"` - Secret string `mapstructure:"secret" koanf:"secret"` - Meta map[string]string `mapstructure:"meta" koanf:"meta"` - Headers map[string][]string `mapstructure:"headers" koanf:"headers"` - WebhookURL string `mapstructure:"webhookUrl" koanf:"webhookurl"` + EndpointURL string `mapstructure:"endpointUrl" koanf:"endpointUrl"` + Secret string `mapstructure:"secret" koanf:"secret"` + Meta map[string]string `mapstructure:"meta" koanf:"meta"` + Headers map[string][]string `mapstructure:"headers" koanf:"headers"` + WebhookURL string `mapstructure:"webhookUrl" koanf:"webhookurl"` } func (c *NotifierConf) IsValid() error { @@ -35,10 +36,10 @@ func (c *NotifierConf) IsValid() error { type NotifierKind string const ( - SlackNotifier NotifierKind = "slack" - MicrosoftTeamsNotifier NotifierKind = "microsoftteams" - 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 diff --git a/cmd/relayproxy/service/gofeatureflag.go b/cmd/relayproxy/service/gofeatureflag.go index e03e0215918..dad9ac9721c 100644 --- a/cmd/relayproxy/service/gofeatureflag.go +++ b/cmd/relayproxy/service/gofeatureflag.go @@ -21,8 +21,8 @@ import ( "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/slacknotifier" "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" "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" @@ -323,7 +323,12 @@ func initNotifier(c []config.NotifierConf) ([]notifier.Notifier, error) { } notifiers = append(notifiers, &slacknotifier.Notifier{SlackWebhookURL: cNotif.WebhookURL}) case config.MicrosoftTeamsNotifier: - notifiers = append(notifiers, µsoftteamsnotifier.Notifier{MicrosoftTeamsWebhookURL: cNotif.MicrosoftTeamsWebhookURL}) + notifiers = append( + notifiers, + µsoftteamsnotifier.Notifier{ + MicrosoftTeamsWebhookURL: cNotif.MicrosoftTeamsWebhookURL, + }, + ) case config.WebhookNotifier: notifiers = append(notifiers, &webhooknotifier.Notifier{ diff --git a/cmd/relayproxy/service/gofeatureflag_test.go b/cmd/relayproxy/service/gofeatureflag_test.go index ea8721a87ee..3edfee28745 100644 --- a/cmd/relayproxy/service/gofeatureflag_test.go +++ b/cmd/relayproxy/service/gofeatureflag_test.go @@ -20,8 +20,8 @@ import ( "github.com/thomaspoignant/go-feature-flag/exporter/sqsexporter" "github.com/thomaspoignant/go-feature-flag/exporter/webhookexporter" "github.com/thomaspoignant/go-feature-flag/notifier" - "github.com/thomaspoignant/go-feature-flag/notifier/slacknotifier" "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" "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" @@ -426,7 +426,7 @@ func Test_initNotifier(t *testing.T) { SlackWebhookURL: "http:xxxx.xxx", }, { - Kind: config.MicrosoftTeamsNotifier, + Kind: config.MicrosoftTeamsNotifier, MicrosoftTeamsWebhookURL: "http:zzzz.zzz", }, { diff --git a/notifier/microsoftteamsnotifier/notifier.go b/notifier/microsoftteamsnotifier/notifier.go index 2602727573e..ca3e268de2f 100644 --- a/notifier/microsoftteamsnotifier/notifier.go +++ b/notifier/microsoftteamsnotifier/notifier.go @@ -11,7 +11,7 @@ import ( "sort" "strings" "sync" - + "github.com/luci/go-render/render" "github.com/r3labs/diff/v3" "github.com/thomaspoignant/go-feature-flag/internal" @@ -19,11 +19,11 @@ import ( ) const ( - colorDeleted = "#FF0000" - colorUpdated = "#FFA500" - colorAdded = "#008000" - goFFLogo = "https://raw.githubusercontent.com/thomaspoignant/go-feature-flag/main/logo_128.png" - microsoftteamsFooter = "go-feature-flag" + 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 ) @@ -49,7 +49,8 @@ func (c *Notifier) Notify(diff notifier.DiffCache) error { microsoftteamsURL, err := url.Parse(c.MicrosoftTeamsWebhookURL) if err != nil { - return fmt.Errorf("error: (Microsoft Teams Notifier) invalid MicrosoftTeamsWebhookURL: %v", c.MicrosoftTeamsWebhookURL) + return fmt.Errorf("error: (Microsoft Teams Notifier) invalid MicrosoftTeamsWebhookURL: %v", + c.MicrosoftTeamsWebhookURL) } reqBody := convertToMicrosoftTeamsMessage(diff) @@ -88,13 +89,12 @@ func convertToMicrosoftTeamsMessage(diffCache notifier.DiffCache) AdaptiveCard { 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, + 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 { @@ -173,10 +173,10 @@ func attachmentsToSections(attachments []attachment) []Section { } type AdaptiveCard struct { - Type string `json:"@type"` - Context string `json:"@context"` - Summary string `json:"summary"` - Sections []Section `json:"sections"` + Type string `json:"@type"` + Context string `json:"@context"` + Summary string `json:"summary"` + Sections []Section `json:"sections"` } type Section struct { diff --git a/notifier/microsoftteamsnotifier/notifier_test.go b/notifier/microsoftteamsnotifier/notifier_test.go index d1982c98089..5b03d69b758 100644 --- a/notifier/microsoftteamsnotifier/notifier_test.go +++ b/notifier/microsoftteamsnotifier/notifier_test.go @@ -194,7 +194,7 @@ func TestMicrosoftTeamsNotifier_Notify(t *testing.T) { c := Notifier{ MicrosoftTeamsWebhookURL: tt.args.url, - httpClient: mockHTTPClient, + httpClient: mockHTTPClient, } err := c.Notify(tt.args.diff) From 9cc0cdd0b463ee74383b1a2a2754ae409ff58b6c Mon Sep 17 00:00:00 2001 From: abdegenius Date: Wed, 6 Nov 2024 18:33:05 +0100 Subject: [PATCH 13/13] Updates --- cmd/relayproxy/config/notifier.go | 5 ++--- notifier/microsoftteamsnotifier/notifier.go | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cmd/relayproxy/config/notifier.go b/cmd/relayproxy/config/notifier.go index 1852760fc64..7310f18a8eb 100644 --- a/cmd/relayproxy/config/notifier.go +++ b/cmd/relayproxy/config/notifier.go @@ -6,7 +6,6 @@ type NotifierConf struct { Kind NotifierKind `mapstructure:"kind" koanf:"kind"` // Deprecated: Use WebhookURL instead SlackWebhookURL string `mapstructure:"slackWebhookUrl" koanf:"slackWebhookUrl"` - MicrosoftTeamsWebhookURL string `mapstructure:"microsoftteamsWebhookUrl" koanf:"microsoftteamsWebhookUrl"` EndpointURL string `mapstructure:"endpointUrl" koanf:"endpointUrl"` Secret string `mapstructure:"secret" koanf:"secret"` Meta map[string]string `mapstructure:"meta" koanf:"meta"` @@ -21,8 +20,8 @@ func (c *NotifierConf) IsValid() error { if c.Kind == SlackNotifier && c.SlackWebhookURL == "" { return fmt.Errorf("invalid notifier: no \"slackWebhookUrl\" property found for kind \"%s\"", c.Kind) } - if c.Kind == MicrosoftTeamsNotifier && c.MicrosoftTeamsWebhookURL == "" { - return fmt.Errorf("invalid notifier: no \"microsoftteamsWebhookUrl\" 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) } if c.Kind == WebhookNotifier && c.EndpointURL == "" { return fmt.Errorf("invalid notifier: no \"endpointUrl\" property found for kind \"%s\"", c.Kind) diff --git a/notifier/microsoftteamsnotifier/notifier.go b/notifier/microsoftteamsnotifier/notifier.go index ca3e268de2f..96ccccee9cb 100644 --- a/notifier/microsoftteamsnotifier/notifier.go +++ b/notifier/microsoftteamsnotifier/notifier.go @@ -134,7 +134,7 @@ func convertUpdatedFlagsToMicrosoftTeamsMessage(diffCache notifier.DiffCache) [] } } - sort.Sort(ByTitle(attachment.Fields)) + sort.Sort(byTitle(attachment.Fields)) attachments = append(attachments, attachment) } @@ -205,8 +205,8 @@ type Field struct { Short bool `json:"short"` } -type ByTitle []Field +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] } +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] }