report_status.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. package porter_app
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "fmt"
  6. "net/http"
  7. "strings"
  8. "github.com/google/go-github/v39/github"
  9. "github.com/google/uuid"
  10. "github.com/porter-dev/api-contracts/generated/go/helpers"
  11. porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
  12. "github.com/porter-dev/porter/api/server/handlers"
  13. "github.com/porter-dev/porter/api/server/shared"
  14. "github.com/porter-dev/porter/api/server/shared/apierrors"
  15. "github.com/porter-dev/porter/api/server/shared/config"
  16. "github.com/porter-dev/porter/api/server/shared/requestutils"
  17. "github.com/porter-dev/porter/api/types"
  18. "github.com/porter-dev/porter/internal/deployment_target"
  19. "github.com/porter-dev/porter/internal/models"
  20. "github.com/porter-dev/porter/internal/porter_app"
  21. v2 "github.com/porter-dev/porter/internal/porter_app/v2"
  22. "github.com/porter-dev/porter/internal/telemetry"
  23. "k8s.io/utils/pointer"
  24. )
  25. // ReportRevisionStatusHandler is the handler for the /apps/{porter_app_name}/revisions/{app_revision_id}/status endpoint
  26. type ReportRevisionStatusHandler struct {
  27. handlers.PorterHandlerReadWriter
  28. }
  29. // NewReportRevisionStatusHandler handles POST requests to the endpoint /apps/{porter_app_name}/revisions/{app_revision_id}/status
  30. func NewReportRevisionStatusHandler(
  31. config *config.Config,
  32. decoderValidator shared.RequestDecoderValidator,
  33. writer shared.ResultWriter,
  34. ) *ReportRevisionStatusHandler {
  35. return &ReportRevisionStatusHandler{
  36. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  37. }
  38. }
  39. // ReportRevisionStatusRequest is the request object for the /apps/{porter_app_name}/revisions/{app_revision_id}/status endpoint
  40. type ReportRevisionStatusRequest struct {
  41. PRNumber int `json:"pr_number"`
  42. CommitSHA string `json:"commit_sha"`
  43. }
  44. // ReportRevisionStatusResponse is the response object for the /apps/{porter_app_name}/revisions/{app_revision_id}/status endpoint
  45. type ReportRevisionStatusResponse struct{}
  46. // ServeHTTP reports the status of a revision to Github and other integrations, depending on the status and the deployment target
  47. func (c *ReportRevisionStatusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  48. ctx, span := telemetry.NewSpan(r.Context(), "serve-report-revision-status")
  49. defer span.End()
  50. project, _ := ctx.Value(types.ProjectScope).(*models.Project)
  51. cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
  52. if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) {
  53. err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled")
  54. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
  55. return
  56. }
  57. appName, reqErr := requestutils.GetURLParamString(r, types.URLParamPorterAppName)
  58. if reqErr != nil {
  59. err := telemetry.Error(ctx, span, nil, "error parsing porter app name")
  60. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  61. return
  62. }
  63. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-app-name", Value: appName})
  64. revisionID, reqErr := requestutils.GetURLParamString(r, types.URLParamAppRevisionID)
  65. if reqErr != nil {
  66. err := telemetry.Error(ctx, span, nil, "error parsing app revision id")
  67. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  68. return
  69. }
  70. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-revision-id", Value: revisionID})
  71. appRevisionUuid, err := uuid.Parse(revisionID)
  72. if err != nil {
  73. err := telemetry.Error(ctx, span, err, "error parsing app revision id")
  74. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  75. return
  76. }
  77. if appRevisionUuid == uuid.Nil {
  78. err := telemetry.Error(ctx, span, nil, "app revision id is nil")
  79. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  80. return
  81. }
  82. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-revision-id", Value: appRevisionUuid.String()})
  83. porterApp, err := c.Repo().PorterApp().ReadPorterAppByName(cluster.ID, appName)
  84. if err != nil {
  85. err := telemetry.Error(ctx, span, err, "error reading porter app by name")
  86. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  87. return
  88. }
  89. if porterApp.ID == 0 {
  90. err := telemetry.Error(ctx, span, nil, "porter app not found")
  91. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusNotFound))
  92. return
  93. }
  94. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "porter-app-id", Value: porterApp.ID})
  95. request := &ReportRevisionStatusRequest{}
  96. if ok := c.DecodeAndValidate(w, r, request); !ok {
  97. err := telemetry.Error(ctx, span, nil, "error decoding request")
  98. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  99. return
  100. }
  101. revision, err := porter_app.GetAppRevision(ctx, porter_app.GetAppRevisionInput{
  102. AppRevisionID: appRevisionUuid,
  103. ProjectID: project.ID,
  104. CCPClient: c.Config().ClusterControlPlaneClient,
  105. })
  106. if err != nil {
  107. err := telemetry.Error(ctx, span, err, "error getting app revision")
  108. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  109. return
  110. }
  111. deploymentTarget, err := deployment_target.DeploymentTargetDetails(ctx, deployment_target.DeploymentTargetDetailsInput{
  112. ProjectID: int64(project.ID),
  113. ClusterID: int64(cluster.ID),
  114. DeploymentTargetID: revision.DeploymentTargetID,
  115. CCPClient: c.Config().ClusterControlPlaneClient,
  116. })
  117. if err != nil {
  118. err := telemetry.Error(ctx, span, err, "error getting deployment target details")
  119. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  120. return
  121. }
  122. telemetry.WithAttributes(span,
  123. telemetry.AttributeKV{Key: "deployment-target-id", Value: deploymentTarget.ID},
  124. telemetry.AttributeKV{Key: "pr-number", Value: request.PRNumber},
  125. telemetry.AttributeKV{Key: "commit-sha", Value: request.CommitSHA},
  126. telemetry.AttributeKV{Key: "preview", Value: deploymentTarget.Preview},
  127. telemetry.AttributeKV{Key: "revision-number", Value: revision.RevisionNumber},
  128. )
  129. resp := &ReportRevisionStatusResponse{}
  130. if !deploymentTarget.Preview || request.PRNumber == 0 || revision.RevisionNumber > 1 {
  131. c.WriteResult(w, r, resp)
  132. return
  133. }
  134. err = writePRComment(ctx, writePRCommentInput{
  135. revision: revision,
  136. porterApp: porterApp,
  137. prNumber: request.PRNumber,
  138. commitSha: request.CommitSHA,
  139. serverURL: c.Config().ServerConf.ServerURL,
  140. githubAppSecret: c.Config().ServerConf.GithubAppSecret,
  141. githubAppID: c.Config().ServerConf.GithubAppID,
  142. })
  143. if err != nil {
  144. err := telemetry.Error(ctx, span, err, "error writing pr comment")
  145. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  146. return
  147. }
  148. c.WriteResult(w, r, resp)
  149. }
  150. type writePRCommentInput struct {
  151. revision porter_app.Revision
  152. porterApp *models.PorterApp
  153. prNumber int
  154. commitSha string
  155. serverURL string
  156. githubAppSecret []byte
  157. githubAppID string
  158. }
  159. func writePRComment(ctx context.Context, inp writePRCommentInput) error {
  160. ctx, span := telemetry.NewSpan(ctx, "write-pr-comment")
  161. defer span.End()
  162. if inp.porterApp == nil {
  163. return telemetry.Error(ctx, span, nil, "porter app is nil")
  164. }
  165. if inp.prNumber == 0 {
  166. return telemetry.Error(ctx, span, nil, "pr number is empty")
  167. }
  168. if inp.commitSha == "" {
  169. return telemetry.Error(ctx, span, nil, "commit sha is empty")
  170. }
  171. if inp.githubAppSecret == nil {
  172. return telemetry.Error(ctx, span, nil, "github app secret is empty")
  173. }
  174. if inp.githubAppID == "" {
  175. return telemetry.Error(ctx, span, nil, "github app id is empty")
  176. }
  177. if inp.serverURL == "" {
  178. return telemetry.Error(ctx, span, nil, "server url is empty")
  179. }
  180. client, err := porter_app.GetGithubClientByRepoID(ctx, inp.porterApp.GitRepoID, inp.githubAppSecret, inp.githubAppID)
  181. if err != nil {
  182. return telemetry.Error(ctx, span, err, "error getting github client")
  183. }
  184. repoDetails := strings.Split(inp.porterApp.RepoName, "/")
  185. if len(repoDetails) != 2 {
  186. return telemetry.Error(ctx, span, nil, "repo name is not in the format <org>/<repo>")
  187. }
  188. telemetry.WithAttributes(span,
  189. telemetry.AttributeKV{Key: "repo-owner", Value: repoDetails[0]},
  190. telemetry.AttributeKV{Key: "repo-name", Value: repoDetails[1]},
  191. telemetry.AttributeKV{Key: "pr-number", Value: inp.prNumber},
  192. telemetry.AttributeKV{Key: "commit-sha", Value: inp.commitSha},
  193. )
  194. decoded, err := base64.StdEncoding.DecodeString(inp.revision.B64AppProto)
  195. if err != nil {
  196. return telemetry.Error(ctx, span, err, "error decoding base proto")
  197. }
  198. appProto := &porterv1.PorterApp{}
  199. err = helpers.UnmarshalContractObject(decoded, appProto)
  200. if err != nil {
  201. return telemetry.Error(ctx, span, err, "error unmarshalling app proto")
  202. }
  203. app, err := v2.AppFromProto(appProto)
  204. if err != nil {
  205. return telemetry.Error(ctx, span, err, "error converting app proto to app")
  206. }
  207. body := "## Porter Preview Environments\n"
  208. porterURL := fmt.Sprintf("%s/preview-environments/apps/%s?target=%s", inp.serverURL, inp.porterApp.Name, inp.revision.DeploymentTargetID)
  209. switch inp.revision.Status {
  210. case models.AppRevisionStatus_BuildFailed:
  211. body = fmt.Sprintf("%s❌ The latest deploy failed to build. Check the [Porter Dashboard](%s) or [action logs](https://github.com/%s/actions/runs/) for more information.", body, porterURL, inp.porterApp.RepoName)
  212. case models.AppRevisionStatus_DeployFailed:
  213. body = fmt.Sprintf("%s❌ The latest SHA ([`%s`](https://github.com/%s/%s/commit/%s)) failed to deploy.\nCheck the [Porter Dashboard](%s) or [action logs](https://github.com/%s/actions/runs/) for more information.\nContact Porter Support if the errors persists", body, inp.commitSha, repoDetails[0], repoDetails[1], inp.commitSha, porterURL, inp.porterApp.RepoName)
  214. case models.AppRevisionStatus_Deployed:
  215. body = fmt.Sprintf("%s✅ The latest SHA ([`%s`](https://github.com/%s/%s/commit/%s)) has been successfully deployed.\nApp details available in the [Porter Dashboard](%s)", body, inp.commitSha, repoDetails[0], repoDetails[1], inp.commitSha, porterURL)
  216. default:
  217. return nil
  218. }
  219. for _, service := range app.Services {
  220. if service.Domains != nil && len(service.Domains) > 0 {
  221. body = fmt.Sprintf("%s\n\n**Preview URL**: https://%s", body, service.Domains[0].Name)
  222. }
  223. }
  224. _, _, err = client.Issues.CreateComment(
  225. ctx,
  226. repoDetails[0],
  227. repoDetails[1],
  228. inp.prNumber,
  229. &github.IssueComment{
  230. Body: pointer.String(body),
  231. },
  232. )
  233. if err != nil {
  234. return telemetry.Error(ctx, span, err, "error creating github comment")
  235. }
  236. return nil
  237. }