github_incoming.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. package webhook
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "strconv"
  7. "strings"
  8. "github.com/bradleyfalzon/ghinstallation/v2"
  9. "github.com/google/go-github/v41/github"
  10. "github.com/porter-dev/porter/api/server/authz"
  11. "github.com/porter-dev/porter/api/server/handlers"
  12. "github.com/porter-dev/porter/api/server/shared"
  13. "github.com/porter-dev/porter/api/server/shared/apierrors"
  14. "github.com/porter-dev/porter/api/server/shared/config"
  15. "github.com/porter-dev/porter/api/server/shared/requestutils"
  16. "github.com/porter-dev/porter/api/types"
  17. "github.com/porter-dev/porter/internal/models"
  18. )
  19. type GithubIncomingWebhookHandler struct {
  20. handlers.PorterHandlerReadWriter
  21. authz.KubernetesAgentGetter
  22. }
  23. func NewGithubIncomingWebhookHandler(
  24. config *config.Config,
  25. decoderValidator shared.RequestDecoderValidator,
  26. writer shared.ResultWriter,
  27. ) *GithubIncomingWebhookHandler {
  28. return &GithubIncomingWebhookHandler{
  29. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  30. KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
  31. }
  32. }
  33. func (c *GithubIncomingWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  34. payload, err := github.ValidatePayload(r, []byte(c.Config().ServerConf.GithubIncomingWebhookSecret))
  35. if err != nil {
  36. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  37. return
  38. }
  39. event, err := github.ParseWebHook(github.WebHookType(r), payload)
  40. if err != nil {
  41. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  42. return
  43. }
  44. switch event := event.(type) {
  45. case *github.PullRequestEvent:
  46. err = c.processPullRequestEvent(event, r)
  47. if err != nil {
  48. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  49. return
  50. }
  51. }
  52. }
  53. func (c *GithubIncomingWebhookHandler) processPullRequestEvent(event *github.PullRequestEvent, r *http.Request) error {
  54. // get the webhook id from the request
  55. webhookID, reqErr := requestutils.GetURLParamString(r, types.URLParamIncomingWebhookID)
  56. if reqErr != nil {
  57. return fmt.Errorf(reqErr.Error())
  58. }
  59. owner := event.GetRepo().GetOwner().GetLogin()
  60. repo := event.GetRepo().GetName()
  61. env, err := c.Repo().Environment().ReadEnvironmentByWebhookIDOwnerRepoName(webhookID, owner, repo)
  62. if err != nil {
  63. return err
  64. }
  65. // create deployment on GitHub API
  66. client, err := getGithubClientFromEnvironment(c.Config(), env)
  67. if err != nil {
  68. return err
  69. }
  70. if env.Mode == "auto" && event.GetAction() == "opened" {
  71. _, err := client.Actions.CreateWorkflowDispatchEventByFileName(
  72. r.Context(), owner, repo, fmt.Sprintf("porter_%s_env.yml", env.Name),
  73. github.CreateWorkflowDispatchEventRequest{
  74. Ref: event.PullRequest.GetHead().GetRef(),
  75. Inputs: map[string]interface{}{
  76. "pr_number": strconv.FormatUint(uint64(event.PullRequest.GetNumber()), 10),
  77. "pr_title": event.PullRequest.GetTitle(),
  78. "pr_branch_from": event.PullRequest.GetHead().GetRef(),
  79. "pr_branch_into": event.PullRequest.GetBase().GetRef(),
  80. },
  81. },
  82. )
  83. if err != nil {
  84. return err
  85. }
  86. } else if event.GetAction() == "synchronize" || event.GetAction() == "closed" {
  87. depl, err := c.Repo().Environment().ReadDeploymentByGitDetails(
  88. env.ID, owner, repo, uint(event.GetPullRequest().GetNumber()),
  89. )
  90. if err != nil {
  91. return err
  92. }
  93. if depl.Status != types.DeploymentStatusInactive {
  94. if event.GetAction() == "synchronize" {
  95. _, err := client.Actions.CreateWorkflowDispatchEventByFileName(
  96. r.Context(), owner, repo, fmt.Sprintf("porter_%s_env.yml", env.Name),
  97. github.CreateWorkflowDispatchEventRequest{
  98. Ref: event.PullRequest.GetHead().GetRef(),
  99. Inputs: map[string]interface{}{
  100. "pr_number": strconv.FormatUint(uint64(event.PullRequest.GetNumber()), 10),
  101. "pr_title": event.PullRequest.GetTitle(),
  102. "pr_branch_from": event.PullRequest.GetHead().GetRef(),
  103. "pr_branch_into": event.PullRequest.GetBase().GetRef(),
  104. },
  105. },
  106. )
  107. if err != nil {
  108. return err
  109. }
  110. } else {
  111. err = c.deleteDeployment(r, depl, env, client)
  112. if err != nil {
  113. return err
  114. }
  115. }
  116. }
  117. }
  118. return nil
  119. }
  120. func (c *GithubIncomingWebhookHandler) deleteDeployment(
  121. r *http.Request,
  122. depl *models.Deployment,
  123. env *models.Environment,
  124. client *github.Client,
  125. ) error {
  126. cluster, err := c.Repo().Cluster().ReadCluster(env.ProjectID, env.ClusterID)
  127. if err != nil {
  128. return err
  129. }
  130. agent, err := c.GetAgent(r, cluster, "")
  131. if err != nil {
  132. return err
  133. }
  134. // make sure we don't delete default or kube-system by checking for prefix, for now
  135. if strings.Contains(depl.Namespace, "pr-") {
  136. err = agent.DeleteNamespace(depl.Namespace)
  137. if err != nil {
  138. return err
  139. }
  140. }
  141. // Create new deployment status to indicate deployment is ready
  142. state := "inactive"
  143. deploymentStatusRequest := github.DeploymentStatusRequest{
  144. State: &state,
  145. }
  146. client.Repositories.CreateDeploymentStatus(
  147. context.Background(),
  148. env.GitRepoOwner,
  149. env.GitRepoName,
  150. depl.GHDeploymentID,
  151. &deploymentStatusRequest,
  152. )
  153. depl.Status = types.DeploymentStatusInactive
  154. // update the deployment to mark it inactive
  155. _, err = c.Repo().Environment().UpdateDeployment(depl)
  156. if err != nil {
  157. return err
  158. }
  159. return nil
  160. }
  161. func getGithubClientFromEnvironment(config *config.Config, env *models.Environment) (*github.Client, error) {
  162. // get the github app client
  163. ghAppId, err := strconv.Atoi(config.ServerConf.GithubAppID)
  164. if err != nil {
  165. return nil, err
  166. }
  167. // authenticate as github app installation
  168. itr, err := ghinstallation.NewKeyFromFile(
  169. http.DefaultTransport,
  170. int64(ghAppId),
  171. int64(env.GitInstallationID),
  172. config.ServerConf.GithubAppSecretPath,
  173. )
  174. if err != nil {
  175. return nil, err
  176. }
  177. return github.NewClient(&http.Client{Transport: itr}), nil
  178. }