diff --git a/middlewares/teams.go b/middlewares/teams.go new file mode 100644 index 0000000..8e96e1e --- /dev/null +++ b/middlewares/teams.go @@ -0,0 +1,148 @@ +package middlewares + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/PremoWeb/Chadburn/core" +) + +var ( + teamsAvatarURL = "https://raw.githubusercontent.com/PremoWeb/chadburn/main/static/chadburn.png" +) + +// TeamsConfig configuration for the Teams middleware +type TeamsConfig struct { + TeamsWebhook string `gcfg:"teams-webhook" mapstructure:"teams-webhook"` + TeamsOnlyOnError bool `gcfg:"teams-only-on-error" mapstructure:"teams-only-on-error"` +} + +func NewTeams(c *TeamsConfig) core.Middleware { + var m core.Middleware + if !IsEmpty(c) { + m = &Teams{*c} + } + + return m +} + +// Teams middleware calls to a Teams input-hook after every execution of a job +type Teams struct { + TeamsConfig +} + +// ContinueOnStop returns always true +func (m *Teams) ContinueOnStop() bool { + return true +} + +// Run sends a message to the Teams channel, its close stop the execution to +// collect the metrics +func (m *Teams) Run(ctx *core.Context) error { + err := ctx.Next() + ctx.Stop(err) + + if ctx.Execution.Failed || !m.TeamsOnlyOnError { + m.pushMessage(ctx) + } + + return err +} + +func (m *Teams) pushMessage(ctx *core.Context) { + content, _ := json.Marshal(m.buildMessage(ctx)) + reader := bytes.NewReader(content) + + r, err := http.Post(m.TeamsWebhook, "application/json", reader) + if err != nil { + ctx.Logger.Errorf("Teams error calling %q error: %q", m.TeamsWebhook, err) + } else if r.StatusCode != 200 { + body, _ := ioutil.ReadAll(r.Body) + ctx.Logger.Errorf("Teams error non-200 status code calling %q: %v", m.TeamsWebhook, string(body)) + } +} + +func (m *Teams) buildMessage(ctx *core.Context) *teamsMessage { + msg := newTeamsMessage() + + title := fmt.Sprintf( + "Job *%q* finished in *%s*, command `%s`", + ctx.Job.GetName(), ctx.Execution.Duration, ctx.Job.GetCommand(), + ) + + s1 := teamsMessageSections{ + ActivityTitle: title, + ActivitySubtitle: "Execution successful", + ActivityImage: teamsAvatarURL, + Facts: make([]teamsMessageSectionFact, 0), + Markdown: true, + } + + if ctx.Execution.Failed { + msg.ThemeColor = "F35A00" + msg.Summary = "Execution failed" + s1.ActivitySubtitle = fmt.Sprintf("Execution failed: %v", ctx.Execution.Error.Error()) + } else if ctx.Execution.Skipped { + msg.ThemeColor = "FFA500" + msg.Summary = "Execution skipped" + s1.ActivitySubtitle = fmt.Sprintf("Execution skipped") + } + + msg.Sections = append(msg.Sections, s1) + + if isSuccess(ctx.Execution) { + s2 := teamsMessageSections{ + ActivityTitle: "Execution results", + ActivityText: strings.ReplaceAll(ctx.Execution.OutputStream.String(), "\n", "
"), + ActivityImage: "", + Facts: nil, + Markdown: true, + } + msg.Sections = append(msg.Sections, s2) + } + + return msg +} + +func isSuccess(e *core.Execution) bool { + if e.Failed || e.Skipped { + return false + } + return true +} + +func newTeamsMessage() *teamsMessage { + return &teamsMessage{ + Type: "MessageCard", + Context: "http://schema.org/extensions", + ThemeColor: "0076D7", + Summary: "Execution successful", + Sections: make([]teamsMessageSections, 0), + } +} + +type teamsMessage struct { + Type string `json:"@type"` + Context string `json:"@context"` + ThemeColor string `json:"themeColor"` + Summary string `json:"summary"` + Sections []teamsMessageSections `json:"sections"` +} + +type teamsMessageSections struct { + ActivityTitle string `json:"activityTitle"` + ActivitySubtitle string `json:"activitySubtitle"` + ActivityImage string `json:"activityImage"` + ActivityText string `json:"activityText"` + Facts []teamsMessageSectionFact `json:"facts"` + Markdown bool `json:"markdown"` +} + +type teamsMessageSectionFact struct { + Name string `json:"name"` + Value string `json:"value"` +} \ No newline at end of file diff --git a/middlewares/teams_test.go b/middlewares/teams_test.go new file mode 100644 index 0000000..84a6b0e --- /dev/null +++ b/middlewares/teams_test.go @@ -0,0 +1,69 @@ +package middlewares + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + + . "gopkg.in/check.v1" +) + +type SuiteTeams struct { + BaseSuite +} + +var _ = Suite(&SuiteTeams{}) + +func (s *SuiteTeams) TestNewTeamsEmpty(c *C) { + c.Assert(NewTeams(&TeamsConfig{}), IsNil) +} + +func (s *SuiteTeams) TestRunTeamsSuccess(c *C) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var m teamsMessage + b, _ := ioutil.ReadAll(r.Body) + _ = json.Unmarshal(b, &m) + c.Assert(m.Summary, Equals, "Execution successful") + })) + + defer ts.Close() + + s.ctx.Start() + s.ctx.Stop(nil) + + m := NewTeams(&TeamsConfig{TeamsWebhook: ts.URL}) + c.Assert(m.Run(s.ctx), IsNil) +} + +func (s *SuiteTeams) TestRunTeamsSuccessFailed(c *C) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var m teamsMessage + b, _ := ioutil.ReadAll(r.Body) + _ = json.Unmarshal(b, &m) + c.Assert(m.Summary, Equals, "Execution failed") + })) + + defer ts.Close() + + s.ctx.Start() + s.ctx.Stop(errors.New("foo")) + + m := NewTeams(&TeamsConfig{TeamsWebhook: ts.URL}) + c.Assert(m.Run(s.ctx), IsNil) +} + +func (s *SuiteTeams) TestRunTeamsSuccessOnError(c *C) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Assert(true, Equals, false) + })) + + defer ts.Close() + + s.ctx.Start() + s.ctx.Stop(nil) + + m := NewTeams(&TeamsConfig{TeamsWebhook: ts.URL, TeamsOnlyOnError: true}) + c.Assert(m.Run(s.ctx), IsNil) +} \ No newline at end of file