revisions.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. package porter_app
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "fmt"
  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. DeploymentTargetID string `json:"deployment_target_id"`
  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. // GetAppRevisionInput is the input struct for GetAppRevisions
  41. type GetAppRevisionInput struct {
  42. ProjectID uint
  43. AppRevisionID uuid.UUID
  44. CCPClient porterv1connect.ClusterControlPlaneServiceClient
  45. }
  46. // GetAppRevision returns a single app revision by id
  47. func GetAppRevision(ctx context.Context, inp GetAppRevisionInput) (Revision, error) {
  48. ctx, span := telemetry.NewSpan(ctx, "get-app-revision")
  49. defer span.End()
  50. var revision Revision
  51. if inp.ProjectID == 0 {
  52. return revision, telemetry.Error(ctx, span, nil, "must provide a project id")
  53. }
  54. if inp.AppRevisionID == uuid.Nil {
  55. return revision, telemetry.Error(ctx, span, nil, "must provide an app revision id")
  56. }
  57. getRevisionReq := connect.NewRequest(&porterv1.GetAppRevisionRequest{
  58. ProjectId: int64(inp.ProjectID),
  59. AppRevisionId: inp.AppRevisionID.String(),
  60. })
  61. ccpResp, err := inp.CCPClient.GetAppRevision(ctx, getRevisionReq)
  62. if err != nil {
  63. return revision, telemetry.Error(ctx, span, err, "error getting app revision")
  64. }
  65. if ccpResp == nil || ccpResp.Msg == nil {
  66. return revision, telemetry.Error(ctx, span, nil, "get app revision response is nil")
  67. }
  68. appRevisionProto := ccpResp.Msg.AppRevision
  69. revision, err = EncodedRevisionFromProto(ctx, appRevisionProto)
  70. if err != nil {
  71. return revision, telemetry.Error(ctx, span, err, "error converting app revision from proto")
  72. }
  73. return revision, nil
  74. }
  75. // EncodedRevisionFromProto converts an AppRevision proto object into a Revision object
  76. func EncodedRevisionFromProto(ctx context.Context, appRevision *porterv1.AppRevision) (Revision, error) {
  77. ctx, span := telemetry.NewSpan(ctx, "encoded-revision-from-proto")
  78. defer span.End()
  79. var revision Revision
  80. if appRevision == nil {
  81. return revision, telemetry.Error(ctx, span, nil, "current app revision definition is nil")
  82. }
  83. appProto := appRevision.App
  84. if appProto == nil {
  85. return revision, telemetry.Error(ctx, span, nil, "app proto is nil")
  86. }
  87. encoded, err := helpers.MarshalContractObject(ctx, appProto)
  88. if err != nil {
  89. return revision, telemetry.Error(ctx, span, err, "error marshalling app proto back to json")
  90. }
  91. b64 := base64.StdEncoding.EncodeToString(encoded)
  92. status, err := appRevisionStatusFromProto(appRevision.Status)
  93. if err != nil {
  94. return revision, telemetry.Error(ctx, span, err, "error getting app revision status from proto")
  95. }
  96. appInstanceIdStr := appRevision.AppInstanceId
  97. appInstanceId, err := uuid.Parse(appInstanceIdStr)
  98. if err != nil {
  99. return revision, telemetry.Error(ctx, span, err, "error parsing app instance id")
  100. }
  101. revision = Revision{
  102. B64AppProto: b64,
  103. Status: status,
  104. ID: appRevision.Id,
  105. RevisionNumber: appRevision.RevisionNumber,
  106. CreatedAt: appRevision.CreatedAt.AsTime(),
  107. UpdatedAt: appRevision.UpdatedAt.AsTime(),
  108. DeploymentTargetID: appRevision.DeploymentTargetId,
  109. AppInstanceID: appInstanceId,
  110. }
  111. return revision, nil
  112. }
  113. // AttachEnvToRevisionInput is the input struct for AttachEnvToRevision
  114. type AttachEnvToRevisionInput struct {
  115. ProjectID uint
  116. ClusterID int
  117. Revision Revision
  118. DeploymentTarget deployment_target.DeploymentTarget
  119. K8SAgent *kubernetes.Agent
  120. PorterAppRepository repository.PorterAppRepository
  121. }
  122. // AttachEnvToRevision attaches the environment variables from the app's default env group to a revision
  123. // These are the variables that are displayed to the user in the UI as associated with the app rather than an env group
  124. func AttachEnvToRevision(ctx context.Context, inp AttachEnvToRevisionInput) (Revision, error) {
  125. ctx, span := telemetry.NewSpan(ctx, "attach-env-to-revision")
  126. defer span.End()
  127. revision := inp.Revision
  128. if inp.ProjectID == 0 {
  129. return revision, telemetry.Error(ctx, span, nil, "must provide a project id")
  130. }
  131. if inp.ClusterID == 0 {
  132. return revision, telemetry.Error(ctx, span, nil, "must provide a cluster id")
  133. }
  134. if inp.K8SAgent == nil {
  135. return revision, telemetry.Error(ctx, span, nil, "k8s agent is nil")
  136. }
  137. decoded, err := base64.StdEncoding.DecodeString(revision.B64AppProto)
  138. if err != nil {
  139. return revision, telemetry.Error(ctx, span, err, "error decoding app proto")
  140. }
  141. appDef := &porterv1.PorterApp{}
  142. err = helpers.UnmarshalContractObject(decoded, appDef)
  143. if err != nil {
  144. return revision, telemetry.Error(ctx, span, err, "error unmarshalling app proto")
  145. }
  146. envName, err := AppEnvGroupName(ctx, appDef.Name, inp.Revision.DeploymentTargetID, uint(inp.ClusterID), inp.PorterAppRepository)
  147. if err != nil {
  148. return revision, telemetry.Error(ctx, span, err, "error getting app env group name")
  149. }
  150. envNameFilter := []string{envName}
  151. envFromProtoInp := AppEnvironmentFromProtoInput{
  152. ProjectID: inp.ProjectID,
  153. ClusterID: inp.ClusterID,
  154. App: appDef,
  155. K8SAgent: inp.K8SAgent,
  156. DeploymentTarget: inp.DeploymentTarget,
  157. }
  158. envGroups, err := AppEnvironmentFromProto(ctx, envFromProtoInp, WithEnvGroupFilter(envNameFilter), WithSecrets())
  159. if err != nil {
  160. return revision, telemetry.Error(ctx, span, err, "error getting app environment from revision")
  161. }
  162. if len(envGroups) > 1 {
  163. return revision, telemetry.Error(ctx, span, err, "multiple app envs groups returned for same name")
  164. }
  165. if len(envGroups) == 1 {
  166. revision.Env = envGroups[0]
  167. }
  168. return revision, nil
  169. }
  170. func appRevisionStatusFromProto(status string) (models.AppRevisionStatus, error) {
  171. var appRevisionStatus models.AppRevisionStatus
  172. switch status {
  173. case string(models.AppRevisionStatus_ImageAvailable):
  174. appRevisionStatus = models.AppRevisionStatus_ImageAvailable
  175. case string(models.AppRevisionStatus_AwaitingBuild):
  176. appRevisionStatus = models.AppRevisionStatus_AwaitingBuild
  177. case string(models.AppRevisionStatus_AwaitingPredeploy):
  178. appRevisionStatus = models.AppRevisionStatus_AwaitingPredeploy
  179. case string(models.AppRevisionStatus_Deployed):
  180. appRevisionStatus = models.AppRevisionStatus_Deployed
  181. case string(models.AppRevisionStatus_Deploying):
  182. appRevisionStatus = models.AppRevisionStatus_Deploying
  183. case string(models.AppRevisionStatus_AwaitingDeploy):
  184. appRevisionStatus = models.AppRevisionStatus_AwaitingDeploy
  185. case string(models.AppRevisionStatus_BuildCanceled):
  186. appRevisionStatus = models.AppRevisionStatus_BuildCanceled
  187. case string(models.AppRevisionStatus_BuildFailed):
  188. appRevisionStatus = models.AppRevisionStatus_BuildFailed
  189. case string(models.AppRevisionStatus_PredeployFailed):
  190. appRevisionStatus = models.AppRevisionStatus_PredeployFailed
  191. case string(models.AppRevisionStatus_PredeploySuccessful):
  192. appRevisionStatus = models.AppRevisionStatus_PredeploySuccessful
  193. case string(models.AppRevisionStatus_PredeployProgressing):
  194. appRevisionStatus = models.AppRevisionStatus_PredeployProgressing
  195. case string(models.AppRevisionStatus_DeployFailed):
  196. appRevisionStatus = models.AppRevisionStatus_DeployFailed
  197. case string(models.AppRevisionStatus_Created):
  198. appRevisionStatus = models.AppRevisionStatus_Created
  199. case string(models.AppRevisionStatus_BuildSuccessful):
  200. appRevisionStatus = models.AppRevisionStatus_BuildSuccessful
  201. case string(models.AppRevisionStatus_ApplyFailed):
  202. appRevisionStatus = models.AppRevisionStatus_ApplyFailed
  203. default:
  204. return appRevisionStatus, fmt.Errorf("unknown app revision status")
  205. }
  206. return appRevisionStatus, nil
  207. }