diff --git a/cmd/flagger/main.go b/cmd/flagger/main.go index d2d77e4a..e0b3ecfc 100644 --- a/cmd/flagger/main.go +++ b/cmd/flagger/main.go @@ -54,10 +54,10 @@ func init() { flag.DurationVar(&controlLoopInterval, "control-loop-interval", 10*time.Second, "Kubernetes API sync interval.") flag.StringVar(&logLevel, "log-level", "debug", "Log level can be: debug, info, warning, error.") flag.StringVar(&port, "port", "8080", "Port to listen on.") - flag.StringVar(&msteamsURL, "msteams-url", "", "MS Teams incoming webhook URL.") flag.StringVar(&slackURL, "slack-url", "", "Slack hook URL.") flag.StringVar(&slackUser, "slack-user", "flagger", "Slack user name.") flag.StringVar(&slackChannel, "slack-channel", "", "Slack channel.") + flag.StringVar(&msteamsURL, "msteams-url", "", "MS Teams incoming webhook URL.") flag.IntVar(&threadiness, "threadiness", 2, "Worker concurrency.") flag.BoolVar(&zapReplaceGlobals, "zap-replace-globals", false, "Whether to change the logging level of the global zap logger.") flag.StringVar(&zapEncoding, "zap-encoding", "json", "Zap logger encoding.") @@ -160,25 +160,21 @@ func main() { logger.Errorf("Metrics server %s unreachable %v", metricsServer, err) } - var notifierClient notifier.Interface - if slackURL != "" { - f := notifier.NewFactory(slackURL, slackUser, slackChannel) - var err error - notifierClient, err = f.Notifier() - if err != nil { - logger.Errorf("Notifier %v", err) - } else { - logger.Infof("Slack notifications enabled for channel %s", slackChannel) - } - } + // setup Slack or MS Teams notifications + notifierURL := slackURL if msteamsURL != "" { - f := notifier.NewFactory(slackURL, slackUser, slackChannel) + notifierURL = msteamsURL + } + notifierFactory := notifier.NewFactory(notifierURL, slackUser, slackChannel) + + var notifierClient notifier.Interface + if notifierURL != "" { var err error - notifierClient, err = f.Notifier() + notifierClient, err = notifierFactory.Notifier() if err != nil { logger.Errorf("Notifier %v", err) } else { - logger.Infof("Slack notifications enabled for channel %s", slackChannel) + logger.Infof("Notifications enabled for %s", notifierURL[0:30]) } } diff --git a/pkg/notifier/factory.go b/pkg/notifier/factory.go index 482fd736..54716973 100644 --- a/pkg/notifier/factory.go +++ b/pkg/notifier/factory.go @@ -22,6 +22,8 @@ func (f Factory) Notifier() (Interface, error) { switch { case strings.Contains(f.URL, "slack.com"): return NewSlack(f.URL, f.Username, f.Channel) + case strings.Contains(f.URL, "office.com"): + return NewMSTeams(f.URL) } return nil, nil diff --git a/pkg/notifier/teams.go b/pkg/notifier/teams.go new file mode 100644 index 00000000..8545837e --- /dev/null +++ b/pkg/notifier/teams.go @@ -0,0 +1,94 @@ +package notifier + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" +) + +// MS Teams holds the incoming webhook URL +type MSTeams struct { + URL string +} + +// MSTeamsPayload holds the message card data +type MSTeamsPayload struct { + Type string `json:"@type"` + Context string `json:"@context"` + ThemeColor string `json:"themeColor"` + Summary string `json:"summary"` + Sections []MSTeamsSection `json:"sections"` +} + +// MSTeamsSection holds the canary analysis result +type MSTeamsSection struct { + ActivityTitle string `json:"activityTitle"` + ActivitySubtitle string `json:"activitySubtitle"` + Facts []MSTeamsField `json:"facts"` +} + +type MSTeamsField struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// NewMSTeams validates the MS Teams URL and returns a MSTeams object +func NewMSTeams(hookURL string) (*MSTeams, error) { + _, err := url.ParseRequestURI(hookURL) + if err != nil { + return nil, fmt.Errorf("invalid MS Teams webhook URL %s", hookURL) + } + + return &MSTeams{ + URL: hookURL, + }, nil +} + +// Post MS Teams message +func (s *MSTeams) Post(workload string, namespace string, message string, fields []Field, warn bool) error { + facts := make([]MSTeamsField, len(fields)) + for _, f := range fields { + facts = append(facts, MSTeamsField{f.Name, f.Value}) + } + + payload := MSTeamsPayload{ + Type: "MessageCard", + Context: "http://schema.org/extensions", + ThemeColor: "0076D7", + Summary: fmt.Sprintf("%s.%s", workload, namespace), + Sections: []MSTeamsSection{ + { + ActivityTitle: message, + ActivitySubtitle: fmt.Sprintf("%s.%s", workload, namespace), + Facts: facts, + }, + }, + } + + if warn { + payload.ThemeColor = "FF0000" + } + + data, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshalling slack payload failed %v", err) + } + + b := bytes.NewBuffer(data) + + if res, err := http.Post(s.URL, "application/json", b); err != nil { + return fmt.Errorf("sending data to MS Teams failed %v", err) + } else { + defer res.Body.Close() + statusCode := res.StatusCode + if statusCode != 200 { + body, _ := ioutil.ReadAll(res.Body) + return fmt.Errorf("sending data to MS Teams failed %v", string(body)) + } + } + + return nil +}