create.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. package environment
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "strings"
  8. "github.com/google/go-github/v41/github"
  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/auth/token"
  16. "github.com/porter-dev/porter/internal/encryption"
  17. "github.com/porter-dev/porter/internal/integrations/ci/actions"
  18. "github.com/porter-dev/porter/internal/models"
  19. "github.com/porter-dev/porter/internal/models/integrations"
  20. )
  21. type CreateEnvironmentHandler struct {
  22. handlers.PorterHandlerReadWriter
  23. }
  24. func NewCreateEnvironmentHandler(
  25. config *config.Config,
  26. decoderValidator shared.RequestDecoderValidator,
  27. writer shared.ResultWriter,
  28. ) *CreateEnvironmentHandler {
  29. return &CreateEnvironmentHandler{
  30. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  31. }
  32. }
  33. func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  34. ga, _ := r.Context().Value(types.GitInstallationScope).(*integrations.GithubAppInstallation)
  35. user, _ := r.Context().Value(types.UserScope).(*models.User)
  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. // create the environment
  43. request := &types.CreateEnvironmentRequest{}
  44. if ok := c.DecodeAndValidate(w, r, request); !ok {
  45. return
  46. }
  47. // create a random webhook id
  48. webhookUID, err := encryption.GenerateRandomBytes(32)
  49. if err != nil {
  50. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error generating webhook UID for new preview "+
  51. "environment: %w", err)))
  52. return
  53. }
  54. env := &models.Environment{
  55. ProjectID: project.ID,
  56. ClusterID: cluster.ID,
  57. GitInstallationID: uint(ga.InstallationID),
  58. Name: request.Name,
  59. GitRepoOwner: owner,
  60. GitRepoName: name,
  61. GitRepoBranches: strings.Join(request.GitRepoBranches, ","),
  62. Mode: request.Mode,
  63. WebhookID: string(webhookUID),
  64. NewCommentsDisabled: request.DisableNewComments,
  65. GitDeployBranches: strings.Join(request.GitDeployBranches, ","),
  66. }
  67. if len(request.NamespaceLabels) > 0 {
  68. var labels []string
  69. for k, v := range request.NamespaceLabels {
  70. labels = append(labels, fmt.Sprintf("%s=%s", k, v))
  71. }
  72. env.NamespaceLabels = []byte(strings.Join(labels, ","))
  73. }
  74. // write Github actions files to the repo
  75. client, err := getGithubClientFromEnvironment(c.Config(), env)
  76. if err != nil {
  77. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  78. return
  79. }
  80. webhookURL := getGithubWebhookURLFromUID(c.Config().ServerConf.ServerURL, string(webhookUID))
  81. // create incoming webhook
  82. hook, _, err := client.Repositories.CreateHook(
  83. context.Background(), owner, name, &github.Hook{
  84. Config: map[string]interface{}{
  85. "url": webhookURL,
  86. "content_type": "json",
  87. "secret": c.Config().ServerConf.GithubIncomingWebhookSecret,
  88. },
  89. Events: []string{"pull_request", "push"},
  90. Active: github.Bool(true),
  91. },
  92. )
  93. if err != nil && !strings.Contains(err.Error(), "already exists") {
  94. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, err),
  95. http.StatusConflict))
  96. return
  97. }
  98. env.GithubWebhookID = hook.GetID()
  99. env, err = c.Repo().Environment().CreateEnvironment(env)
  100. if err != nil {
  101. _, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
  102. if deleteErr != nil {
  103. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
  104. http.StatusConflict, "error creating environment"))
  105. return
  106. }
  107. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating environment: %w", err)))
  108. return
  109. }
  110. // generate porter jwt token
  111. jwt, err := token.GetTokenForAPI(user.ID, project.ID)
  112. if err != nil {
  113. _, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
  114. if deleteErr != nil {
  115. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
  116. http.StatusConflict, "error getting token for API while creating environment"))
  117. return
  118. }
  119. _, deleteErr = c.Repo().Environment().DeleteEnvironment(env)
  120. if deleteErr != nil {
  121. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deleting created preview environment: %w",
  122. deleteErr)))
  123. return
  124. }
  125. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting token for API: %w", err)))
  126. return
  127. }
  128. encoded, err := jwt.EncodeToken(c.Config().TokenConf)
  129. if err != nil {
  130. _, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
  131. if deleteErr != nil {
  132. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
  133. http.StatusConflict, "error encoding token while creating environment"))
  134. return
  135. }
  136. _, deleteErr = c.Repo().Environment().DeleteEnvironment(env)
  137. if deleteErr != nil {
  138. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deleting created preview environment: %w",
  139. deleteErr)))
  140. return
  141. }
  142. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error encoding API token: %w", err)))
  143. return
  144. }
  145. err = actions.SetupEnv(&actions.EnvOpts{
  146. Client: client,
  147. ServerURL: c.Config().ServerConf.ServerURL,
  148. PorterToken: encoded,
  149. GitRepoOwner: owner,
  150. GitRepoName: name,
  151. ProjectID: project.ID,
  152. ClusterID: cluster.ID,
  153. GitInstallationID: uint(ga.InstallationID),
  154. EnvironmentName: request.Name,
  155. InstanceName: c.Config().ServerConf.InstanceName,
  156. })
  157. if err != nil {
  158. unwrappedErr := errors.Unwrap(err)
  159. if unwrappedErr != nil {
  160. if errors.Is(unwrappedErr, actions.ErrProtectedBranch) {
  161. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
  162. } else if errors.Is(unwrappedErr, actions.ErrCreatePRForProtectedBranch) {
  163. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusPreconditionFailed))
  164. }
  165. } else {
  166. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error setting up preview environment in the github "+
  167. "repo: %w", err)))
  168. return
  169. }
  170. }
  171. c.WriteResult(w, r, env.ToEnvironmentType())
  172. }
  173. func getGithubWebhookURLFromUID(serverURL, webhookUID string) string {
  174. return fmt.Sprintf("%s/api/github/incoming_webhook/%s", serverURL, string(webhookUID))
  175. }