notification.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. package notifications
  2. import (
  3. "context"
  4. "strings"
  5. "time"
  6. "github.com/google/uuid"
  7. "github.com/porter-dev/porter/internal/kubernetes"
  8. "github.com/porter-dev/porter/internal/porter_app/notifications/porter_error"
  9. "github.com/porter-dev/porter/internal/repository"
  10. "github.com/porter-dev/porter/internal/telemetry"
  11. )
  12. // HandleNotificationInput is the input to HandleNotification
  13. type HandleNotificationInput struct {
  14. // RawAgentEventMetadata is the raw metadata from the agent event
  15. RawAgentEventMetadata map[string]any
  16. // EventRepo is the repository for app events
  17. EventRepo repository.PorterAppEventRepository
  18. // DeploymentTargetID is the ID of the deployment target
  19. DeploymentTargetID string
  20. // Namespace is the namespace of the deployment target
  21. Namespace string
  22. // K8sAgent is the k8s agent, used to query for deployment info
  23. K8sAgent kubernetes.Agent
  24. }
  25. // HandleNotification handles the logic for processing agent events
  26. func HandleNotification(ctx context.Context, inp HandleNotificationInput) error {
  27. ctx, span := telemetry.NewSpan(ctx, "internal-handle-notification")
  28. defer span.End()
  29. // 1. parse agent event
  30. agentEventMetadata, err := parseAgentEventMetadata(inp.RawAgentEventMetadata)
  31. if err != nil {
  32. return telemetry.Error(ctx, span, err, "failed to unmarshal app event metadata")
  33. }
  34. if agentEventMetadata == nil {
  35. return telemetry.Error(ctx, span, nil, "app event metadata is nil")
  36. }
  37. // 2. convert agent event to notification
  38. hydratedNotification := agentEventToNotification(*agentEventMetadata)
  39. // 3. dedupe notification
  40. isDuplicate, err := isNotificationDuplicate(ctx, hydratedNotification, inp.EventRepo, inp.DeploymentTargetID)
  41. if err != nil {
  42. return telemetry.Error(ctx, span, err, "failed to check if app event is duplicate")
  43. }
  44. if isDuplicate {
  45. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "is-duplicate", Value: true})
  46. return nil
  47. }
  48. telemetry.WithAttributes(span,
  49. telemetry.AttributeKV{Key: "app-id", Value: hydratedNotification.AppID},
  50. telemetry.AttributeKV{Key: "app-name", Value: hydratedNotification.AppName},
  51. telemetry.AttributeKV{Key: "service-name", Value: hydratedNotification.ServiceName},
  52. telemetry.AttributeKV{Key: "app-revision-id", Value: hydratedNotification.AppRevisionID},
  53. telemetry.AttributeKV{Key: "agent-event-id", Value: hydratedNotification.AgentEventID},
  54. telemetry.AttributeKV{Key: "agent-detail", Value: hydratedNotification.AgentDetail},
  55. telemetry.AttributeKV{Key: "agent-summary", Value: hydratedNotification.AgentSummary},
  56. )
  57. if !strings.Contains(hydratedNotification.AgentSummary, "job run") {
  58. // 4. hydrate notification with k8s deployment info, only if this isn't a job run
  59. hydratedNotification, err = hydrateNotificationWithDeployment(ctx, hydrateNotificationWithDeploymentInput{
  60. Notification: hydratedNotification,
  61. DeploymentTargetId: inp.DeploymentTargetID,
  62. Namespace: inp.Namespace,
  63. K8sAgent: inp.K8sAgent,
  64. EventRepo: inp.EventRepo,
  65. })
  66. if err != nil {
  67. return telemetry.Error(ctx, span, err, "failed to hydrate notification with deployment")
  68. }
  69. }
  70. // 5. hydrate notification with a Porter error containing user-facing details
  71. hydratedNotification = hydrateNotificationWithError(ctx, hydratedNotification)
  72. // 6. based on notification + k8s deployment, update the status of the matching deploy event
  73. if hydratedNotification.Deployment.Status == DeploymentStatus_Failure ||
  74. (hydratedNotification.Deployment.Status == DeploymentStatus_Pending &&
  75. errorCodeIndicatesDeploymentFailure(hydratedNotification.Error.Code)) {
  76. err = updateDeployEvent(ctx, updateDeployEventInput{
  77. Notification: hydratedNotification,
  78. EventRepo: inp.EventRepo,
  79. Status: PorterAppEventStatus_Failed,
  80. })
  81. if err != nil {
  82. return telemetry.Error(ctx, span, err, "failed to update deploy event matching notification")
  83. }
  84. }
  85. // 7. save notification to db
  86. err = saveNotification(ctx, hydratedNotification, inp.EventRepo, inp.DeploymentTargetID)
  87. if err != nil {
  88. return telemetry.Error(ctx, span, err, "failed to save notification")
  89. }
  90. return nil
  91. }
  92. // Notification is a struct that contains all actionable information from an app event
  93. type Notification struct {
  94. // AppID is the ID of the app
  95. AppID string `json:"app_id"`
  96. // AppName is the name of the app
  97. AppName string `json:"app_name"`
  98. // ServiceName is the name of the service
  99. ServiceName string `json:"service_name"`
  100. // AppRevisionID is the ID of the app revision that the notification belongs to
  101. AppRevisionID string `json:"app_revision_id"`
  102. // AgentEventID is the ID of the agent event, used for deduping
  103. AgentEventID int `json:"agent_event_id"`
  104. // AgentDetail is the raw detail of the agent event
  105. AgentDetail string `json:"agent_detail"`
  106. // AgentSummary is the raw summary of the agent event
  107. AgentSummary string `json:"agent_summary"`
  108. // Error is the Porter error parsed from the agent event
  109. Error porter_error.PorterError `json:"error"`
  110. // Deployment is the deployment metadata, used to determine if the notification occurred during deployment or after
  111. Deployment Deployment `json:"deployment"`
  112. // Timestamp is the time that the notification was created
  113. Timestamp time.Time `json:"timestamp"`
  114. // ID is the ID of the notification
  115. ID uuid.UUID `json:"id"`
  116. }
  117. // agentEventToNotification converts an app event to a notification
  118. func agentEventToNotification(appEventMetadata AppEventMetadata) Notification {
  119. // There is a discrepancy between the predeploy naming; the front-end calls it "pre-deploy", but the job name is "predeploy"
  120. // This is a hack to make sure that the front-end can still parse the notification
  121. // TODO: rename the job to pre-deploy on the backend to match the front-end UI representation
  122. serviceName := appEventMetadata.ServiceName
  123. if serviceName == "predeploy" {
  124. serviceName = "pre-deploy"
  125. }
  126. notification := Notification{
  127. AppID: appEventMetadata.AppID,
  128. AppName: appEventMetadata.AppName,
  129. ServiceName: serviceName,
  130. AgentEventID: appEventMetadata.AgentEventID,
  131. AgentDetail: appEventMetadata.Detail,
  132. AgentSummary: appEventMetadata.Summary,
  133. AppRevisionID: appEventMetadata.AppRevisionID,
  134. Deployment: Deployment{Status: DeploymentStatus_Unknown},
  135. Timestamp: time.Now().UTC(),
  136. ID: uuid.New(),
  137. }
  138. return notification
  139. }