create.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. package environment
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "strings"
  8. "sync"
  9. "github.com/google/go-github/v41/github"
  10. "github.com/porter-dev/porter/api/server/handlers"
  11. "github.com/porter-dev/porter/api/server/shared"
  12. "github.com/porter-dev/porter/api/server/shared/apierrors"
  13. "github.com/porter-dev/porter/api/server/shared/commonutils"
  14. "github.com/porter-dev/porter/api/server/shared/config"
  15. "github.com/porter-dev/porter/api/types"
  16. "github.com/porter-dev/porter/internal/auth/token"
  17. "github.com/porter-dev/porter/internal/encryption"
  18. "github.com/porter-dev/porter/internal/integrations/ci/actions"
  19. "github.com/porter-dev/porter/internal/models"
  20. "github.com/porter-dev/porter/internal/models/integrations"
  21. "gorm.io/gorm"
  22. )
  23. type CreateEnvironmentHandler struct {
  24. handlers.PorterHandlerReadWriter
  25. }
  26. func NewCreateEnvironmentHandler(
  27. config *config.Config,
  28. decoderValidator shared.RequestDecoderValidator,
  29. writer shared.ResultWriter,
  30. ) *CreateEnvironmentHandler {
  31. return &CreateEnvironmentHandler{
  32. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  33. }
  34. }
  35. func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  36. ga, _ := r.Context().Value(types.GitInstallationScope).(*integrations.GithubAppInstallation)
  37. user, _ := r.Context().Value(types.UserScope).(*models.User)
  38. project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
  39. cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
  40. owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)
  41. if !ok {
  42. return
  43. }
  44. // create the environment
  45. request := &types.CreateEnvironmentRequest{}
  46. if ok := c.DecodeAndValidate(w, r, request); !ok {
  47. return
  48. }
  49. // create a random webhook id
  50. webhookUID, err := encryption.GenerateRandomBytes(32)
  51. if err != nil {
  52. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error generating webhook UID for new preview "+
  53. "environment: %w", err)))
  54. return
  55. }
  56. env := &models.Environment{
  57. ProjectID: project.ID,
  58. ClusterID: cluster.ID,
  59. GitInstallationID: uint(ga.InstallationID),
  60. Name: request.Name,
  61. GitRepoOwner: owner,
  62. GitRepoName: name,
  63. GitRepoBranches: strings.Join(request.GitRepoBranches, ","),
  64. Mode: request.Mode,
  65. WebhookID: string(webhookUID),
  66. NewCommentsDisabled: request.DisableNewComments,
  67. GitDeployBranches: strings.Join(request.GitDeployBranches, ","),
  68. }
  69. if len(request.NamespaceLabels) > 0 {
  70. var labels []string
  71. for k, v := range request.NamespaceLabels {
  72. labels = append(labels, fmt.Sprintf("%s=%s", k, v))
  73. }
  74. env.NamespaceLabels = []byte(strings.Join(labels, ","))
  75. }
  76. // write Github actions files to the repo
  77. client, err := getGithubClientFromEnvironment(c.Config(), env)
  78. if err != nil {
  79. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  80. return
  81. }
  82. webhookURL := getGithubWebhookURLFromUID(c.Config().ServerConf.ServerURL, string(webhookUID))
  83. // create incoming webhook
  84. hook, _, err := client.Repositories.CreateHook(
  85. context.Background(), owner, name, &github.Hook{
  86. Config: map[string]interface{}{
  87. "url": webhookURL,
  88. "content_type": "json",
  89. "secret": c.Config().ServerConf.GithubIncomingWebhookSecret,
  90. },
  91. Events: []string{"pull_request", "push"},
  92. Active: github.Bool(true),
  93. },
  94. )
  95. if err != nil && !strings.Contains(err.Error(), "already exists") {
  96. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, err),
  97. http.StatusConflict))
  98. return
  99. }
  100. env.GithubWebhookID = hook.GetID()
  101. env, err = c.Repo().Environment().CreateEnvironment(env)
  102. if err != nil {
  103. _, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
  104. if deleteErr != nil {
  105. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
  106. http.StatusConflict, "error creating environment"))
  107. return
  108. }
  109. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating environment: %w", err)))
  110. return
  111. }
  112. // generate porter jwt token
  113. jwt, err := token.GetTokenForAPI(user.ID, project.ID)
  114. if err != nil {
  115. _, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
  116. if deleteErr != nil {
  117. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
  118. http.StatusConflict, "error getting token for API while creating environment"))
  119. return
  120. }
  121. _, deleteErr = c.Repo().Environment().DeleteEnvironment(env)
  122. if deleteErr != nil {
  123. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deleting created preview environment: %w",
  124. deleteErr)))
  125. return
  126. }
  127. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting token for API: %w", err)))
  128. return
  129. }
  130. encoded, err := jwt.EncodeToken(c.Config().TokenConf)
  131. if err != nil {
  132. _, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())
  133. if deleteErr != nil {
  134. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
  135. http.StatusConflict, "error encoding token while creating environment"))
  136. return
  137. }
  138. _, deleteErr = c.Repo().Environment().DeleteEnvironment(env)
  139. if deleteErr != nil {
  140. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deleting created preview environment: %w",
  141. deleteErr)))
  142. return
  143. }
  144. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error encoding API token: %w", err)))
  145. return
  146. }
  147. err = actions.SetupEnv(&actions.EnvOpts{
  148. Client: client,
  149. ServerURL: c.Config().ServerConf.ServerURL,
  150. PorterToken: encoded,
  151. GitRepoOwner: owner,
  152. GitRepoName: name,
  153. ProjectID: project.ID,
  154. ClusterID: cluster.ID,
  155. GitInstallationID: uint(ga.InstallationID),
  156. EnvironmentName: request.Name,
  157. InstanceName: c.Config().ServerConf.InstanceName,
  158. })
  159. if err != nil {
  160. unwrappedErr := errors.Unwrap(err)
  161. if unwrappedErr != nil {
  162. if errors.Is(unwrappedErr, actions.ErrProtectedBranch) {
  163. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
  164. } else if errors.Is(unwrappedErr, actions.ErrCreatePRForProtectedBranch) {
  165. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusPreconditionFailed))
  166. }
  167. } else {
  168. c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error setting up preview environment in the github "+
  169. "repo: %w", err)))
  170. return
  171. }
  172. }
  173. envType := env.ToEnvironmentType()
  174. if len(envType.GitDeployBranches) > 0 && c.Config().ServerConf.EnableAutoPreviewBranchDeploy {
  175. errs := autoDeployBranch(env, c.Config(), envType.GitDeployBranches, false)
  176. if len(errs) > 0 {
  177. errString := errs[0].Error()
  178. for _, e := range errs {
  179. errString += ": " + e.Error()
  180. }
  181. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
  182. fmt.Errorf("error auto deploying preview branches: %s", errString), http.StatusConflict),
  183. )
  184. return
  185. }
  186. }
  187. c.WriteResult(w, r, envType)
  188. }
  189. func getGithubWebhookURLFromUID(serverURL, webhookUID string) string {
  190. return fmt.Sprintf("%s/api/github/incoming_webhook/%s", serverURL, string(webhookUID))
  191. }
  192. func autoDeployBranch(
  193. env *models.Environment,
  194. config *config.Config,
  195. branches []string,
  196. onlyNewDeployments bool,
  197. ) []error {
  198. var (
  199. errs []error
  200. wg sync.WaitGroup
  201. )
  202. for _, branch := range branches {
  203. wg.Add(1)
  204. go func(errs []error, branch string) {
  205. defer wg.Done()
  206. errs = append(errs, createWorkflowDispatchForBranch(env, config, onlyNewDeployments, branch)...)
  207. }(errs, branch)
  208. }
  209. wg.Wait()
  210. return errs
  211. }
  212. func createWorkflowDispatchForBranch(
  213. env *models.Environment,
  214. config *config.Config,
  215. onlyNewDeployments bool,
  216. branch string,
  217. ) []error {
  218. var errs []error
  219. client, err := getGithubClientFromEnvironment(config, env)
  220. if err != nil {
  221. errs = append(errs, err)
  222. return errs
  223. }
  224. var deplID uint
  225. depl, err := config.Repo.Environment().ReadDeploymentForBranch(env.ID, env.GitRepoOwner, env.GitRepoName, branch)
  226. if err == nil {
  227. if onlyNewDeployments {
  228. return errs
  229. }
  230. deplID = depl.ID
  231. } else {
  232. if errors.Is(err, gorm.ErrRecordNotFound) {
  233. depl, err := config.Repo.Environment().CreateDeployment(&models.Deployment{
  234. EnvironmentID: env.ID,
  235. Status: types.DeploymentStatusCreating,
  236. PRName: fmt.Sprintf("Deployment for branch %s", branch),
  237. RepoName: env.GitRepoName,
  238. RepoOwner: env.GitRepoOwner,
  239. PRBranchFrom: branch,
  240. PRBranchInto: branch,
  241. })
  242. if err != nil {
  243. errs = append(errs, fmt.Errorf("error creating deployment for branch %s: %w", branch, err))
  244. return errs
  245. }
  246. deplID = depl.ID
  247. } else {
  248. errs = append(errs, fmt.Errorf("error reading deployment for branch %s: %w", branch, err))
  249. return errs
  250. }
  251. }
  252. if deplID == 0 {
  253. errs = append(errs, fmt.Errorf("deployment id is 0 for branch %s", branch))
  254. return errs
  255. }
  256. _, err = client.Actions.CreateWorkflowDispatchEventByFileName(
  257. context.Background(), env.GitRepoOwner, env.GitRepoName, fmt.Sprintf("porter_%s_env.yml", env.Name),
  258. github.CreateWorkflowDispatchEventRequest{
  259. Ref: branch,
  260. Inputs: map[string]interface{}{
  261. "pr_number": fmt.Sprintf("%d", deplID),
  262. "pr_title": fmt.Sprintf("Deployment for branch %s", branch),
  263. "pr_branch_from": branch,
  264. "pr_branch_into": branch,
  265. },
  266. },
  267. )
  268. if err != nil {
  269. errs = append(errs, err)
  270. }
  271. return errs
  272. }