finalize_deployment_with_errors.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. package environment
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net/http"
  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. "gorm.io/gorm"
  17. )
  18. type FinalizeDeploymentWithErrorsHandler struct {
  19. handlers.PorterHandlerReadWriter
  20. }
  21. func NewFinalizeDeploymentWithErrorsHandler(
  22. config *config.Config,
  23. decoderValidator shared.RequestDecoderValidator,
  24. writer shared.ResultWriter,
  25. ) *FinalizeDeploymentWithErrorsHandler {
  26. return &FinalizeDeploymentWithErrorsHandler{
  27. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  28. }
  29. }
  30. func (c *FinalizeDeploymentWithErrorsHandler) 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. owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
  35. if !ok {
  36. return
  37. }
  38. request := &types.FinalizeDeploymentWithErrorsRequest{}
  39. if ok := c.DecodeAndValidate(w, r, request); !ok {
  40. return
  41. }
  42. if request.Namespace == "" && request.PRNumber == 0 {
  43. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  44. fmt.Errorf("either namespace or pr_number must be present in request body"), http.StatusBadRequest,
  45. ))
  46. return
  47. }
  48. if len(request.Errors) == 0 {
  49. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  50. fmt.Errorf("at least one error is required to report"), http.StatusPreconditionFailed,
  51. ))
  52. return
  53. }
  54. var err error
  55. // read the environment to get the environment id
  56. env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
  57. if err != nil {
  58. if errors.Is(err, gorm.ErrRecordNotFound) {
  59. c.HandleAPIError(w, r, apierrors.NewErrNotFound(errEnvironmentNotFound))
  60. return
  61. }
  62. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  63. return
  64. }
  65. var depl *models.Deployment
  66. // read the deployment
  67. if request.PRNumber != 0 {
  68. depl, err = c.Repo().Environment().ReadDeploymentByGitDetails(env.ID, owner, name, request.PRNumber)
  69. if err != nil {
  70. if errors.Is(err, gorm.ErrRecordNotFound) {
  71. c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
  72. return
  73. }
  74. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  75. return
  76. }
  77. } else if request.Namespace != "" {
  78. depl, err = c.Repo().Environment().ReadDeployment(env.ID, request.Namespace)
  79. if err != nil {
  80. if errors.Is(err, gorm.ErrRecordNotFound) {
  81. c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
  82. return
  83. }
  84. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  85. return
  86. }
  87. }
  88. if depl == nil {
  89. c.HandleAPIError(w, r, apierrors.NewErrNotFound(errDeploymentNotFound))
  90. return
  91. }
  92. client, err := getGithubClientFromEnvironment(c.Config(), env)
  93. if err != nil {
  94. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  95. return
  96. }
  97. depl.Status = types.DeploymentStatusFailed
  98. // we do not care of the error in this case because the list deployments endpoint
  99. // talks to the github API to fetch the deployment status correctly
  100. c.Repo().Environment().UpdateDeployment(depl)
  101. // FIXME: ignore the status of this API call for now
  102. client.Repositories.CreateDeploymentStatus(
  103. context.Background(), owner, name, depl.GHDeploymentID, &github.DeploymentStatusRequest{
  104. State: github.String("failure"),
  105. Description: github.String("one or more resources failed to build"),
  106. },
  107. )
  108. if !depl.IsBranchDeploy() {
  109. // add a check for the PR to be open before creating a comment
  110. prClosed, err := isGithubPRClosed(client, owner, name, int(depl.PullRequestID))
  111. if err != nil {
  112. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
  113. return
  114. }
  115. if prClosed {
  116. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("github PR has been closed"),
  117. http.StatusConflict))
  118. return
  119. }
  120. workflowRun, err := commonutils.GetLatestWorkflowRun(client, depl.RepoOwner, depl.RepoName,
  121. fmt.Sprintf("porter_%s_env.yml", env.Name), depl.PRBranchFrom)
  122. if err != nil {
  123. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  124. return
  125. }
  126. commentBody := fmt.Sprintf(
  127. "## Porter Preview Environments\n"+
  128. "❌ Errors encountered while deploying the changes\n"+
  129. "||Deployment Information|\n"+
  130. "|-|-|\n"+
  131. "| Latest SHA | [`%s`](https://github.com/%s/%s/commit/%s) |\n"+
  132. "| Build Logs | %s |\n",
  133. depl.CommitSHA, depl.RepoOwner, depl.RepoName, depl.CommitSHA, workflowRun.GetHTMLURL(),
  134. )
  135. if len(request.SuccessfulResources) > 0 {
  136. commentBody += "#### Successfully deployed resources\n"
  137. for _, res := range request.SuccessfulResources {
  138. if res.ReleaseType == "job" {
  139. commentBody += fmt.Sprintf("- [`%s`](%s/jobs/%s/%s/%s?project_id=%d)\n",
  140. res.ReleaseName, c.Config().ServerConf.ServerURL, cluster.Name, depl.Namespace,
  141. res.ReleaseName, project.ID)
  142. } else {
  143. commentBody += fmt.Sprintf("- [`%s`](%s/applications/%s/%s/%s?project_id=%d)\n",
  144. res.ReleaseName, c.Config().ServerConf.ServerURL, cluster.Name, depl.Namespace,
  145. res.ReleaseName, project.ID)
  146. }
  147. }
  148. }
  149. commentBody += "#### Failed resources\n"
  150. for res, err := range request.Errors {
  151. commentBody += fmt.Sprintf("<details>\n <summary><code>%s</code></summary>\n\n **Error:** %s\n</details>\n", res, err)
  152. }
  153. err = createOrUpdateComment(client, c.Repo(), env.NewCommentsDisabled, depl, github.String(commentBody))
  154. if err != nil {
  155. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  156. return
  157. }
  158. }
  159. c.WriteResult(w, r, depl.ToDeploymentType())
  160. }