Skip to content

Commit

Permalink
feat: refactoring of alerts and send correct email alerts
Browse files Browse the repository at this point in the history
Previously the sent email alerts have been missing mandatory headers
like `Date` and it was also missing content type, content transfer
encoding and mime version. I have taken proper examples form the
unmaintained gomail library to build right emails.

Besides that I have refactored the calls for alerts, they git the same
structure now and it should be prepared to inject custom templates for
all altering methods at some later point. Generally it is prepared for a
more flexible alert handling.
  • Loading branch information
tboerger committed Feb 27, 2024
1 parent 570a9d3 commit c26c451
Show file tree
Hide file tree
Showing 6 changed files with 353 additions and 206 deletions.
305 changes: 166 additions & 139 deletions services/tasks/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,99 @@ package tasks

import (
"bytes"
"github.com/ansible-semaphore/semaphore/lib"
"github.com/ansible-semaphore/semaphore/util"
"html/template"
"embed"
"fmt"
"net/http"
"strconv"
"strings"
)
"text/template"

const emailTemplate = "Subject: Task '{{ .Name }}' failed\r\n" +
"From: {{ .From }}\r\n" +
"\r\n" +
"Task {{ .TaskID }} with template '{{ .Name }}' has failed!`\n" +
"Task Log: {{ .TaskURL }}"

const telegramTemplate = `{"chat_id": "{{ .ChatID }}","parse_mode":"HTML","text":"<code>{{ .Name }}</code>\n#{{ .TaskID }} <b>{{ .TaskResult }}</b> <code>{{ .TaskVersion }}</code> {{ .TaskDescription }}\nby {{ .Author }}\n{{ .TaskURL }}"}`
"github.com/ansible-semaphore/semaphore/lib"
"github.com/ansible-semaphore/semaphore/util"
"github.com/ansible-semaphore/semaphore/util/mailer"
)

const slackTemplate = `{ "attachments": [ { "title": "Task: {{ .Name }}", "title_link": "{{ .TaskURL }}", "text": "execution ID #{{ .TaskID }}, status: {{ .TaskResult }}!", "color": "{{ .Color }}", "mrkdwn_in": ["text"], "fields": [ { "title": "Author", "value": "{{ .Author }}", "short": true }] } ]}`
//go:embed templates/*.tmpl
var templates embed.FS

// Alert represents an alert that will be templated and sent to the appropriate service
type Alert struct {
TaskID string
Name string
TaskURL string
ChatID string
TaskResult string
TaskDescription string
TaskVersion string
Author string
Color string
From string
Name string
Author string
Color string
Task alertTask
Chat alertChat
}

type alertTask struct {
ID string
URL string
Result string
Desc string
Version string
}

type alertChat struct {
ID string
}

