revisions.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. package porter_app
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "errors"
  6. "time"
  7. "connectrpc.com/connect"
  8. "github.com/google/uuid"
  9. "github.com/porter-dev/api-contracts/generated/go/helpers"
  10. porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
  11. "github.com/porter-dev/api-contracts/generated/go/porter/v1/porterv1connect"
  12. "github.com/porter-dev/porter/internal/deployment_target"
  13. "github.com/porter-dev/porter/internal/kubernetes"
  14. "github.com/porter-dev/porter/internal/kubernetes/environment_groups"
  15. "github.com/porter-dev/porter/internal/models"
  16. "github.com/porter-dev/porter/internal/repository"
  17. "github.com/porter-dev/porter/internal/telemetry"
  18. )
  19. // Revision represents the data for a single revision
  20. type Revision struct {
  21. // ID is the revision id
  22. ID string `json:"id"`
  23. // B64AppProto is the base64 encoded app proto definition
  24. B64AppProto string `json:"b64_app_proto"`
  25. // Status is the status of the revision
  26. Status models.AppRevisionStatus `json:"status"`
  27. // RevisionNumber is the revision number with respect to the app and deployment target
  28. RevisionNumber uint64 `json:"revision_number"`
  29. // CreatedAt is the time the revision was created
  30. CreatedAt time.Time `json:"created_at"`
  31. // UpdatedAt is the time the revision was updated
  32. UpdatedAt time.Time `json:"updated_at"`
  33. // DeploymentTargetID is the id of the deployment target the revision is associated with
  34. DeploymentTarget DeploymentTarget `json:"deployment_target"`
  35. // Env is the environment variables for the revision
  36. Env environment_groups.EnvironmentGroup `json:"env,omitempty"`
  37. // AppInstanceID is the id of the app instance the revision is associated with
  38. AppInstanceID uuid.UUID `json:"app_instance_id"`
  39. // Description is the description of the revision
  40. B64Description string `json:"b64_description"`
  41. }
  42. // RevisionProgress describes the progress of a revision in its lifecycle
  43. type RevisionProgress struct {
  44. // PredeployStarted is true if the predeploy process has started
  45. PredeployStarted bool `json:"predeploy_started"`
  46. // PredeploySuccessful is true if the predeploy process has completed successfully
  47. PredeploySuccessful bool `json:"predeploy_successful"`
  48. // PredeployFailed is true if the predeploy process has failed
  49. PredeployFailed bool `json:"predeploy_failed"`
  50. // InstallStarted is true if the install process has started
  51. InstallStarted bool `json:"install_started"`
  52. // InstallSuccessful is true if the install process has completed successfully
  53. InstallSuccessful bool `json:"install_successful"`
  54. // InstallFailed is true if the install process has failed
  55. InstallFailed bool `json:"install_failed"`
  56. // DeploymentStarted is true if the deployment process has started
  57. DeploymentStarted bool `json:"deployment_started"`
  58. // DeploymentSuccessful is true if the deployment process has completed successfully
  59. DeploymentSuccessful bool `json:"deployment_successful"`
  60. // DeploymentFailed is true if the deployment process has failed
  61. DeploymentFailed bool `json:"deployment_failed"`
  62. // IsInTerminalStatus is true if the revision is in a terminal status
  63. IsInTerminalStatus bool `json:"is_in_terminal_status"`
  64. }
  65. // DeploymentTarget is a simplified version of the deployment target struct
  66. type DeploymentTarget struct {
  67. ID string `json:"id"`
  68. Name string `json:"name"`
  69. }
  70. // GetAppRevisionInput is the input struct for GetAppRevisions
  71. type GetAppRevisionInput struct {
  72. ProjectID uint
  73. AppRevisionID uuid.UUID
  74. CCPClient porterv1connect.ClusterControlPlaneServiceClient
  75. }
  76. // GetAppRevision returns a single app revision by id
  77. func GetAppRevision(ctx context.Context, inp GetAppRevisionInput) (Revision, error) {
  78. ctx, span := telemetry.NewSpan(ctx, "get-app-revision")
  79. defer span.End()
  80. var revision Revision
  81. if inp.ProjectID == 0 {
  82. return revision, telemetry.Error(ctx, span, nil, "must provide a project id")
  83. }
  84. if inp.AppRevisionID == uuid.Nil {
  85. return revision, telemetry.Error(ctx, span, nil, "must provide an app revision id")
  86. }
  87. getRevisionReq := connect.NewRequest(&porterv1.GetAppRevisionRequest{
  88. ProjectId: int64(inp.ProjectID),
  89. AppRevisionId: inp.AppRevisionID.String(),
  90. })
  91. ccpResp, err := inp.CCPClient.GetAppRevision(ctx, getRevisionReq)
  92. if err != nil {
  93. return revision, telemetry.Error(ctx, span, err, "error getting app revision")
  94. }
  95. if ccpResp == nil || ccpResp.Msg == nil {
  96. return revision, telemetry.Error(ctx, span, nil, "get app revision response is nil")
  97. }
  98. appRevisionProto := ccpResp.Msg.AppRevision
  99. revision, err = EncodedRevisionFromProto(ctx, appRevisionProto)
  100. if err != nil {
  101. return revision, telemetry.Error(ctx, span, err, "error converting app revision from proto")
  102. }
  103. return revision, nil
  104. }
  105. // EncodedRevisionFromProto converts an AppRevision proto object into a Revision object
  106. func EncodedRevisionFromProto(ctx context.Context, appRevision *porterv1.AppRevision) (Revision, error) {
  107. ctx, span := telemetry.NewSpan(ctx, "encoded-revision-from-proto")
  108. defer span.End()
  109. var revision Revision
  110. if appRevision == nil {
  111. return revision, telemetry.Error(ctx, span, nil, "current app revision definition is nil")
  112. }
  113. appProto := appRevision.App
  114. if appProto == nil {
  115. return revision, telemetry.Error(ctx, span, nil, "app proto is nil")
  116. }
  117. encoded, err := helpers.MarshalContractObject(ctx, appProto)
  118. if err != nil {
  119. return revision, telemetry.Error(ctx, span, err, "error marshalling app proto back to json")
  120. }
  121. b64 := base64.StdEncoding.EncodeToString(encoded)
  122. status, err := appRevisionStatusFromProto(appRevision.Status)
  123. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "status", Value: string(status)})
  124. if err != nil {
  125. _ = telemetry.Error(ctx, span, nil, "unknown revision type") // flagged as an error for visibility
  126. }
  127. appInstanceIdStr := appRevision.AppInstanceId
  128. appInstanceId, err := uuid.Parse(appInstanceIdStr)
  129. if err != nil {
  130. return revision, telemetry.Error(ctx, span, err, "error parsing app instance id")
  131. }
  132. revision = Revision{
  133. B64AppProto: b64,
  134. Status: status,
  135. ID: appRevision.Id,
  136. RevisionNumber: appRevision.RevisionNumber,
  137. CreatedAt: appRevision.CreatedAt.AsTime(),
  138. UpdatedAt: appRevision.UpdatedAt.AsTime(),
  139. DeploymentTarget: DeploymentTarget{ID: appRevision.DeploymentTargetId},
  140. AppInstanceID: appInstanceId,
  141. B64Description: appRevision.B64Description,
  142. }
  143. return revision, nil
  144. }
  145. // AttachEnvToRevisionInput is the input struct for AttachEnvToRevision
  146. type AttachEnvToRevisionInput struct {
  147. ProjectID uint
  148. ClusterID int
  149. Revision Revision
  150. DeploymentTarget deployment_target.DeploymentTarget
  151. K8SAgent *kubernetes.Agent
  152. PorterAppRepository repository.PorterAppRepository
  153. }
  154. // AttachEnvToRevision attaches the environment variables from the app's default env group to a revision
  155. // These are the variables that are displayed to the user in the UI as associated with the app rather than an env group
  156. func AttachEnvToRevision(ctx context.Context, inp AttachEnvToRevisionInput) (Revision, error) {
  157. ctx, span := telemetry.NewSpan(ctx, "attach-env-to-revision")
  158. defer span.End()
  159. revision := inp.Revision
  160. if inp.ProjectID == 0 {
  161. return revision, telemetry.Error(ctx, span, nil, "must provide a project id")
  162. }
  163. if inp.ClusterID == 0 {
  164. return revision, telemetry.Error(ctx, span, nil, "must provide a cluster id")
  165. }
  166. if inp.K8SAgent == nil {
  167. return revision, telemetry.Error(ctx, span, nil, "k8s agent is nil")
  168. }
  169. decoded, err := base64.StdEncoding.DecodeString(revision.B64AppProto)
  170. if err != nil {
  171. return revision, telemetry.Error(ctx, span, err, "error decoding app proto")
  172. }
  173. appDef := &porterv1.PorterApp{}
  174. err = helpers.UnmarshalContractObject(decoded, appDef)
  175. if err != nil {
  176. return revision, telemetry.Error(ctx, span, err, "error unmarshalling app proto")
  177. }
  178. envName, err := AppEnvGroupName(ctx, appDef.Name, inp.Revision.DeploymentTarget.ID, uint(inp.ClusterID), inp.PorterAppRepository)
  179. if err != nil {
  180. return revision, telemetry.Error(ctx, span, err, "error getting app env group name")
  181. }
  182. envNameFilter := []string{envName}
  183. envFromProtoInp := AppEnvironmentFromProtoInput{
  184. ProjectID: inp.ProjectID,
  185. ClusterID: inp.ClusterID,
  186. App: appDef,
  187. K8SAgent: inp.K8SAgent,
  188. DeploymentTarget: inp.DeploymentTarget,
  189. }
  190. envGroups, err := AppEnvironmentFromProto(ctx, envFromProtoInp, WithEnvGroupFilter(envNameFilter), WithSecrets())
  191. if err != nil {
  192. return revision, telemetry.Error(ctx, span, err, "error getting app environment from revision")
  193. }
  194. if len(envGroups) > 1 {
  195. return revision, telemetry.Error(ctx, span, err, "multiple app envs groups returned for same name")
  196. }
  197. if len(envGroups) == 1 {
  198. revision.Env = envGroups[0]
  199. }
  200. return revision, nil
  201. }
  202. func appRevisionStatusFromProto(status string) (models.AppRevisionStatus, error) {
  203. appRevisionStatus := models.AppRevisionStatus_Unknown
  204. switch status {
  205. case string(models.AppRevisionStatus_ImageAvailable):
  206. appRevisionStatus = models.AppRevisionStatus_ImageAvailable
  207. case string(models.AppRevisionStatus_AwaitingBuild):
  208. appRevisionStatus = models.AppRevisionStatus_AwaitingBuild
  209. case string(models.AppRevisionStatus_AwaitingPredeploy):
  210. appRevisionStatus = models.AppRevisionStatus_AwaitingPredeploy
  211. case string(models.AppRevisionStatus_InstallSuccessful):
  212. appRevisionStatus = models.AppRevisionStatus_InstallSuccessful
  213. case string(models.AppRevisionStatus_InstallProgressing):
  214. appRevisionStatus = models.AppRevisionStatus_InstallProgressing
  215. case string(models.AppRevisionStatus_AwaitingInstall):
  216. appRevisionStatus = models.AppRevisionStatus_AwaitingInstall
  217. case string(models.AppRevisionStatus_BuildCanceled):
  218. appRevisionStatus = models.AppRevisionStatus_BuildCanceled
  219. case string(models.AppRevisionStatus_BuildFailed):
  220. appRevisionStatus = models.AppRevisionStatus_BuildFailed
  221. case string(models.AppRevisionStatus_PredeployFailed):
  222. appRevisionStatus = models.AppRevisionStatus_PredeployFailed
  223. case string(models.AppRevisionStatus_PredeploySuccessful):
  224. appRevisionStatus = models.AppRevisionStatus_PredeploySuccessful
  225. case string(models.AppRevisionStatus_PredeployProgressing):
  226. appRevisionStatus = models.AppRevisionStatus_PredeployProgressing
  227. case string(models.AppRevisionStatus_InstallFailed):
  228. appRevisionStatus = models.AppRevisionStatus_InstallFailed
  229. case string(models.AppRevisionStatus_Created):
  230. appRevisionStatus = models.AppRevisionStatus_Created
  231. case string(models.AppRevisionStatus_BuildSuccessful):
  232. appRevisionStatus = models.AppRevisionStatus_BuildSuccessful
  233. case string(models.AppRevisionStatus_ApplyFailed):
  234. appRevisionStatus = models.AppRevisionStatus_ApplyFailed
  235. case string(models.AppRevisionStatus_UpdateFailed):
  236. appRevisionStatus = models.AppRevisionStatus_UpdateFailed
  237. case string(models.AppRevisionStatus_DeploymentProgressing):
  238. appRevisionStatus = models.AppRevisionStatus_DeploymentProgressing
  239. case string(models.AppRevisionStatus_DeploymentSuccessful):
  240. appRevisionStatus = models.AppRevisionStatus_DeploymentSuccessful
  241. case string(models.AppRevisionStatus_DeploymentFailed):
  242. appRevisionStatus = models.AppRevisionStatus_DeploymentFailed
  243. case string(models.AppRevisionStatus_DeploymentSuperseded):
  244. appRevisionStatus = models.AppRevisionStatus_DeploymentSuperseded
  245. case string(models.AppRevisionStatus_RollbackSuccessful):
  246. appRevisionStatus = models.AppRevisionStatus_RollbackSuccessful
  247. case string(models.AppRevisionStatus_RollbackFailed):
  248. appRevisionStatus = models.AppRevisionStatus_RollbackFailed
  249. default:
  250. return appRevisionStatus, errors.New("unknown app revision status")
  251. }
  252. return appRevisionStatus, nil
  253. }