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

Merge branch 'belanger/por-160-pod-events-backend' of github.com:porter-dev/porter into nico/alert-events-frontend-implementation

jnfrati 4 лет назад
Родитель
Сommit
69bc4db5a7

+ 3 - 0
.gitignore

@@ -15,6 +15,9 @@ staging.sh
 *.key
 bin
 
+# Local docs directories
+/docs/.obsidian
+
 # Local .terraform directories
 **/.terraform/*
 

+ 106 - 0
api/server/handlers/kube_events/create.go

@@ -1,7 +1,9 @@
 package kube_events
 
 import (
+	"errors"
 	"net/http"
+	"strings"
 	"time"
 
 	"github.com/porter-dev/porter/api/server/authz"
@@ -10,7 +12,9 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/integrations/slack"
 	"github.com/porter-dev/porter/internal/models"
+	"gorm.io/gorm"
 )
 
 type CreateKubeEventHandler struct {
@@ -81,4 +85,106 @@ func (c *CreateKubeEventHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	}
 
 	w.WriteHeader(http.StatusCreated)
+
+	if strings.ToLower(string(request.EventType)) == "critical" && strings.ToLower(request.ResourceType) == "pod" {
+		err := notifyPodCrashing(c.Config(), proj, cluster, request)
+
+		if err != nil {
+			c.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
+		}
+	}
+}
+
+func notifyPodCrashing(
+	config *config.Config,
+	project *models.Project,
+	cluster *models.Cluster,
+	event *types.CreateKubeEventRequest,
+) error {
+	notifConfig := &types.NotificationConfig{
+		Enabled: true,
+		Success: true,
+		Failure: true,
+	}
+
+	// attempt to get a matching Porter release to get the notification configuration
+	var conf *models.NotificationConfig
+	var err error
+	matchedRel := getMatchedPorterRelease(config, cluster.ID, event.OwnerName, event.Namespace)
+
+	if matchedRel != nil {
+		conf, err = config.Repo.NotificationConfig().ReadNotificationConfig(matchedRel.NotificationConfig)
+
+		if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
+			conf = &models.NotificationConfig{
+				Enabled: true,
+				Success: true,
+				Failure: true,
+			}
+
+			conf, err = config.Repo.NotificationConfig().CreateNotificationConfig(conf)
+
+			if err == nil {
+				notifConfig = conf.ToNotificationConfigType()
+			}
+		} else if err == nil && conf != nil {
+			if !conf.ShouldNotify() {
+				return nil
+			}
+
+			notifConfig = conf.ToNotificationConfigType()
+		}
+	}
+
+	slackInts, _ := config.Repo.SlackIntegration().ListSlackIntegrationsByProjectID(project.ID)
+
+	notifier := slack.NewSlackNotifier(notifConfig, slackInts...)
+
+	notifyOpts := &slack.NotifyOpts{
+		ProjectID:   cluster.ProjectID,
+		ClusterID:   cluster.ID,
+		ClusterName: cluster.Name,
+		Name:        event.OwnerName,
+		Namespace:   event.Namespace,
+		URL:         config.ServerConf.ServerURL,
+	}
+
+	notifyOpts.Status = slack.StatusPodCrashed
+
+	err = notifier.Notify(notifyOpts)
+
+	if err != nil {
+		return err
+	}
+
+	// update the last updated time
+	if matchedRel != nil && conf != nil {
+		conf.LastNotifiedTime = time.Now()
+		conf, err = config.Repo.NotificationConfig().UpdateNotificationConfig(conf)
+	}
+
+	return err
+}
+
+// getMatchedPorterRelease attempts to find a matching Porter release from the name of a controller.
+// For example, if the controller has a suffix "-web", it is likely a Porter web application, and
+// so we query for a Porter release with a matching name. Returns nil if no match is found
+func getMatchedPorterRelease(config *config.Config, clusterID uint, ownerName, namespace string) *models.Release {
+	matchingName := ""
+
+	if strings.Contains(ownerName, "-web") {
+		matchingName = strings.Split(ownerName, "-web")[0]
+	} else if strings.Contains(ownerName, "-worker") {
+		matchingName = strings.Split(ownerName, "-worker")[0]
+	} else if strings.Contains(ownerName, "-job") {
+		matchingName = strings.Split(ownerName, "-job")[0]
+	}
+
+	rel, err := config.Repo.Release().ReadRelease(clusterID, matchingName, namespace)
+
+	if err != nil {
+		return nil
+	}
+
+	return rel
 }

+ 2 - 2
api/server/handlers/release/ugprade.go

@@ -153,7 +153,7 @@ func (c *UpgradeReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	}
 
 	if upgradeErr != nil {
-		notifyOpts.Status = slack.StatusFailed
+		notifyOpts.Status = slack.StatusHelmFailed
 		notifyOpts.Info = upgradeErr.Error()
 
 		notifier.Notify(notifyOpts)
@@ -166,7 +166,7 @@ func (c *UpgradeReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		return
 	}
 
-	notifyOpts.Status = string(helmRelease.Info.Status)
+	notifyOpts.Status = slack.StatusHelmDeployed
 	notifyOpts.Version = helmRelease.Version
 
 	notifier.Notify(notifyOpts)

+ 2 - 2
api/server/handlers/release/upgrade_webhook.go

@@ -168,7 +168,7 @@ func (c *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	rel, err = helmAgent.UpgradeReleaseByValues(conf, c.Config().DOConf)
 
 	if err != nil {
-		notifyOpts.Status = slack.StatusFailed
+		notifyOpts.Status = slack.StatusHelmFailed
 		notifyOpts.Info = err.Error()
 
 		notifier.Notify(notifyOpts)
@@ -181,7 +181,7 @@ func (c *WebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	notifyOpts.Status = string(rel.Info.Status)
+	notifyOpts.Status = slack.StatusHelmDeployed
 	notifyOpts.Version = rel.Version
 
 	notifier.Notify(notifyOpts)

+ 2 - 0
api/types/release.go

@@ -114,6 +114,8 @@ type NotificationConfig struct {
 	Enabled bool `json:"enabled"`
 	Success bool `json:"success"`
 	Failure bool `json:"failure"`
+
+	NotifLimit string `json:"notif_limit"`
 }
 
 type GetNotificationConfigResponse struct {

+ 65 - 30
internal/integrations/slack/notifier.go

@@ -19,8 +19,9 @@ type Notifier interface {
 type DeploymentStatus string
 
 const (
-	StatusDeployed string = "deployed"
-	StatusFailed   string = "failed"
+	StatusHelmDeployed DeploymentStatus = "helm_deployed"
+	StatusPodCrashed   DeploymentStatus = "pod_crashed"
+	StatusHelmFailed   DeploymentStatus = "helm_failed"
 )
 
 type NotifyOpts struct {
@@ -34,7 +35,7 @@ type NotifyOpts struct {
 	ClusterName string
 
 	// Status is the current status of the deployment.
-	Status string
+	Status DeploymentStatus
 
 	// Info is any additional information about this status, such as an error message if
 	// the deployment failed.
@@ -82,38 +83,29 @@ func (s *SlackNotifier) Notify(opts *NotifyOpts) error {
 		if !s.Config.Enabled {
 			return nil
 		}
-		if opts.Status == StatusDeployed && !s.Config.Success {
+		if opts.Status == StatusHelmDeployed && !s.Config.Success {
 			return nil
 		}
-		if opts.Status == StatusFailed && !s.Config.Failure {
+		if opts.Status == StatusPodCrashed && !s.Config.Failure {
+			return nil
+		}
+		if opts.Status == StatusHelmFailed && !s.Config.Failure {
 			return nil
 		}
-	}
-
-	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)),
 	}
 
 	// 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,
-	}
-
-	infoBlock := getInfoBlock(opts)
-
-	if infoBlock != nil {
-		blocks = append(blocks, infoBlock)
-	}
+	blocks, basicBlocks := getSlackBlocks(opts)
 
 	slackPayload := &SlackPayload{
 		Blocks: blocks,
 	}
 
+	basicSlackPayload := &SlackPayload{
+		Blocks: basicBlocks,
+	}
+
 	basicPayload, err := json.Marshal(basicSlackPayload)
 
 	if err != nil {
@@ -143,6 +135,37 @@ func (s *SlackNotifier) Notify(opts *NotifyOpts) error {
 	return nil
 }
 
+func getSlackBlocks(opts *NotifyOpts) ([]*SlackBlock, []*SlackBlock) {
+	res := []*SlackBlock{}
+
+	if opts.Status == StatusHelmDeployed || opts.Status == StatusHelmFailed {
+		res = append(res, getHelmMessageBlock(opts))
+	} else if opts.Status == StatusPodCrashed {
+		res = append(res, getPodCrashedMessageBlock(opts))
+	}
+
+	res = append(
+		res,
+		getDividerBlock(),
+		getMarkdownBlock(fmt.Sprintf("*Name:* %s", "`"+opts.Name+"`")),
+		getMarkdownBlock(fmt.Sprintf("*Namespace:* %s", "`"+opts.Namespace+"`")),
+	)
+
+	if opts.Status == StatusHelmDeployed || opts.Status == StatusHelmFailed {
+		res = append(res, getMarkdownBlock(fmt.Sprintf("*Version:* %d", opts.Version)))
+	}
+
+	basicRes := res
+
+	infoBlock := getInfoBlock(opts)
+
+	if infoBlock != nil {
+		res = append(res, infoBlock)
+	}
+
+	return res, basicRes
+}
+
 func getDividerBlock() *SlackBlock {
 	return &SlackBlock{
 		Type: "divider",
@@ -159,24 +182,36 @@ func getMarkdownBlock(md string) *SlackBlock {
 	}
 }
 
-func getMessageBlock(opts *NotifyOpts) *SlackBlock {
+func getHelmMessageBlock(opts *NotifyOpts) *SlackBlock {
 	var md string
 
 	switch opts.Status {
-	case StatusDeployed:
-		md = getSuccessMessage(opts)
-	case StatusFailed:
-		md = getFailedMessage(opts)
+	case StatusHelmDeployed:
+		md = getHelmSuccessMessage(opts)
+	case StatusHelmFailed:
+		md = getHelmFailedMessage(opts)
 	}
 
 	return getMarkdownBlock(md)
 }
 
+func getPodCrashedMessageBlock(opts *NotifyOpts) *SlackBlock {
+	md := fmt.Sprintf(
+		":x: Your application %s crashed on Porter. <%s|View the new release.>",
+		"`"+opts.Name+"`",
+		opts.URL,
+	)
+
+	return getMarkdownBlock(md)
+}
+
 func getInfoBlock(opts *NotifyOpts) *SlackBlock {
 	var md string
 
 	switch opts.Status {
-	case StatusFailed:
+	case StatusHelmFailed:
+		md = getFailedInfoMessage(opts)
+	case StatusPodCrashed:
 		md = getFailedInfoMessage(opts)
 	default:
 		return nil
@@ -185,7 +220,7 @@ func getInfoBlock(opts *NotifyOpts) *SlackBlock {
 	return getMarkdownBlock(md)
 }
 
-func getSuccessMessage(opts *NotifyOpts) string {
+func getHelmSuccessMessage(opts *NotifyOpts) string {
 	return fmt.Sprintf(
 		":rocket: Your application %s was successfully updated on Porter! <%s|View the new release.>",
 		"`"+opts.Name+"`",
@@ -193,7 +228,7 @@ func getSuccessMessage(opts *NotifyOpts) string {
 	)
 }
 
-func getFailedMessage(opts *NotifyOpts) string {
+func getHelmFailedMessage(opts *NotifyOpts) string {
 	return fmt.Sprintf(
 		":x: Your application %s failed to deploy on Porter. <%s|View the status here.>",
 		"`"+opts.Name+"`",

+ 19 - 3
internal/models/notification.go

@@ -1,6 +1,8 @@
 package models
 
 import (
+	"time"
+
 	"github.com/porter-dev/porter/api/types"
 	"gorm.io/gorm"
 )
@@ -12,12 +14,26 @@ type NotificationConfig struct {
 
 	Success bool
 	Failure bool
+
+	LastNotifiedTime time.Time
+	NotifLimit       string
 }
 
 func (conf *NotificationConfig) ToNotificationConfigType() *types.NotificationConfig {
 	return &types.NotificationConfig{
-		Enabled: conf.Enabled,
-		Success: conf.Success,
-		Failure: conf.Failure,
+		Enabled:    conf.Enabled,
+		Success:    conf.Success,
+		Failure:    conf.Failure,
+		NotifLimit: conf.NotifLimit,
 	}
 }
+
+func (conf *NotificationConfig) ShouldNotify() bool {
+	// check the last notified time against the notification limit
+	return conf.LastNotifiedTime.Before(notifLimitToTime(conf.NotifLimit))
+}
+
+func notifLimitToTime(notifTime string) time.Time {
+	// TODO: compute a time that's not just 5 min
+	return time.Now().Add(-5 * time.Minute)
+}

+ 1 - 4
internal/repository/gorm/event.go

@@ -195,10 +195,7 @@ func (repo *KubeEventRepository) ListEventsByProjectID(
 		)
 	}
 
-	if listOpts.SortBy == "timestamp" {
-		// sort by the updated_at field
-		query = query.Order("updated_at desc").Order("id desc")
-	}
+	query = query.Order("updated_at desc").Order("id desc")
 
 	// get the count before limit and offset
 	var count int64