notifier.go 4.4 KB

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