client.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. //go:build ee
  2. // +build ee
  3. package billing
  4. import (
  5. "crypto/hmac"
  6. "crypto/sha256"
  7. "encoding/hex"
  8. "encoding/json"
  9. "fmt"
  10. "io/ioutil"
  11. "net/http"
  12. "net/url"
  13. "strings"
  14. "time"
  15. cemodels "github.com/porter-dev/porter/internal/models"
  16. )
  17. // Client contains an API client for the internal billing engine
  18. type Client struct {
  19. apiKey string
  20. serverURL string
  21. httpClient *http.Client
  22. }
  23. // NewClient creates a new billing API client
  24. func NewClient(serverURL, apiKey string) (*Client, error) {
  25. httpClient := &http.Client{
  26. Timeout: time.Minute,
  27. }
  28. client := &Client{apiKey, serverURL, httpClient}
  29. return client, nil
  30. }
  31. func (c *Client) CreateTeam(user *cemodels.User, proj *cemodels.Project) (string, error) {
  32. // call the internal billing endpoint to create a new customer in the database
  33. reqData := &CreateCustomerRequest{
  34. Email: user.Email,
  35. UserID: user.ID,
  36. ProjectID: proj.ID,
  37. }
  38. err := c.postRequest("/api/v1/private/customer", reqData, nil)
  39. if err != nil {
  40. return "", err
  41. }
  42. return fmt.Sprintf("%d-%d", proj.ID, user.ID), nil
  43. }
  44. func (c *Client) DeleteTeam(user *cemodels.User, proj *cemodels.Project) error {
  45. // call delete customer
  46. reqData := &DeleteCustomerRequest{
  47. UserID: user.ID,
  48. ProjectID: proj.ID,
  49. }
  50. return c.deleteRequest("/api/v1/private/customer", reqData, nil)
  51. }
  52. // VerifySignature verifies a webhook signature based on hmac protocol
  53. func (c *Client) VerifySignature(signature string, body []byte) bool {
  54. if len(signature) != 71 || !strings.HasPrefix(signature, "sha256=") {
  55. return false
  56. }
  57. actual := make([]byte, 32)
  58. _, err := hex.Decode(actual, []byte(signature[7:]))
  59. if err != nil {
  60. return false
  61. }
  62. computed := hmac.New(sha256.New, []byte(c.apiKey))
  63. _, err = computed.Write(body)
  64. if err != nil {
  65. return false
  66. }
  67. return hmac.Equal(computed.Sum(nil), actual)
  68. }
  69. func (c *Client) postRequest(path string, data interface{}, dst interface{}) error {
  70. return c.writeRequest("POST", path, data, dst)
  71. }
  72. func (c *Client) putRequest(path string, data interface{}, dst interface{}) error {
  73. return c.writeRequest("PUT", path, data, dst)
  74. }
  75. func (c *Client) deleteRequest(path string, data interface{}, dst interface{}) error {
  76. return c.writeRequest("DELETE", path, data, dst)
  77. }
  78. func (c *Client) getRequest(path string, dst interface{}, query ...map[string]string) error {
  79. reqURL, err := url.Parse(c.serverURL)
  80. if err != nil {
  81. return nil
  82. }
  83. reqURL.Path = path
  84. q := reqURL.Query()
  85. for _, queryGroup := range query {
  86. for key, val := range queryGroup {
  87. q.Add(key, val)
  88. }
  89. }
  90. reqURL.RawQuery = q.Encode()
  91. req, err := http.NewRequest(
  92. "GET",
  93. reqURL.String(),
  94. nil,
  95. )
  96. if err != nil {
  97. return err
  98. }
  99. req.Header.Set("Content-Type", "application/json; charset=utf-8")
  100. req.Header.Set("Accept", "application/json; charset=utf-8")
  101. req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
  102. res, err := c.httpClient.Do(req)
  103. if err != nil {
  104. return err
  105. }
  106. defer res.Body.Close()
  107. if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
  108. resBytes, err := ioutil.ReadAll(res.Body)
  109. if err != nil {
  110. return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
  111. }
  112. return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
  113. }
  114. if dst != nil {
  115. return json.NewDecoder(res.Body).Decode(dst)
  116. }
  117. return nil
  118. }
  119. func (c *Client) writeRequest(method, path string, data interface{}, dst interface{}) error {
  120. reqURL, err := url.Parse(c.serverURL)
  121. if err != nil {
  122. return nil
  123. }
  124. reqURL.Path = path
  125. var strData []byte
  126. if data != nil {
  127. strData, err = json.Marshal(data)
  128. if err != nil {
  129. return err
  130. }
  131. }
  132. req, err := http.NewRequest(
  133. method,
  134. reqURL.String(),
  135. strings.NewReader(string(strData)),
  136. )
  137. if err != nil {
  138. return err
  139. }
  140. req.Header.Set("Content-Type", "application/json; charset=utf-8")
  141. req.Header.Set("Accept", "application/json; charset=utf-8")
  142. req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
  143. res, err := c.httpClient.Do(req)
  144. if err != nil {
  145. return err
  146. }
  147. defer res.Body.Close()
  148. if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
  149. resBytes, err := ioutil.ReadAll(res.Body)
  150. if err != nil {
  151. return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
  152. }
  153. return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
  154. }
  155. if dst != nil {
  156. return json.NewDecoder(res.Body).Decode(dst)
  157. }
  158. return nil
  159. }
  160. const (
  161. FeatureSlugCPU string = "cpu"
  162. FeatureSlugMemory string = "memory"
  163. FeatureSlugClusters string = "clusters"
  164. FeatureSlugUsers string = "users"
  165. )
  166. func (c *Client) ParseProjectUsageFromWebhook(payload []byte) (*cemodels.ProjectUsage, error) {
  167. // TODO: parse webhook model
  168. return nil, nil
  169. // subscription := &SubscriptionWebhookRequest{}
  170. // err := json.Unmarshal(payload, subscription)
  171. // if err != nil {
  172. // return nil, err
  173. // }
  174. // // if event type is not subscription, return wrong webhook event type error
  175. // if subscription.EventType != "subscription" {
  176. // return nil, nil
  177. // }
  178. // // get the project id linked to that team
  179. // projBilling, err := c.repo.ProjectBilling().ReadProjectBillingByTeamID(subscription.TeamID)
  180. // if err != nil {
  181. // return nil, err
  182. // }
  183. // usage := &cemodels.ProjectUsage{
  184. // ProjectID: projBilling.ProjectID,
  185. // }
  186. // for _, feature := range subscription.Plan.Features {
  187. // // look for slug of "cpus" and "memory"
  188. // maxLimit := uint(feature.FeatureSpec.MaxLimit)
  189. // switch feature.Feature.Slug {
  190. // case FeatureSlugCPU:
  191. // usage.ResourceCPU = maxLimit
  192. // case FeatureSlugMemory:
  193. // usage.ResourceMemory = 1000 * maxLimit
  194. // case FeatureSlugClusters:
  195. // usage.Clusters = maxLimit
  196. // case FeatureSlugUsers:
  197. // usage.Users = maxLimit
  198. // }
  199. // }
  200. // return usage, nil
  201. }