notifier.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. package slack
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "github.com/porter-dev/porter/internal/models"
  7. "net/http"
  8. "strings"
  9. "time"
  10. "github.com/porter-dev/porter/internal/models/integrations"
  11. )
  12. type Notifier interface {
  13. Notify(opts *NotifyOpts) error
  14. }
  15. type DeploymentStatus string
  16. const (
  17. StatusDeployed string = "deployed"
  18. StatusFailed string = "failed"
  19. )
  20. type NotifyOpts struct {
  21. // ProjectID is the id of the Porter project that this deployment belongs to
  22. ProjectID uint
  23. // ClusterID is the id of the Porter cluster that this deployment belongs to
  24. ClusterID uint
  25. // ClusterName is the name of the cluster that this deployment was deployed in
  26. ClusterName string
  27. // Status is the current status of the deployment.
  28. Status string
  29. // Info is any additional information about this status, such as an error message if
  30. // the deployment failed.
  31. Info string
  32. // Name is the name of the deployment that this notification refers to.
  33. Name string
  34. // Namespace is the Kubernetes namespace of the deployment that this notification refers to.
  35. Namespace string
  36. URL string
  37. Version int
  38. }
  39. type SlackNotifier struct {
  40. slackInts []*integrations.SlackIntegration
  41. Config *models.NotificationConfigExternal
  42. }
  43. func NewSlackNotifier(conf *models.NotificationConfigExternal, slackInts ...*integrations.SlackIntegration) Notifier {
  44. return &SlackNotifier{
  45. slackInts: slackInts,
  46. Config: conf,
  47. }
  48. }
  49. type SlackPayload struct {
  50. Blocks []*SlackBlock `json:"blocks"`
  51. }
  52. type SlackBlock struct {
  53. Type string `json:"type"`
  54. Text *SlackText `json:"text,omitempty"`
  55. }
  56. type SlackText struct {
  57. Type string `json:"type"`
  58. Text string `json:"text"`
  59. }
  60. func (s *SlackNotifier) Notify(opts *NotifyOpts) error {
  61. if s.Config != nil {
  62. if !s.Config.Enabled {
  63. return nil
  64. }
  65. if opts.Status == StatusDeployed && !s.Config.Success {
  66. return nil
  67. }
  68. if opts.Status == StatusFailed && !s.Config.Failure {
  69. return nil
  70. }
  71. }
  72. blocks := []*SlackBlock{
  73. getMessageBlock(opts),
  74. getDividerBlock(),
  75. getMarkdownBlock(fmt.Sprintf("*Name:* %s", "`"+opts.Name+"`")),
  76. getMarkdownBlock(fmt.Sprintf("*Namespace:* %s", "`"+opts.Namespace+"`")),
  77. getMarkdownBlock(fmt.Sprintf("*Version:* %d", opts.Version)),
  78. }
  79. // we create a basic payload as a fallback if the detailed payload with "info" fails, due to
  80. // marshaling errors on the Slack API side.
  81. basicSlackPayload := &SlackPayload{
  82. Blocks: blocks,
  83. }
  84. infoBlock := getInfoBlock(opts)
  85. if infoBlock != nil {
  86. blocks = append(blocks, infoBlock)
  87. }
  88. slackPayload := &SlackPayload{
  89. Blocks: blocks,
  90. }
  91. basicPayload, err := json.Marshal(basicSlackPayload)
  92. if err != nil {
  93. return err
  94. }
  95. payload, err := json.Marshal(slackPayload)
  96. if err != nil {
  97. return err
  98. }
  99. basicReqBody := bytes.NewReader(basicPayload)
  100. reqBody := bytes.NewReader(payload)
  101. client := &http.Client{
  102. Timeout: time.Second * 5,
  103. }
  104. for _, slackInt := range s.slackInts {
  105. resp, err := client.Post(string(slackInt.Webhook), "application/json", reqBody)
  106. if err != nil || resp.StatusCode != 200 {
  107. client.Post(string(slackInt.Webhook), "application/json", basicReqBody)
  108. }
  109. }
  110. return nil
  111. }
  112. func getDividerBlock() *SlackBlock {
  113. return &SlackBlock{
  114. Type: "divider",
  115. }
  116. }
  117. func getMarkdownBlock(md string) *SlackBlock {
  118. return &SlackBlock{
  119. Type: "section",
  120. Text: &SlackText{
  121. Type: "mrkdwn",
  122. Text: md,
  123. },
  124. }
  125. }
  126. func getMessageBlock(opts *NotifyOpts) *SlackBlock {
  127. var md string
  128. switch opts.Status {
  129. case StatusDeployed:
  130. md = getSuccessMessage(opts)
  131. case StatusFailed:
  132. md = getFailedMessage(opts)
  133. }
  134. return getMarkdownBlock(md)
  135. }
  136. func getInfoBlock(opts *NotifyOpts) *SlackBlock {
  137. var md string
  138. switch opts.Status {
  139. case StatusFailed:
  140. md = getFailedInfoMessage(opts)
  141. default:
  142. return nil
  143. }
  144. return getMarkdownBlock(md)
  145. }
  146. func getSuccessMessage(opts *NotifyOpts) string {
  147. return fmt.Sprintf(
  148. ":rocket: Your application %s was successfully updated on Porter! <%s|View the new release.>",
  149. "`"+opts.Name+"`",
  150. opts.URL,
  151. )
  152. }
  153. func getFailedMessage(opts *NotifyOpts) string {
  154. return fmt.Sprintf(
  155. ":x: Your application %s failed to deploy on Porter. <%s|View the status here.>",
  156. "`"+opts.Name+"`",
  157. opts.URL,
  158. )
  159. }
  160. func getFailedInfoMessage(opts *NotifyOpts) string {
  161. info := opts.Info
  162. // TODO: this casing is quite ugly and looks for particular types of API server
  163. // errors, otherwise it truncates the error message to 200 characters. This should
  164. // handle the errors more gracefully.
  165. if strings.Contains(info, "Invalid value:") {
  166. errArr := strings.Split(info, "Invalid value:")
  167. // look for "unmarshalerDecoder" error
  168. if strings.Contains(info, "unmarshalerDecoder") {
  169. udArr := strings.Split(info, "unmarshalerDecoder:")
  170. info = errArr[0] + udArr[1]
  171. } else {
  172. info = errArr[0] + "..."
  173. }
  174. } else if len(info) > 200 {
  175. info = info[0:200] + "..."
  176. }
  177. return fmt.Sprintf("```\n%s\n```", info)
  178. }