add_project.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. package billing
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/http"
  6. "strings"
  7. "github.com/porter-dev/porter/api/server/authz"
  8. "github.com/porter-dev/porter/api/server/handlers"
  9. "github.com/porter-dev/porter/api/server/shared"
  10. "github.com/porter-dev/porter/api/server/shared/apierrors"
  11. "github.com/porter-dev/porter/api/server/shared/config"
  12. "github.com/porter-dev/porter/api/types"
  13. "github.com/porter-dev/porter/internal/models"
  14. "gorm.io/gorm"
  15. )
  16. type BillingAddProjectHandler struct {
  17. handlers.PorterHandlerReadWriter
  18. authz.KubernetesAgentGetter
  19. }
  20. func NewBillingAddProjectHandler(
  21. config *config.Config,
  22. decoderValidator shared.RequestDecoderValidator,
  23. ) http.Handler {
  24. return &BillingAddProjectHandler{
  25. PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, nil),
  26. }
  27. }
  28. // Adds a project to a billing team in IronPlans. Takes the following steps:
  29. // 1. Looks for project billing data for the given project.
  30. // 2. Checks for project billing data. If the project already has billing data, move to step 3b, otherwise 3a.
  31. // 3a. Creates a new team in IronPlans, and creates a custom plan in IronPlans. Subscribes the team to the plan.
  32. // 3b. Finds the relevant team in IronPlans, creates a custom plan, and updates the subscription for the team.
  33. // 4. If team was created, creates ProjectBilling object.
  34. // 5. If team was created, finds all roles in the team. Adds all roles as a team member to the project billing. Updates UserBilling models.
  35. func (c *BillingAddProjectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  36. // validation for internal token
  37. // if internal token is empty, throw forbidden error; this server is misconfigured
  38. if c.Config().ServerConf.RetoolToken == "" {
  39. c.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("internal retool token does not exist: re-configure the server")))
  40. return
  41. }
  42. reqToken := r.Header.Get("Authorization")
  43. splitToken := strings.Split(reqToken, "Bearer")
  44. if len(splitToken) != 2 {
  45. c.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("no token found")))
  46. return
  47. }
  48. reqToken = strings.TrimSpace(splitToken[1])
  49. if reqToken != c.Config().ServerConf.RetoolToken {
  50. c.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("passed retool token does not match env")))
  51. return
  52. }
  53. request := &types.AddProjectBillingRequest{}
  54. if ok := c.DecodeAndValidate(w, r, request); !ok {
  55. return
  56. }
  57. // make sure the project exists; if it does not exist, throw forbidden error
  58. proj, err := c.Repo().Project().ReadProject(request.ProjectID)
  59. if err != nil {
  60. if errors.Is(err, gorm.ErrRecordNotFound) {
  61. c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
  62. return
  63. }
  64. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  65. return
  66. }
  67. // look for project billing data for the given project
  68. teamID, err := c.Config().BillingManager.GetTeamID(proj)
  69. isNotFound := err != nil && errors.Is(err, gorm.ErrRecordNotFound)
  70. // if the error is not nil and is not "ErrRecordNotFound", throw error
  71. if err != nil && !isNotFound {
  72. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  73. return
  74. }
  75. // if the team is not found, create a new team
  76. if isNotFound {
  77. teamID, err = c.Config().BillingManager.CreateTeam(proj)
  78. if err != nil {
  79. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  80. return
  81. }
  82. }
  83. // determine whether to place the team on a custom plan or an existing plan
  84. if request.ExistingPlanName != "" {
  85. err = addToExistingPlan(c.Config(), request.ExistingPlanName, teamID)
  86. } else {
  87. err = addToCustomPlan(c.Config(), teamID, proj, request)
  88. }
  89. if err != nil {
  90. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  91. return
  92. }
  93. // add users in project to the plan
  94. projRoles, err := c.Repo().Project().ListProjectRoles(proj.ID)
  95. if err != nil {
  96. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  97. return
  98. }
  99. for _, role := range projRoles {
  100. user, err := c.Repo().User().ReadUser(role.UserID)
  101. if err != nil {
  102. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  103. return
  104. }
  105. err = c.Config().BillingManager.AddUserToTeam(teamID, user, &role)
  106. if err != nil {
  107. c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
  108. return
  109. }
  110. }
  111. w.WriteHeader(http.StatusOK)
  112. }
  113. func addToCustomPlan(c *config.Config, teamID string, proj *models.Project, req *types.AddProjectBillingRequest) error {
  114. // create a new plan in IronPlans
  115. planID, err := c.BillingManager.CreatePlan(teamID, proj, req)
  116. if err != nil {
  117. return err
  118. }
  119. // create a new subscription to this plan in IronPlans
  120. return c.BillingManager.CreateOrUpdateSubscription(teamID, planID)
  121. }
  122. func addToExistingPlan(c *config.Config, existingPlanName, teamID string) error {
  123. // look for existing plans in IronPlans
  124. planID, err := c.BillingManager.GetExistingPublicPlan(existingPlanName)
  125. if err != nil {
  126. return err
  127. }
  128. // create a new subscription to this plan in IronPlans
  129. return c.BillingManager.CreateOrUpdateSubscription(teamID, planID)
  130. }