client.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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. ProjectName: proj.Name,
  38. }
  39. err := c.postRequest("/api/v1/private/customer", reqData, nil)
  40. if err != nil {
  41. return "", err
  42. }
  43. return fmt.Sprintf("%d-%d", proj.ID, user.ID), nil
  44. }
  45. func (c *Client) DeleteTeam(user *cemodels.User, proj *cemodels.Project) error {
  46. // call delete customer
  47. reqData := &DeleteCustomerRequest{
  48. UserID: user.ID,
  49. ProjectID: proj.ID,
  50. }
  51. return c.deleteRequest("/api/v1/private/customer", reqData, nil)
  52. }
  53. // VerifySignature verifies a webhook signature based on hmac protocol
  54. func (c *Client) VerifySignature(signature string, body []byte) bool {
  55. if len(signature) != 71 || !strings.HasPrefix(signature, "sha256=") {
  56. return false
  57. }
  58. actual := make([]byte, 32)
  59. _, err := hex.Decode(actual, []byte(signature[7:]))
  60. if err != nil {
  61. return false
  62. }
  63. computed := hmac.New(sha256.New, []byte(c.apiKey))
  64. _, err = computed.Write(body)
  65. if err != nil {
  66. return false
  67. }
  68. return hmac.Equal(computed.Sum(nil), actual)
  69. }
  70. func (c *Client) postRequest(path string, data interface{}, dst interface{}) error {
  71. return c.writeRequest("POST", path, data, dst)
  72. }
  73. func (c *Client) putRequest(path string, data interface{}, dst interface{}) error {
  74. return c.writeRequest("PUT", path, data, dst)
  75. }
  76. func (c *Client) deleteRequest(path string, data interface{}, dst interface{}) error {
  77. return c.writeRequest("DELETE", path, data, dst)
  78. }
  79. func (c *Client) getRequest(path string, dst interface{}, query ...map[string]string) error {
  80. reqURL, err := url.Parse(c.serverURL)
  81. if err != nil {
  82. return nil
  83. }
  84. reqURL.Path = path
  85. q := reqURL.Query()
  86. for _, queryGroup := range query {
  87. for key, val := range queryGroup {
  88. q.Add(key, val)
  89. }
  90. }
  91. reqURL.RawQuery = q.Encode()
  92. req, err := http.NewRequest(
  93. "GET",
  94. reqURL.String(),
  95. nil,
  96. )
  97. if err != nil {
  98. return err
  99. }
  100. req.Header.Set("Content-Type", "application/json; charset=utf-8")
  101. req.Header.Set("Accept", "application/json; charset=utf-8")
  102. req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
  103. res, err := c.httpClient.Do(req)
  104. if err != nil {
  105. return err
  106. }
  107. defer res.Body.Close()
  108. if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
  109. resBytes, err := ioutil.ReadAll(res.Body)
  110. if err != nil {
  111. return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
  112. }
  113. return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
  114. }
  115. if dst != nil {
  116. return json.NewDecoder(res.Body).Decode(dst)
  117. }
  118. return nil
  119. }
  120. func (c *Client) writeRequest(method, path string, data interface{}, dst interface{}) error {
  121. reqURL, err := url.Parse(c.serverURL)
  122. if err != nil {
  123. return nil
  124. }
  125. reqURL.Path = path
  126. var strData []byte
  127. if data != nil {
  128. strData, err = json.Marshal(data)
  129. if err != nil {
  130. return err
  131. }
  132. }
  133. req, err := http.NewRequest(
  134. method,
  135. reqURL.String(),
  136. strings.NewReader(string(strData)),
  137. )
  138. if err != nil {
  139. return err
  140. }
  141. req.Header.Set("Content-Type", "application/json; charset=utf-8")
  142. req.Header.Set("Accept", "application/json; charset=utf-8")
  143. req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
  144. res, err := c.httpClient.Do(req)
  145. if err != nil {
  146. return err
  147. }
  148. defer res.Body.Close()
  149. if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
  150. resBytes, err := ioutil.ReadAll(res.Body)
  151. if err != nil {
  152. return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
  153. }
  154. return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
  155. }
  156. if dst != nil {
  157. return json.NewDecoder(res.Body).Decode(dst)
  158. }
  159. return nil
  160. }
  161. const (
  162. FeatureSlugCPU string = "cpu"
  163. FeatureSlugMemory string = "memory"
  164. FeatureSlugClusters string = "clusters"
  165. FeatureSlugUsers string = "users"
  166. )
  167. func (c *Client) ParseProjectUsageFromWebhook(payload []byte) (*cemodels.ProjectUsage, error) {
  168. usageData := &APIWebhookRequest{}
  169. err := json.Unmarshal(payload, usageData)
  170. if err != nil {
  171. return nil, err
  172. }
  173. return &cemodels.ProjectUsage{
  174. ProjectID: usageData.ProjectID,
  175. ResourceCPU: usageData.CPU,
  176. ResourceMemory: usageData.Memory * 1000,
  177. Clusters: usageData.Clusters,
  178. Users: usageData.Users,
  179. }, nil
  180. }