deployment.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. package notifications
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "strings"
  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. v1 "k8s.io/api/apps/v1"
  12. )
  13. // Deployment represents metadata about a k8s deployment
  14. type Deployment struct {
  15. Status DeploymentStatus `json:"status"`
  16. }
  17. // DeploymentStatus represents the status of a k8s deployment
  18. type DeploymentStatus string
  19. const (
  20. // DeploymentStatus_Unknown indicates that the status of the deployment is unknown because we have not queried for it yet
  21. DeploymentStatus_Unknown DeploymentStatus = "UNKNOWN"
  22. // DeploymentStatus_Pending indicates that the deployment is still in progress
  23. DeploymentStatus_Pending DeploymentStatus = "PENDING"
  24. // DeploymentStatus_Success indicates that the deployment was successful
  25. DeploymentStatus_Success DeploymentStatus = "SUCCESS"
  26. // DeploymentStatus_Failure indicates that the deployment failed
  27. DeploymentStatus_Failure DeploymentStatus = "FAILURE"
  28. )
  29. // hydrateNotificationWithDeploymentInput is the input struct for hydrateNotificationWithDeployment
  30. type hydrateNotificationWithDeploymentInput struct {
  31. // Notification is the notification to hydrate
  32. Notification
  33. // DeploymentTargetId is the ID of the deployment target
  34. DeploymentTargetId string
  35. // Namespace is the namespace of the deployment target
  36. Namespace string
  37. // K8sAgent is the k8s agent, used to query for deployment info
  38. K8sAgent kubernetes.Agent
  39. // EventRepo is the repository for app events, used to check if we've already marked this deployment as successful/failed
  40. EventRepo repository.PorterAppEventRepository
  41. }
  42. // hydrateNotificationWithDeployment hydrates a notification with k8s deployment info
  43. func hydrateNotificationWithDeployment(ctx context.Context, inp hydrateNotificationWithDeploymentInput) (Notification, error) {
  44. ctx, span := telemetry.NewSpan(ctx, "hydrate-notification-with-deployment")
  45. defer span.End()
  46. hydratedNotification := inp.Notification
  47. if inp.Notification.Deployment.Status != DeploymentStatus_Unknown {
  48. return hydratedNotification, nil
  49. }
  50. telemetry.WithAttributes(span,
  51. telemetry.AttributeKV{Key: "deployment-target-id", Value: inp.DeploymentTargetId},
  52. telemetry.AttributeKV{Key: "namespace", Value: inp.Namespace},
  53. telemetry.AttributeKV{Key: "app-name", Value: inp.AppName},
  54. telemetry.AttributeKV{Key: "app-revision-id", Value: inp.Notification.AppRevisionID},
  55. telemetry.AttributeKV{Key: "service-name", Value: inp.ServiceName},
  56. )
  57. // first, we check if we've already marked this deployment as successful or failed
  58. status, err := porterAppDeployEventStatus(ctx, porterAppDeployEventStatusInput{
  59. AppID: inp.AppID,
  60. EventRepo: inp.EventRepo,
  61. AppRevisionID: inp.Notification.AppRevisionID,
  62. ServiceName: inp.Notification.ServiceName,
  63. })
  64. if err != nil {
  65. err := telemetry.Error(ctx, span, err, "failed to get deployment status from db")
  66. return hydratedNotification, err
  67. }
  68. // the status is still pending in the db, so we haven't updated the user on it yet
  69. // therefore, we check the k8s deployment status
  70. if status == DeploymentStatus_Pending {
  71. selectors := []string{
  72. fmt.Sprintf("porter.run/deployment-target-id=%s", inp.DeploymentTargetId),
  73. fmt.Sprintf("porter.run/app-name=%s", inp.AppName),
  74. fmt.Sprintf("porter.run/app-revision-id=%s", inp.Notification.AppRevisionID),
  75. fmt.Sprintf("porter.run/service-name=%s", inp.ServiceName),
  76. }
  77. depls, err := inp.K8sAgent.GetDeploymentsBySelector(ctx, inp.Namespace, strings.Join(selectors, ","))
  78. if err != nil {
  79. err := telemetry.Error(ctx, span, err, "failed to get deployments for notification")
  80. return hydratedNotification, err
  81. }
  82. if len(depls.Items) == 0 {
  83. err := telemetry.Error(ctx, span, nil, "no deployments found for notification")
  84. return hydratedNotification, err
  85. }
  86. if len(depls.Items) > 1 {
  87. err := telemetry.Error(ctx, span, nil, "multiple deployments found for notification")
  88. return hydratedNotification, err
  89. }
  90. matchingDeployment := depls.Items[0]
  91. telemetry.WithAttributes(span,
  92. telemetry.AttributeKV{Key: "deployment-name", Value: matchingDeployment.Name},
  93. telemetry.AttributeKV{Key: "deployment-uid", Value: matchingDeployment.ObjectMeta.UID},
  94. telemetry.AttributeKV{Key: "deployment-creation-timestamp", Value: matchingDeployment.ObjectMeta.CreationTimestamp},
  95. )
  96. status = k8sDeploymentStatus(matchingDeployment)
  97. }
  98. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-status", Value: status})
  99. if status == DeploymentStatus_Unknown {
  100. err := telemetry.Error(ctx, span, nil, "unable to determine status of deployment")
  101. return hydratedNotification, err
  102. }
  103. hydratedNotification.Deployment = Deployment{
  104. Status: status,
  105. }
  106. return hydratedNotification, nil
  107. }
  108. // porterAppDeployEventStatusInput is the input struct for porterAppDeployEventStatus
  109. type porterAppDeployEventStatusInput struct {
  110. // AppID is the ID of the app
  111. AppID string
  112. // EventRepo is the repository for app events, used to check if we've already marked this deployment as successful/failed
  113. EventRepo repository.PorterAppEventRepository
  114. // AppRevisionID is the ID of the app revision
  115. AppRevisionID string
  116. // ServiceName is the name of the service
  117. ServiceName string
  118. }
  119. // porterAppDeployEventStatus returns the status of a deploy event from the app events repository
  120. func porterAppDeployEventStatus(ctx context.Context, inp porterAppDeployEventStatusInput) (DeploymentStatus, error) {
  121. ctx, span := telemetry.NewSpan(ctx, "db-deploy-event-status")
  122. defer span.End()
  123. deploymentStatus := DeploymentStatus_Unknown
  124. appIdInt, err := strconv.Atoi(inp.AppID)
  125. if err != nil {
  126. return deploymentStatus, telemetry.Error(ctx, span, err, "failed to convert app id to int")
  127. }
  128. matchingDeployEvent, err := inp.EventRepo.ReadDeployEventByAppRevisionID(ctx, uint(appIdInt), inp.AppRevisionID)
  129. if err != nil {
  130. return deploymentStatus, telemetry.Error(ctx, span, err, "failed to read deploy event by app revision id")
  131. }
  132. serviceDeploymentMetadata, err := serviceDeploymentMetadataFromDeployEvent(ctx, matchingDeployEvent, inp.ServiceName)
  133. if err != nil {
  134. return deploymentStatus, telemetry.Error(ctx, span, err, "failed to get service deployment metadata from deploy event")
  135. }
  136. switch serviceDeploymentMetadata.Status {
  137. case PorterAppEventStatus_Success:
  138. deploymentStatus = DeploymentStatus_Success
  139. case PorterAppEventStatus_Failed:
  140. deploymentStatus = DeploymentStatus_Failure
  141. case PorterAppEventStatus_Progressing:
  142. deploymentStatus = DeploymentStatus_Pending
  143. default:
  144. deploymentStatus = DeploymentStatus_Unknown
  145. }
  146. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-status", Value: string(deploymentStatus)})
  147. return deploymentStatus, nil
  148. }
  149. // k8sDeploymentStatus returns the status of a k8s deployment
  150. func k8sDeploymentStatus(depl v1.Deployment) DeploymentStatus {
  151. deploymentStatus := DeploymentStatus_Unknown
  152. if depl.Status.Replicas == depl.Status.ReadyReplicas &&
  153. depl.Status.Replicas == depl.Status.AvailableReplicas &&
  154. depl.Status.Replicas == depl.Status.UpdatedReplicas {
  155. deploymentStatus = DeploymentStatus_Success
  156. } else {
  157. for _, condition := range depl.Status.Conditions {
  158. if condition.Type == "Progressing" {
  159. if condition.Status == "False" && condition.Reason == "ProgressDeadlineExceeded" {
  160. deploymentStatus = DeploymentStatus_Failure
  161. break
  162. } else {
  163. deploymentStatus = DeploymentStatus_Pending
  164. }
  165. }
  166. }
  167. }
  168. return deploymentStatus
  169. }
  170. var fatalDeploymentErrorCodes = []porter_error.PorterErrorCode{
  171. porter_error.PorterErrorCode_NonZeroExitCode,
  172. porter_error.PorterErrorCode_NonZeroExitCode_InvalidStartCommand,
  173. porter_error.PorterErrorCode_NonZeroExitCode_CommonIssues,
  174. porter_error.PorterErrorCode_ReadinessHealthCheck,
  175. porter_error.PorterErrorCode_LivenessHealthCheck,
  176. porter_error.PorterErrorCode_InvalidImageError,
  177. porter_error.PorterErrorCode_RestartedDueToError,
  178. porter_error.PorterErrorCode_MemoryLimitExceeded_ScaleUp,
  179. porter_error.PorterErrorCode_CPULimitExceeded_ScaleUp,
  180. porter_error.PorterErrorCode_CannotBeScheduled,
  181. }
  182. // errorCodeIndicatesDeploymentFailure returns true if the error code indicates that the deployment will eventually time out and fail
  183. // we use this to report deployment failure to the user early, rather than waiting for the deployment to time out
  184. func errorCodeIndicatesDeploymentFailure(errorCode porter_error.PorterErrorCode) bool {
  185. for _, fatalErrorCode := range fatalDeploymentErrorCodes {
  186. if errorCode == fatalErrorCode {
  187. return true
  188. }
  189. }
  190. return false
  191. }