status.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. package porter_app
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "time"
  7. "github.com/porter-dev/porter/internal/deployment_target"
  8. "github.com/porter-dev/porter/internal/kubernetes"
  9. "github.com/porter-dev/porter/internal/telemetry"
  10. v1 "k8s.io/api/core/v1"
  11. )
  12. const (
  13. // LabelKey_DeploymentTargetID is the label key for the deployment target id
  14. LabelKey_DeploymentTargetID = "porter.run/deployment-target-id"
  15. // LabelKey_AppName is the label key for the app name
  16. LabelKey_AppName = "porter.run/app-name"
  17. // LabelKey_ServiceName is the label key for the service name
  18. LabelKey_ServiceName = "porter.run/service-name"
  19. // LabelKey_AppRevisionID is the label key for the app revision id
  20. LabelKey_AppRevisionID = "porter.run/app-revision-id"
  21. )
  22. // ServiceStatus describes the status of a service of a porter app
  23. type ServiceStatus struct {
  24. ServiceName string `json:"service_name"`
  25. RevisionStatusList []RevisionStatus `json:"revision_status_list"`
  26. }
  27. // RevisionStatus describes the status of a revision of a service of a porter app
  28. type RevisionStatus struct {
  29. RevisionID string `json:"revision_id"`
  30. RevisionNumber int `json:"revision_number"`
  31. InstanceStatusList []InstanceStatus `json:"instance_status_list"`
  32. }
  33. // InstanceStatusDescriptor is a string that summarizes the status of an instance
  34. type InstanceStatusDescriptor string
  35. const (
  36. // InstanceStatusDescriptor_Pending means the instance is pending
  37. InstanceStatusDescriptor_Pending InstanceStatusDescriptor = "PENDING"
  38. // InstanceStatusDescriptor_Running means the instance is running normally
  39. InstanceStatusDescriptor_Running InstanceStatusDescriptor = "RUNNING"
  40. // InstanceStatusDescriptor_Failed means the instance has failed
  41. InstanceStatusDescriptor_Failed InstanceStatusDescriptor = "FAILED"
  42. )
  43. // CrashLoopBackOff is a string that describes the status of a pod that is in a crash loop backoff
  44. const CrashLoopBackOff = "CrashLoopBackOff"
  45. // InstanceStatus describes the status of an instance of a revision of a service of a porter app
  46. type InstanceStatus struct {
  47. Status InstanceStatusDescriptor `json:"status"`
  48. RestartCount int `json:"restart_count"`
  49. CreationTimestamp time.Time `json:"creation_timestamp"`
  50. }
  51. // GetServiceStatusInput is the input type for GetServiceStatus
  52. type GetServiceStatusInput struct {
  53. DeploymentTarget deployment_target.DeploymentTarget
  54. Agent kubernetes.Agent
  55. AppName string
  56. ServiceName string
  57. AppRevisions []Revision
  58. }
  59. // GetServiceStatus returns the status of a service of a porter app
  60. func GetServiceStatus(ctx context.Context, inp GetServiceStatusInput) (ServiceStatus, error) {
  61. ctx, span := telemetry.NewSpan(ctx, "get-service-status")
  62. defer span.End()
  63. telemetry.WithAttributes(span,
  64. telemetry.AttributeKV{Key: "app-name", Value: inp.AppName},
  65. telemetry.AttributeKV{Key: "service-name", Value: inp.ServiceName},
  66. telemetry.AttributeKV{Key: "deployment-target-id", Value: inp.DeploymentTarget.ID},
  67. telemetry.AttributeKV{Key: "deployment-target-namespace", Value: inp.DeploymentTarget.Namespace},
  68. )
  69. serviceStatus := ServiceStatus{
  70. ServiceName: inp.ServiceName,
  71. }
  72. if inp.AppName == "" {
  73. return serviceStatus, telemetry.Error(ctx, span, nil, "must provide app name")
  74. }
  75. if inp.ServiceName == "" {
  76. return serviceStatus, telemetry.Error(ctx, span, nil, "must provide service name")
  77. }
  78. if inp.DeploymentTarget.ID == "" {
  79. return serviceStatus, telemetry.Error(ctx, span, nil, "must provide deployment target id")
  80. }
  81. if inp.DeploymentTarget.Namespace == "" {
  82. return serviceStatus, telemetry.Error(ctx, span, nil, "must provide deployment target namespace")
  83. }
  84. selectorString := fmt.Sprintf(
  85. "%s=%s,%s=%s,%s=%s",
  86. LabelKey_DeploymentTargetID, inp.DeploymentTarget.ID,
  87. LabelKey_AppName, inp.AppName,
  88. LabelKey_ServiceName, inp.ServiceName,
  89. )
  90. podList, err := inp.Agent.GetPodsByLabel(selectorString, inp.DeploymentTarget.Namespace)
  91. if err != nil {
  92. return serviceStatus, telemetry.Error(ctx, span, err, "error getting pods by label")
  93. }
  94. if podList == nil {
  95. return serviceStatus, telemetry.Error(ctx, span, nil, "pod list is nil")
  96. }
  97. revisionStatusList, err := revisionStatusFromPods(ctx, revisionStatusFromPodsInput{
  98. PodList: *podList,
  99. AppRevisions: inp.AppRevisions,
  100. AppName: inp.AppName,
  101. ServiceName: inp.ServiceName,
  102. })
  103. if err != nil {
  104. return serviceStatus, telemetry.Error(ctx, span, err, "error processing pods")
  105. }
  106. serviceStatus.RevisionStatusList = revisionStatusList
  107. return serviceStatus, nil
  108. }
  109. type revisionStatusFromPodsInput struct {
  110. PodList v1.PodList
  111. AppRevisions []Revision
  112. AppName string
  113. ServiceName string
  114. }
  115. func revisionStatusFromPods(ctx context.Context, inp revisionStatusFromPodsInput) ([]RevisionStatus, error) {
  116. ctx, span := telemetry.NewSpan(ctx, "revision-status-from-pods")
  117. defer span.End()
  118. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "num-pods", Value: len(inp.PodList.Items)})
  119. revisionStatusList := []RevisionStatus{}
  120. revisionToInstanceStatusMap := map[string][]InstanceStatus{}
  121. for _, pod := range inp.PodList.Items {
  122. revisionID := pod.Labels[LabelKey_AppRevisionID]
  123. if revisionID == "" {
  124. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "pod-name", Value: pod.Name})
  125. return revisionStatusList, telemetry.Error(ctx, span, nil, "pod does not have revision id label")
  126. }
  127. instanceStatusList, ok := revisionToInstanceStatusMap[revisionID]
  128. if !ok {
  129. instanceStatusList = []InstanceStatus{}
  130. }
  131. instanceStatus, err := instanceStatusFromPod(ctx, instanceStatusFromPodInput{
  132. Pod: pod,
  133. AppName: inp.AppName,
  134. ServiceName: inp.ServiceName,
  135. })
  136. if err != nil {
  137. continue
  138. }
  139. instanceStatusList = append(instanceStatusList, instanceStatus)
  140. revisionToInstanceStatusMap[revisionID] = instanceStatusList
  141. }
  142. for revisionId, instanceStatusList := range revisionToInstanceStatusMap {
  143. revisionNumber, err := getRevisionNumberFromRevisionId(revisionId, inp.AppRevisions)
  144. if err != nil {
  145. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "revision-id", Value: revisionId})
  146. return revisionStatusList, telemetry.Error(ctx, span, err, "error getting revision number from revision id")
  147. }
  148. // no number for this revision yet, so skip it from reporting
  149. if revisionNumber == 0 {
  150. continue
  151. }
  152. revisionStatus := RevisionStatus{
  153. RevisionID: revisionId,
  154. RevisionNumber: revisionNumber,
  155. InstanceStatusList: instanceStatusList,
  156. }
  157. revisionStatusList = append(revisionStatusList, revisionStatus)
  158. }
  159. return revisionStatusList, nil
  160. }
  161. type instanceStatusFromPodInput struct {
  162. Pod v1.Pod
  163. AppName string
  164. ServiceName string
  165. }
  166. func instanceStatusFromPod(ctx context.Context, inp instanceStatusFromPodInput) (InstanceStatus, error) {
  167. ctx, span := telemetry.NewSpan(ctx, "instance-status-from-pod")
  168. defer span.End()
  169. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "pod-name", Value: inp.Pod.Name})
  170. instanceStatus := InstanceStatus{}
  171. // find the container running the app code. Note that this is conditioned on the fact that
  172. // in our worker/web/job charts, there is one container created with this name during the deployment
  173. // there may be other containers (like the sidecar container for jobs), but we only care about the app container for reporting status
  174. appContainerName := fmt.Sprintf("%s-%s", inp.AppName, inp.ServiceName)
  175. var appContainerStatus v1.ContainerStatus
  176. for _, containerStatus := range inp.Pod.Status.ContainerStatuses {
  177. if containerStatus.Name == appContainerName {
  178. appContainerStatus = containerStatus
  179. break
  180. }
  181. }
  182. if appContainerStatus.Name == "" {
  183. return instanceStatus, telemetry.Error(ctx, span, nil, "app container not found")
  184. }
  185. instanceStatus.CreationTimestamp = inp.Pod.CreationTimestamp.Time
  186. instanceStatus.RestartCount = int(appContainerStatus.RestartCount)
  187. switch inp.Pod.Status.Phase {
  188. case v1.PodPending:
  189. instanceStatus.Status = InstanceStatusDescriptor_Pending
  190. case v1.PodRunning:
  191. instanceStatus.Status = InstanceStatusDescriptor_Running
  192. case v1.PodFailed:
  193. instanceStatus.Status = InstanceStatusDescriptor_Failed
  194. }
  195. if appContainerStatus.State.Waiting != nil && appContainerStatus.State.Waiting.Reason == CrashLoopBackOff {
  196. instanceStatus.Status = InstanceStatusDescriptor_Failed
  197. }
  198. return instanceStatus, nil
  199. }
  200. func getRevisionNumberFromRevisionId(revisionId string, appRevisions []Revision) (int, error) {
  201. for _, revision := range appRevisions {
  202. if revision.ID == revisionId {
  203. return int(revision.RevisionNumber), nil
  204. }
  205. }
  206. return 0, errors.New("revision id not found in app revisions")
  207. }