func (t *TaskRunner) sendMailAlert() {
if !util.Config.EmailAlert || !t.alert {
return
}

mailHost := util.Config.EmailHost + ":" + util.Config.EmailPort
body := bytes.NewBufferString("")
author, version := t.alertInfos()

var mailBuffer bytes.Buffer
alert := Alert{
TaskID: strconv.Itoa(t.Task.ID),
Name: t.Template.Name,
TaskURL: util.Config.WebHost + "/project/" + strconv.Itoa(t.Template.ProjectID) +
"/templates/" + strconv.Itoa(t.Template.ID) +
"?t=" + strconv.Itoa(t.Task.ID),
From: util.Config.EmailSender,
Author: author,
Color: t.alertColor("email"),
Task: alertTask{
ID: strconv.Itoa(t.Task.ID),
URL: t.taskLink(),
Result: strings.ToUpper(string(t.Task.Status)),
Version: version,
Desc: t.Task.Message,
},
}
tpl := template.New("mail body template")
tpl, err := tpl.Parse(emailTemplate)
util.LogError(err)

t.panicOnError(tpl.Execute(&mailBuffer, alert), "Can't generate alert template!")
tpl, err := template.ParseFS(templates, "templates/email.tmpl")

for _, user := range t.users {
userObj, err2 := t.pool.store.GetUser(user)
if err != nil {
t.Log("Can't parse email alert template!")
panic(err)
}

if !userObj.Alert {
continue
}
if err := tpl.Execute(body, alert); err != nil {
t.Log("Can't generate email alert template!")
panic(err)
}

for _, uid := range t.users {
user, err := t.pool.store.GetUser(uid)

if err2 != nil {
util.LogError(err2)
if !user.Alert {
continue
}

if util.Config.EmailSecure {
err2 = util.SendSecureMail(util.Config.EmailHost, util.Config.EmailPort,
util.Config.EmailSender, util.Config.EmailUsername, util.Config.EmailPassword,
userObj.Email, mailBuffer)
} else {
err2 = util.SendMail(mailHost, util.Config.EmailSender, userObj.Email, mailBuffer)
if err != nil {
util.LogError(err)
continue
}

if err2 != nil {
util.LogError(err2)
if err := mailer.Send(
util.Config.EmailSecure,
util.Config.EmailHost,
util.Config.EmailPort,
util.Config.EmailUsername,
util.Config.EmailPassword,
util.Config.EmailSender,
user.Email,
fmt.Sprintf("Task '%s' failed", t.Template.Name),
body.String(),
); err != nil {
util.LogError(err)
}
}
}
Expand All @@ -100,60 +117,45 @@ func (t *TaskRunner) sendTelegramAlert() {
return
}

var telegramBuffer bytes.Buffer

var version string
if t.Task.Version != nil {
version = *t.Task.Version
} else if t.Task.BuildTaskID != nil {
buildVer := t.Task.GetIncomingVersion(t.pool.store)
if buildVer != nil {
version = *buildVer
}
} else {
version = ""
}

var message string
if t.Task.Message != "" {
message = "- " + t.Task.Message
}

var author string
if t.Task.UserID != nil {
user, err := t.pool.store.GetUser(*t.Task.UserID)
if err != nil {
panic(err)
}
author = user.Name
}
body := bytes.NewBufferString("")
author, version := t.alertInfos()

alert := Alert{
TaskID: strconv.Itoa(t.Task.ID),
Name: t.Template.Name,
TaskURL: util.Config.WebHost + "/project/" + strconv.Itoa(t.Template.ProjectID) + "/templates/" + strconv.Itoa(t.Template.ID) + "?t=" + strconv.Itoa(t.Task.ID),
ChatID: chatID,
TaskResult: strings.ToUpper(string(t.Task.Status)),
TaskVersion: version,
TaskDescription: message,
Author: author,
Name: t.Template.Name,
Author: author,
Color: t.alertColor("telegram"),
Task: alertTask{
ID: strconv.Itoa(t.Task.ID),
URL: t.taskLink(),
Result: strings.ToUpper(string(t.Task.Status)),
Version: version,
Desc: t.Task.Message,
},
Chat: alertChat{
ID: chatID,
},
}

tpl := template.New("telegram body template")
tpl, err := template.ParseFS(templates, "templates/telegram.tmpl")

tpl, err := tpl.Parse(telegramTemplate)
if err != nil {
t.Log("Can't parse telegram template!")
t.Log("Can't parse telegram alert template!")
panic(err)
}

err = tpl.Execute(&telegramBuffer, alert)
if err != nil {
t.Log("Can't generate alert template!")
if err := tpl.Execute(body, alert); err != nil {
t.Log("Can't generate telegram alert template!")
panic(err)
}

resp, err := http.Post("https://api.telegram.org/bot"+util.Config.TelegramToken+"/sendMessage", "application/json", &telegramBuffer)
resp, err := http.Post(
fmt.Sprintf(
"https://api.telegram.org/bot%s/sendMessage",
util.Config.TelegramToken,
),
"application/json",
body,
)

if err != nil {
t.Log("Can't send telegram alert! Error: " + err.Error())
Expand All @@ -171,11 +173,50 @@ func (t *TaskRunner) sendSlackAlert() {
return
}

slackUrl := util.Config.SlackUrl
body := bytes.NewBufferString("")
author, version := t.alertInfos()

var slackBuffer bytes.Buffer
alert := Alert{
Name: t.Template.Name,
Author: author,
Color: t.alertColor("slack"),
Task: alertTask{
ID: strconv.Itoa(t.Task.ID),
URL: t.taskLink(),
Result: strings.ToUpper(string(t.Task.Status)),
Version: version,
Desc: t.Task.Message,
},
}

tpl, err := template.ParseFS(templates, "templates/slack.tmpl")

if err != nil {
t.Log("Can't parse slack alert template!")
panic(err)
}

if err := tpl.Execute(body, alert); err != nil {
t.Log("Can't generate slack alert template!")
panic(err)
}

resp, err := http.Post(
util.Config.SlackUrl,
"application/json",
body,
)

if err != nil {
t.Log("Can't send slack alert! Error: " + err.Error())
} else if resp.StatusCode != 200 {
t.Log("Can't send slack alert! Response code: " + strconv.Itoa(resp.StatusCode))
}
}

func (t *TaskRunner) alertInfos() (string, string) {
version := ""

var version string
if t.Task.Version != nil {
version = *t.Task.Version
} else if t.Task.BuildTaskID != nil {
Expand All @@ -184,63 +225,49 @@ func (t *TaskRunner) sendSlackAlert() {
version = ""
}

var message string
if t.Task.Message != "" {
message = "- " + t.Task.Message
}
author := ""

var author string
if t.Task.UserID != nil {
user, err := t.pool.store.GetUser(*t.Task.UserID)

if err != nil {
panic(err)
}
author = user.Name
}

var color string
if t.Task.Status == lib.TaskSuccessStatus {
color = "good"
} else if t.Task.Status == lib.TaskFailStatus {
color = "bad"
} else if t.Task.Status == lib.TaskRunningStatus {
color = "#333CFF"
} else if t.Task.Status == lib.TaskWaitingStatus {
color = "#FFFC33"
} else if t.Task.Status == lib.TaskStoppingStatus {
color = "#BEBEBE"
} else if t.Task.Status == lib.TaskStoppedStatus {
color = "#5B5B5B"
}
alert := Alert{
TaskID: strconv.Itoa(t.Task.ID),
Name: t.Template.Name,
TaskURL: util.Config.WebHost + "/project/" + strconv.Itoa(t.Template.ProjectID) + "/templates/" + strconv.Itoa(t.Template.ID) + "?t=" + strconv.Itoa(t.Task.ID),
TaskResult: strings.ToUpper(string(t.Task.Status)),
TaskVersion: version,
TaskDescription: message,
Author: author,
Color: color,
author = user.Name
}

tpl := template.New("slack body template")
return version, author
}

tpl, err := tpl.Parse(slackTemplate)
if err != nil {
t.Log("Can't parse slack template!")
panic(err)
func (t *TaskRunner) alertColor(kind string) string {
switch kind {
case "slack":
switch t.Task.Status {
case lib.TaskSuccessStatus:
return "good"
case lib.TaskFailStatus:
return "danger"
case lib.TaskRunningStatus:
return "#333CFF"
case lib.TaskWaitingStatus:
return "#FFFC33"
case lib.TaskStoppingStatus:
return "#BEBEBE"
case lib.TaskStoppedStatus:
return "#5B5B5B"
}
}

err = tpl.Execute(&slackBuffer, alert)
if err != nil {
t.Log("Can't generate alert template!")
panic(err)
}
resp, err := http.Post(slackUrl, "application/json", &slackBuffer)
return ""
}

if err != nil {
t.Log("Can't send slack alert! Error: " + err.Error())
} else if resp.StatusCode != 200 {
t.Log("Can't send slack alert! Response code: " + strconv.Itoa(resp.StatusCode))
}
func (t *TaskRunner) taskLink() string {
return fmt.Sprintf(
"%s/project/%d/templates/%d?t=%d",
util.Config.WebHost,
t.Template.ProjectID,
t.Template.ID,
t.Task.ID,
)
}
2 changes: 2 additions & 0 deletions services/tasks/templates/email.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Task {{ .Task.ID }} with template '{{ .Name }}' has failed!
Task Log: {{ .Task.URL }}
Loading

0 comments on commit c26c451

Please sign in to comment.