create_deployment.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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/authz"
  9. "github.com/porter-dev/porter/api/server/handlers"
  10. "github.com/porter-dev/porter/api/server/shared"
  11. "github.com/porter-dev/porter/api/server/shared/apierrors"
  12. "github.com/porter-dev/porter/api/server/shared/commonutils"
  13. "github.com/porter-dev/porter/api/server/shared/config"
  14. "github.com/porter-dev/porter/api/types"
  15. "github.com/porter-dev/porter/internal/models"
  16. "github.com/porter-dev/porter/internal/models/integrations"
  17. "gorm.io/gorm"
  18. )
  19. var errGithubAPI = errors.New("error communicating with the github API")
  20. type CreateDeploymentHandler struct {
  21. handlers.PorterHandlerReadWriter
  22. authz.KubernetesAgentGetter
  23. }
  24. func NewCreateDeploymentHandler(
  25. config *config.Config,
  26. decoderValidator shared.RequestDecoderValidator,
  27. writer shared.ResultWriter,
  28. ) *CreateDeploymentHandler {
  29. return &CreateDeploymentHandler{
  30. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  31. KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
  32. }
  33. }
  34. func (c *CreateDeploymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  35. ga, _ := r.Context().Value(types.GitInstallationScope).(*integrations.GithubAppInstallation)
  36. project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
  37. cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
  38. owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
  39. if !ok {
  40. return
  41. }
  42. request := &types.CreateDeploymentRequest{}
  43. if ok := c.DecodeAndValidate(w, r, request); !ok {
  44. return
  45. }
  46. // read the environment to get the environment id
  47. env, err := c.Repo().Environment().ReadEnvironment(project.ID, cluster.ID, uint(ga.InstallationID), owner, name)
  48. if err != nil {
  49. if errors.Is(err, gorm.ErrRecordNotFound) {
  50. c.HandleAPIError(w, r, apierrors.NewErrNotFound(
  51. fmt.Errorf("error creating deployment: no environment found")),
  52. )
  53. return
  54. }
  55. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  56. return
  57. }
  58. // create deployment on GitHub API
  59. client, err := getGithubClientFromEnvironment(c.Config(), env)
  60. if err != nil {
  61. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  62. return
  63. }
  64. // add a check for Github PR status
  65. prClosed, err := isGithubPRClosed(client, owner, name, int(request.PullRequestID))
  66. if err != nil {
  67. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
  68. return
  69. }
  70. if prClosed {
  71. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  72. fmt.Errorf("cannot create deployment for closed github PR"), http.StatusConflict,
  73. ))
  74. return
  75. }
  76. ghDeployment, err := createGithubDeployment(client, env, request.PRBranchFrom, request.ActionID)
  77. if err != nil {
  78. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
  79. return
  80. }
  81. // create the deployment
  82. depl, err := c.Repo().Environment().CreateDeployment(&models.Deployment{
  83. EnvironmentID: env.ID,
  84. Namespace: request.Namespace,
  85. Status: types.DeploymentStatusCreating,
  86. PullRequestID: request.PullRequestID,
  87. GHDeploymentID: ghDeployment.GetID(),
  88. RepoOwner: request.GitHubMetadata.RepoOwner,
  89. RepoName: request.GitHubMetadata.RepoName,
  90. PRName: request.GitHubMetadata.PRName,
  91. CommitSHA: request.GitHubMetadata.CommitSHA,
  92. PRBranchFrom: request.GitHubMetadata.PRBranchFrom,
  93. PRBranchInto: request.GitHubMetadata.PRBranchInto,
  94. })
  95. if err != nil {
  96. // try to delete the GitHub deployment
  97. _, err = client.Repositories.DeleteDeployment(
  98. context.Background(),
  99. env.GitRepoOwner,
  100. env.GitRepoName,
  101. ghDeployment.GetID(),
  102. )
  103. if err != nil {
  104. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, err),
  105. http.StatusConflict))
  106. return
  107. }
  108. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  109. return
  110. }
  111. // create the backing namespace
  112. agent, err := c.GetAgent(r, cluster, "")
  113. if err != nil {
  114. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  115. return
  116. }
  117. _, err = agent.CreateNamespace(depl.Namespace)
  118. if err != nil {
  119. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  120. return
  121. }
  122. c.WriteResult(w, r, depl.ToDeploymentType())
  123. }
  124. func createGithubDeployment(
  125. client *github.Client,
  126. env *models.Environment,
  127. branchFrom string,
  128. actionID uint,
  129. ) (*github.Deployment, error) {
  130. requiredContexts := []string{}
  131. deployment, _, err := client.Repositories.CreateDeployment(
  132. context.Background(),
  133. env.GitRepoOwner,
  134. env.GitRepoName,
  135. &github.DeploymentRequest{
  136. Ref: github.String(branchFrom),
  137. Environment: github.String(env.Name),
  138. AutoMerge: github.Bool(false),
  139. RequiredContexts: &requiredContexts,
  140. },
  141. )
  142. if err != nil {
  143. return nil, fmt.Errorf("%v: %w", errGithubAPI, err)
  144. }
  145. depID := deployment.GetID()
  146. // Create Deployment Status to indicate it's in progress
  147. _, _, err = client.Repositories.CreateDeploymentStatus(
  148. context.Background(),
  149. env.GitRepoOwner,
  150. env.GitRepoName,
  151. depID,
  152. &github.DeploymentStatusRequest{
  153. State: github.String("in_progress"),
  154. LogURL: github.String(fmt.Sprintf("https://github.com/%s/%s/actions/runs/%d",
  155. env.GitRepoOwner, env.GitRepoName, actionID)), // link to actions tab
  156. },
  157. )
  158. if err != nil {
  159. return nil, fmt.Errorf("%v: %w", errGithubAPI, err)
  160. }
  161. return deployment, nil
  162. }