finalize_deployment.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. package environment
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "strings"
  7. "github.com/google/go-github/v41/github"
  8. "github.com/porter-dev/porter/api/server/handlers"
  9. "github.com/porter-dev/porter/api/server/shared"
  10. "github.com/porter-dev/porter/api/server/shared/apierrors"
  11. "github.com/porter-dev/porter/api/server/shared/commonutils"
  12. "github.com/porter-dev/porter/api/server/shared/config"
  13. "github.com/porter-dev/porter/api/types"
  14. "github.com/porter-dev/porter/internal/models"
  15. "github.com/porter-dev/porter/internal/models/integrations"
  16. "github.com/porter-dev/porter/internal/repository"
  17. )
  18. type FinalizeDeploymentHandler struct {
  19. handlers.PorterHandlerReadWriter
  20. }
  21. func NewFinalizeDeploymentHandler(
  22. config *config.Config,
  23. decoderValidator shared.RequestDecoderValidator,
  24. writer shared.ResultWriter,
  25. ) *FinalizeDeploymentHandler {
  26. return &FinalizeDeploymentHandler{
  27. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  28. }
  29. }
  30. func (c *FinalizeDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  31. ga, _ := r.Context().Value(types.GitInstallationScope).(*integrations.GithubAppInstallation)
  32. project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
  33. cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
  34. if !project.PreviewEnvsEnabled {
  35. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewProjectDisabled, http.StatusForbidden))
  36. return
  37. } else if !cluster.PreviewEnvsEnabled {
  38. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errPreviewClusterDisabled, http.StatusForbidden))
  39. return
  40. }
  41. owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
  42. if !ok {
  43. return
  44. }
  45. request := &types.FinalizeDeploymentRequest{}
  46. if ok := c.DecodeAndValidate(w, r, request); !ok {
  47. return
  48. }
  49. // read the environment to get the environment id
  50. env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
  51. if err != nil {
  52. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  53. return
  54. }
  55. // read the deployment
  56. depl, err := c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
  57. if err != nil {
  58. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  59. return
  60. }
  61. depl.Subdomain = request.Subdomain
  62. depl.Status = types.DeploymentStatusCreated
  63. // update the deployment
  64. depl, err = c.Repo().Environment().UpdateDeployment(depl)
  65. if err != nil {
  66. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  67. return
  68. }
  69. client, err := getGithubClientFromEnvironment(c.Config(), env)
  70. if err != nil {
  71. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  72. return
  73. }
  74. // Create new deployment status to indicate deployment is ready
  75. state := "success"
  76. env_url := depl.Subdomain
  77. deploymentStatusRequest := github.DeploymentStatusRequest{
  78. State: &state,
  79. EnvironmentURL: &env_url,
  80. }
  81. _, _, err = client.Repositories.CreateDeploymentStatus(
  82. context.Background(),
  83. env.GitRepoOwner,
  84. env.GitRepoName,
  85. depl.GHDeploymentID,
  86. &deploymentStatusRequest,
  87. )
  88. if err != nil {
  89. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  90. return
  91. }
  92. // add a check for the PR to be open before creating a comment
  93. prClosed, err := isGithubPRClosed(client, owner, name, int(depl.PullRequestID))
  94. if err != nil {
  95. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  96. fmt.Errorf("error fetching details of github PR for deployment ID: %d. Error: %w",
  97. depl.ID, err), http.StatusConflict,
  98. ))
  99. return
  100. }
  101. if prClosed {
  102. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("Github PR has been closed"),
  103. http.StatusConflict))
  104. return
  105. }
  106. commentBody := "## Porter Preview Environments\n"
  107. if depl.Subdomain == "" {
  108. commentBody += fmt.Sprintf(
  109. "✅ The latest SHA ([`%s`](https://github.com/%s/%s/commit/%s)) has been successfully deployed.",
  110. depl.CommitSHA, depl.RepoOwner, depl.RepoName, depl.CommitSHA,
  111. )
  112. } else {
  113. commentBody += fmt.Sprintf(
  114. "✅ The latest SHA ([`%s`](https://github.com/%s/%s/commit/%s)) has been successfully deployed to %s",
  115. depl.CommitSHA, depl.RepoOwner, depl.RepoName, depl.CommitSHA, depl.Subdomain,
  116. )
  117. }
  118. err = createOrUpdateComment(client, c.Repo(), env.NewCommentsDisabled, depl, github.String(commentBody))
  119. if err != nil {
  120. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  121. return
  122. }
  123. c.WriteResult(w, r, depl.ToDeploymentType())
  124. }
  125. func createOrUpdateComment(
  126. client *github.Client,
  127. repo repository.Repository,
  128. newCommentsDisabled bool,
  129. depl *models.Deployment,
  130. commentBody *string,
  131. ) error {
  132. // when updating a PR comment, we have to handle several cases:
  133. // 1. when a Porter environment has deployment status repeat-comments enabled
  134. // - nothing special here, simply create a new comment in the PR
  135. // 2. when a Porter environment has deployment status repeat-comments disabled
  136. // - when a Porter deployment has Github comment ID saved in the DB
  137. // - try to update the comment using the Github comment ID
  138. // - if the above fails, try creating a new comment and save the new comment ID in the DB
  139. // - when a Porter deployment does not have a Github comment ID saved in the DB
  140. // - create a new comment and save the Github comment ID in the DB
  141. if newCommentsDisabled {
  142. if depl.GHPRCommentID == 0 {
  143. // create a new comment
  144. err := createGithubComment(client, repo, depl, commentBody)
  145. if err != nil {
  146. return err
  147. }
  148. } else {
  149. err := updateGithubComment(
  150. client, depl.RepoOwner, depl.RepoName, depl.GHPRCommentID, commentBody,
  151. )
  152. if err != nil {
  153. if strings.Contains(err.Error(), "404") {
  154. // perhaps a deleted comment?
  155. // create a new comment
  156. err := createGithubComment(client, repo, depl, commentBody)
  157. if err != nil {
  158. return fmt.Errorf("invalid github comment ID for deployment with ID: %d. Error creating "+
  159. "new comment: %w", depl.ID, err)
  160. }
  161. }
  162. return err
  163. }
  164. }
  165. } else {
  166. err := createGithubComment(client, repo, depl, commentBody)
  167. if err != nil {
  168. return err
  169. }
  170. }
  171. return nil
  172. }
  173. func createGithubComment(
  174. client *github.Client,
  175. repo repository.Repository,
  176. depl *models.Deployment,
  177. body *string,
  178. ) error {
  179. ghResp, _, err := client.Issues.CreateComment(
  180. context.Background(),
  181. depl.RepoOwner,
  182. depl.RepoName,
  183. int(depl.PullRequestID),
  184. &github.IssueComment{
  185. Body: body,
  186. },
  187. )
  188. if err != nil {
  189. return fmt.Errorf("error creating new github comment for owner: %s repo %s prNumber: %d. Error: %w",
  190. depl.RepoOwner, depl.RepoName, depl.PullRequestID, err)
  191. }
  192. depl.GHPRCommentID = ghResp.GetID()
  193. _, err = repo.Environment().UpdateDeployment(depl)
  194. if err != nil {
  195. return fmt.Errorf("error updating deployment with ID: %d. Error: %w", depl.ID, err)
  196. }
  197. return nil
  198. }
  199. func updateGithubComment(
  200. client *github.Client,
  201. owner, repo string,
  202. commentID int64,
  203. body *string,
  204. ) error {
  205. _, _, err := client.Issues.EditComment(
  206. context.Background(),
  207. owner,
  208. repo,
  209. commentID,
  210. &github.IssueComment{
  211. Body: body,
  212. },
  213. )
  214. return err
  215. }