app_v2_github.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. package webhook
  2. import (
  3. "net/http"
  4. "connectrpc.com/connect"
  5. "github.com/google/go-github/v41/github"
  6. "github.com/google/uuid"
  7. porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
  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/config"
  13. "github.com/porter-dev/porter/api/server/shared/requestutils"
  14. "github.com/porter-dev/porter/api/types"
  15. "github.com/porter-dev/porter/internal/models"
  16. "github.com/porter-dev/porter/internal/telemetry"
  17. )
  18. // GithubPRStatus_Closed is the status for a closed PR (closed, merged)
  19. const GithubPRStatus_Closed = "closed"
  20. // GithubWebhookHandler handles webhooks sent to /api/webhooks/github/{project_id}/{cluster_id}/{porter_app_name}
  21. type GithubWebhookHandler struct {
  22. handlers.PorterHandlerReadWriter
  23. authz.KubernetesAgentGetter
  24. }
  25. // NewGithubWebhookHandler returns a GithubWebhookHandler
  26. func NewGithubWebhookHandler(
  27. config *config.Config,
  28. decoderValidator shared.RequestDecoderValidator,
  29. writer shared.ResultWriter,
  30. ) *GithubWebhookHandler {
  31. return &GithubWebhookHandler{
  32. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
  33. KubernetesAgentGetter: authz.NewOutOfClusterAgentGetter(config),
  34. }
  35. }
  36. // ServeHTTP handles the webhook and deletes the deployment target if a PR has been closed
  37. func (c *GithubWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  38. ctx, span := telemetry.NewSpan(r.Context(), "serve-github-webhook")
  39. defer span.End()
  40. payload, err := github.ValidatePayload(r, []byte(c.Config().ServerConf.GithubIncomingWebhookSecret))
  41. if err != nil {
  42. err := telemetry.Error(ctx, span, err, "could not validate payload")
  43. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  44. return
  45. }
  46. event, err := github.ParseWebHook(github.WebHookType(r), payload)
  47. if err != nil {
  48. err := telemetry.Error(ctx, span, err, "could not parse webhook")
  49. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  50. return
  51. }
  52. webhookID, reqErr := requestutils.GetURLParamString(r, types.URLParamWebhookID)
  53. if reqErr != nil {
  54. err := telemetry.Error(ctx, span, nil, "error parsing porter app name")
  55. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  56. return
  57. }
  58. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "webhook-id", Value: webhookID})
  59. webhookUUID, err := uuid.Parse(webhookID)
  60. if err != nil {
  61. err := telemetry.Error(ctx, span, err, "error parsing webhook id")
  62. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  63. return
  64. }
  65. if webhookUUID == uuid.Nil {
  66. err := telemetry.Error(ctx, span, err, "webhook id is nil")
  67. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  68. return
  69. }
  70. webhook, err := c.Repo().GithubWebhook().Get(ctx, webhookUUID)
  71. if err != nil {
  72. err := telemetry.Error(ctx, span, err, "error getting github webhook")
  73. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  74. return
  75. }
  76. if webhook.ID == uuid.Nil {
  77. err := telemetry.Error(ctx, span, err, "github webhook id is nil")
  78. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  79. return
  80. }
  81. telemetry.WithAttributes(span,
  82. telemetry.AttributeKV{Key: "porter-app-id", Value: webhook.PorterAppID},
  83. telemetry.AttributeKV{Key: "cluster-id", Value: webhook.ClusterID},
  84. telemetry.AttributeKV{Key: "project-id", Value: webhook.ProjectID},
  85. )
  86. switch event := event.(type) {
  87. case *github.PullRequestEvent:
  88. if event.GetAction() != GithubPRStatus_Closed {
  89. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "event-processed", Value: false})
  90. c.WriteResult(w, r, nil)
  91. return
  92. }
  93. branch := event.GetPullRequest().GetHead().GetRef()
  94. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "event-branch", Value: branch})
  95. deploymentTarget, err := c.Repo().DeploymentTarget().DeploymentTargetBySelectorAndSelectorType(
  96. uint(webhook.ProjectID),
  97. uint(webhook.ClusterID),
  98. branch,
  99. string(models.DeploymentTargetSelectorType_Namespace),
  100. )
  101. if err != nil {
  102. err := telemetry.Error(ctx, span, err, "error getting deployment target")
  103. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  104. return
  105. }
  106. if deploymentTarget.ID == uuid.Nil || !deploymentTarget.Preview {
  107. c.WriteResult(w, r, nil)
  108. return
  109. }
  110. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "deployment-target-id", Value: deploymentTarget.ID.String()})
  111. if deploymentTarget.ClusterID != webhook.ClusterID {
  112. err := telemetry.Error(ctx, span, err, "deployment target cluster id does not match")
  113. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
  114. return
  115. }
  116. deleteTargetReq := connect.NewRequest(&porterv1.DeleteDeploymentTargetRequest{
  117. ProjectId: int64(webhook.ProjectID),
  118. DeploymentTargetId: deploymentTarget.ID.String(),
  119. })
  120. _, err = c.Config().ClusterControlPlaneClient.DeleteDeploymentTarget(ctx, deleteTargetReq)
  121. if err != nil {
  122. err := telemetry.Error(ctx, span, err, "error deleting deployment target")
  123. c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
  124. return
  125. }
  126. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "event-processed", Value: true})
  127. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "pr-id", Value: event.GetPullRequest().GetID()})
  128. }
  129. c.WriteResult(w, r, nil)
  130. }