revisions.go 12 KB

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