Просмотр исходного кода

Merge pull request #1019 from porter-dev/0.8.0-detailed-slack-errors

[0.8.0] Detailed Slack error notifications
abelanger5 4 лет назад
Родитель
Сommit
0eccbb87da
2 измененных файлов с 135 добавлено и 58 удалено
  1. 131 57
      internal/integrations/slack/notifier.go
  2. 4 1
      server/api/release_handler.go

+ 131 - 57
internal/integrations/slack/notifier.go

@@ -2,8 +2,10 @@ package slack
 
 import (
 	"bytes"
+	"encoding/json"
 	"fmt"
 	"net/http"
+	"strings"
 	"time"
 
 	"github.com/porter-dev/porter/internal/models/integrations"
@@ -58,80 +60,152 @@ func NewSlackNotifier(slackInts ...*integrations.SlackIntegration) Notifier {
 	}
 }
 
+type SlackPayload struct {
+	Blocks []*SlackBlock `json:"blocks"`
+}
+
+type SlackBlock struct {
+	Type string     `json:"type"`
+	Text *SlackText `json:"text,omitempty"`
+}
+
+type SlackText struct {
+	Type string `json:"type"`
+	Text string `json:"text"`
+}
+
 func (s *SlackNotifier) Notify(opts *NotifyOpts) error {
-	var statusPayload string
+	blocks := []*SlackBlock{
+		getMessageBlock(opts),
+		getDividerBlock(),
+		getMarkdownBlock(fmt.Sprintf("*Name:* %s", "`"+opts.Name+"`")),
+		getMarkdownBlock(fmt.Sprintf("*Namespace:* %s", "`"+opts.Namespace+"`")),
+		getMarkdownBlock(fmt.Sprintf("*Version:* %d", opts.Version)),
+	}
 
-	switch opts.Status {
-	case StatusDeployed:
-		statusPayload = getSuccessPayload(opts)
-	case StatusFailed:
-		statusPayload = getFailedPayload(opts)
+	// we create a basic payload as a fallback if the detailed payload with "info" fails, due to
+	// marshaling errors on the Slack API side.
+	basicSlackPayload := &SlackPayload{
+		Blocks: blocks,
 	}
 
-	payload := fmt.Sprintf(`
-	{
-		"blocks": [
-			%s
-			{
-				"type": "divider"
-			},
-			{
-				"type": "section",
-				"text": {
-					"type": "mrkdwn",
-					"text": "*Name:* %s"
-				}
-			},
-			{
-				"type": "section",
-				"text": {
-					"type": "mrkdwn",
-					"text": "*Namespace:* %s"
-				}
-			},
-			{
-				"type": "section",
-				"text": {
-					"type": "mrkdwn",
-					"text": "*Version:* %d"
-				}
-			}
-		]
+	infoBlock := getInfoBlock(opts)
+
+	if infoBlock != nil {
+		blocks = append(blocks, infoBlock)
 	}
-	`, statusPayload, "`"+opts.Name+"`", "`"+opts.Namespace+"`", opts.Version)
 
-	reqBody := bytes.NewReader([]byte(payload))
+	slackPayload := &SlackPayload{
+		Blocks: blocks,
+	}
+
+	basicPayload, err := json.Marshal(basicSlackPayload)
+
+	if err != nil {
+		return err
+	}
+
+	payload, err := json.Marshal(slackPayload)
+
+	if err != nil {
+		return err
+	}
+
+	basicReqBody := bytes.NewReader(basicPayload)
+	reqBody := bytes.NewReader(payload)
 	client := &http.Client{
 		Timeout: time.Second * 5,
 	}
 
 	for _, slackInt := range s.slackInts {
-		client.Post(string(slackInt.Webhook), "application/json", reqBody)
+		resp, err := client.Post(string(slackInt.Webhook), "application/json", reqBody)
+
+		if err != nil || resp.StatusCode != 200 {
+			client.Post(string(slackInt.Webhook), "application/json", basicReqBody)
+		}
 	}
 
 	return nil
 }
 
-func getSuccessPayload(opts *NotifyOpts) string {
-	return fmt.Sprintf(`
-		{
-			"type": "section",
-			"text": {
-				"type": "mrkdwn",
-				"text": ":rocket: Your application %s was successfully updated on Porter! <%s|View the new release.>"
-			}
-		},
-	`, "`"+opts.Name+"`", opts.URL)
+func getDividerBlock() *SlackBlock {
+	return &SlackBlock{
+		Type: "divider",
+	}
 }
 
-func getFailedPayload(opts *NotifyOpts) string {
-	return fmt.Sprintf(`
-		{
-			"type": "section",
-			"text": {
-				"type": "mrkdwn",
-				"text": ":x: Your application %s failed to deploy on Porter. <%s|View the status here.>"
-			}
+func getMarkdownBlock(md string) *SlackBlock {
+	return &SlackBlock{
+		Type: "section",
+		Text: &SlackText{
+			Type: "mrkdwn",
+			Text: md,
 		},
-	`, "`"+opts.Name+"`", opts.URL)
+	}
+}
+
+func getMessageBlock(opts *NotifyOpts) *SlackBlock {
+	var md string
+
+	switch opts.Status {
+	case StatusDeployed:
+		md = getSuccessMessage(opts)
+	case StatusFailed:
+		md = getFailedMessage(opts)
+	}
+
+	return getMarkdownBlock(md)
+}
+
+func getInfoBlock(opts *NotifyOpts) *SlackBlock {
+	var md string
+
+	switch opts.Status {
+	case StatusFailed:
+		md = getFailedInfoMessage(opts)
+	default:
+		return nil
+	}
+
+	return getMarkdownBlock(md)
+}
+
+func getSuccessMessage(opts *NotifyOpts) string {
+	return fmt.Sprintf(
+		":rocket: Your application %s was successfully updated on Porter! <%s|View the new release.>",
+		"`"+opts.Name+"`",
+		opts.URL,
+	)
+}
+
+func getFailedMessage(opts *NotifyOpts) string {
+	return fmt.Sprintf(
+		":x: Your application %s failed to deploy on Porter. <%s|View the status here.>",
+		"`"+opts.Name+"`",
+		opts.URL,
+	)
+}
+
+func getFailedInfoMessage(opts *NotifyOpts) string {
+	info := opts.Info
+
+	// TODO: this casing is quite ugly and looks for particular types of API server
+	// errors, otherwise it truncates the error message to 200 characters. This should
+	// handle the errors more gracefully.
+	if strings.Contains(info, "Invalid value:") {
+		errArr := strings.Split(info, "Invalid value:")
+
+		// look for "unmarshalerDecoder" error
+		if strings.Contains(info, "unmarshalerDecoder") {
+			udArr := strings.Split(info, "unmarshalerDecoder:")
+
+			info = errArr[0] + udArr[1]
+		} else {
+			info = errArr[0] + "..."
+		}
+	} else if len(info) > 200 {
+		info = info[0:200] + "..."
+	}
+
+	return fmt.Sprintf("```\n%s\n```", info)
 }

+ 4 - 1
server/api/release_handler.go

@@ -1029,8 +1029,10 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 
 	if err != nil {
 		notifyOpts.Status = slack.StatusFailed
+		notifyOpts.Info = err.Error()
 
-		notifier.Notify(notifyOpts)
+		slackErr := notifier.Notify(notifyOpts)
+		fmt.Println("SLACK ERROR IS", slackErr)
 
 		app.sendExternalError(err, http.StatusInternalServerError, HTTPError{
 			Code:   ErrReleaseDeploy,
@@ -1252,6 +1254,7 @@ func (app *App) HandleReleaseDeployWebhook(w http.ResponseWriter, r *http.Reques
 
 	if err != nil {
 		notifyOpts.Status = slack.StatusFailed
+		notifyOpts.Info = err.Error()
 
 		notifier.Notify(notifyOpts)