github.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. package porter_app
  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/v39/github"
  10. "github.com/google/uuid"
  11. "github.com/porter-dev/porter/internal/models"
  12. "github.com/porter-dev/porter/internal/repository"
  13. "github.com/porter-dev/porter/internal/telemetry"
  14. )
  15. // GitSource is a struct that contains the git repo name and id
  16. type GitSource struct {
  17. GitRepoName string
  18. GitRepoID uint
  19. }
  20. // CreateAppWebhookInput is the input to the CreateAppWebhook function
  21. type CreateAppWebhookInput struct {
  22. ProjectID uint
  23. ClusterID uint
  24. PorterAppID uint
  25. GitSource GitSource
  26. GithubAppSecret []byte
  27. GithubAppID string
  28. GithubWebhookSecret string
  29. ServerURL string
  30. GithubWebhookRepository repository.GithubWebhookRepository
  31. }
  32. // CreateAppWebhook creates or updates a github webhook for a porter app associated with a given project / cluster / app
  33. // The webhook watches for pull request and push events, used for managing preview environments
  34. func CreateAppWebhook(ctx context.Context, inp CreateAppWebhookInput) error {
  35. ctx, span := telemetry.NewSpan(ctx, "porter-app-create-app-webhook")
  36. defer span.End()
  37. if inp.ProjectID == 0 {
  38. return telemetry.Error(ctx, span, nil, "project id is empty")
  39. }
  40. if inp.PorterAppID == 0 {
  41. return telemetry.Error(ctx, span, nil, "porter app id is empty")
  42. }
  43. if inp.ClusterID == 0 {
  44. return telemetry.Error(ctx, span, nil, "cluster id is empty")
  45. }
  46. if inp.GitSource.GitRepoName == "" {
  47. return telemetry.Error(ctx, span, nil, "git repo name is empty")
  48. }
  49. if inp.GithubAppSecret == nil {
  50. return telemetry.Error(ctx, span, nil, "github app secret is nil")
  51. }
  52. if inp.GithubAppID == "" {
  53. return telemetry.Error(ctx, span, nil, "github app id is empty")
  54. }
  55. if inp.GithubWebhookSecret == "" {
  56. return telemetry.Error(ctx, span, nil, "github webhook secret is empty")
  57. }
  58. if inp.GithubWebhookRepository == nil {
  59. return telemetry.Error(ctx, span, nil, "github webhook repository is nil")
  60. }
  61. githubClient, err := GetGithubClientByRepoID(ctx, inp.GitSource.GitRepoID, inp.GithubAppSecret, inp.GithubAppID)
  62. if err != nil {
  63. return telemetry.Error(ctx, span, err, "error creating github client")
  64. }
  65. repoDetails := strings.Split(inp.GitSource.GitRepoName, "/")
  66. if len(repoDetails) != 2 {
  67. return telemetry.Error(ctx, span, nil, "repo name is not in the format <org>/<repo>")
  68. }
  69. if _, _, err := githubClient.Repositories.Get(ctx, repoDetails[0], repoDetails[1]); err != nil {
  70. return telemetry.Error(ctx, span, err, "error getting github repo")
  71. }
  72. hook := &github.Hook{
  73. Config: map[string]interface{}{
  74. "content_type": "json",
  75. "secret": inp.GithubWebhookSecret,
  76. },
  77. Events: []string{"pull_request", "push"},
  78. Active: github.Bool(true),
  79. }
  80. // check if the webhook already exists
  81. webhook, err := inp.GithubWebhookRepository.GetByClusterAndAppID(ctx, inp.ClusterID, inp.PorterAppID)
  82. if err != nil {
  83. return telemetry.Error(ctx, span, err, "error getting github webhook")
  84. }
  85. if webhook.ID != uuid.Nil {
  86. hook.Config["url"] = fmt.Sprintf("%s/api/webhooks/github/%s", inp.ServerURL, webhook.ID.String())
  87. _, _, err := githubClient.Repositories.EditHook(ctx, repoDetails[0], repoDetails[1], webhook.GithubWebhookID, hook)
  88. if err != nil {
  89. return telemetry.Error(ctx, span, err, "error editing github webhook")
  90. }
  91. return nil
  92. }
  93. webhookID := uuid.New()
  94. hook.Config["url"] = fmt.Sprintf("%s/api/webhooks/github/%s", inp.ServerURL, webhookID)
  95. hook, _, err = githubClient.Repositories.CreateHook(ctx, repoDetails[0], repoDetails[1], hook)
  96. if err != nil {
  97. return telemetry.Error(ctx, span, err, "error creating github webhook")
  98. }
  99. webhook = &models.GithubWebhook{
  100. ID: webhookID,
  101. ProjectID: int(inp.ProjectID),
  102. ClusterID: int(inp.ClusterID),
  103. PorterAppID: int(inp.PorterAppID),
  104. GithubWebhookID: hook.GetID(),
  105. }
  106. _, err = inp.GithubWebhookRepository.Insert(ctx, webhook)
  107. if err != nil {
  108. return telemetry.Error(ctx, span, err, "error saving github webhook")
  109. }
  110. return nil
  111. }
  112. // GetGithubClientByRepoID creates a github client for a given repo id
  113. func GetGithubClientByRepoID(ctx context.Context, repoID uint, githubAppSecret []byte, githubAppID string) (*github.Client, error) {
  114. ctx, span := telemetry.NewSpan(ctx, "get-github-client-by-repo-id")
  115. defer span.End()
  116. if githubAppSecret == nil {
  117. return nil, telemetry.Error(ctx, span, nil, "github app secret is nil")
  118. }
  119. if githubAppID == "" {
  120. return nil, telemetry.Error(ctx, span, nil, "github app id is empty")
  121. }
  122. appID, err := strconv.Atoi(githubAppID)
  123. if err != nil {
  124. return nil, telemetry.Error(ctx, span, err, "could not convert github app id to int")
  125. }
  126. itr, err := ghinstallation.New(
  127. http.DefaultTransport,
  128. int64(appID),
  129. int64(repoID),
  130. githubAppSecret,
  131. )
  132. if err != nil {
  133. return nil, telemetry.Error(ctx, span, err, "could not create github app client")
  134. }
  135. if itr == nil {
  136. return nil, telemetry.Error(ctx, span, nil, "github app client is nil")
  137. }
  138. return github.NewClient(&http.Client{Transport: itr}), nil
  139. }