| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- //go:build ee
- // +build ee
- package billing
- import (
- "crypto/hmac"
- "crypto/sha256"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/url"
- "strings"
- "time"
- cemodels "github.com/porter-dev/porter/internal/models"
- )
- // Client contains an API client for the internal billing engine
- type Client struct {
- apiKey string
- serverURL string
- httpClient *http.Client
- }
- // NewClient creates a new billing API client
- func NewClient(serverURL, apiKey string) (*Client, error) {
- httpClient := &http.Client{
- Timeout: time.Minute,
- }
- client := &Client{apiKey, serverURL, httpClient}
- return client, nil
- }
- func (c *Client) CreateTeam(user *cemodels.User, proj *cemodels.Project) (string, error) {
- // call the internal billing endpoint to create a new customer in the database
- reqData := &CreateCustomerRequest{
- Email: user.Email,
- UserID: user.ID,
- ProjectID: proj.ID,
- }
- err := c.postRequest("/api/v1/private/customer", reqData, nil)
- if err != nil {
- return "", err
- }
- return fmt.Sprintf("%d-%d", proj.ID, user.ID), nil
- }
- func (c *Client) DeleteTeam(user *cemodels.User, proj *cemodels.Project) error {
- // call delete customer
- reqData := &DeleteCustomerRequest{
- UserID: user.ID,
- ProjectID: proj.ID,
- }
- return c.deleteRequest("/api/v1/private/customer", reqData, nil)
- }
- // VerifySignature verifies a webhook signature based on hmac protocol
- func (c *Client) VerifySignature(signature string, body []byte) bool {
- if len(signature) != 71 || !strings.HasPrefix(signature, "sha256=") {
- return false
- }
- actual := make([]byte, 32)
- _, err := hex.Decode(actual, []byte(signature[7:]))
- if err != nil {
- return false
- }
- computed := hmac.New(sha256.New, []byte(c.apiKey))
- _, err = computed.Write(body)
- if err != nil {
- return false
- }
- return hmac.Equal(computed.Sum(nil), actual)
- }
- func (c *Client) postRequest(path string, data interface{}, dst interface{}) error {
- return c.writeRequest("POST", path, data, dst)
- }
- func (c *Client) putRequest(path string, data interface{}, dst interface{}) error {
- return c.writeRequest("PUT", path, data, dst)
- }
- func (c *Client) deleteRequest(path string, data interface{}, dst interface{}) error {
- return c.writeRequest("DELETE", path, data, dst)
- }
- func (c *Client) getRequest(path string, dst interface{}, query ...map[string]string) error {
- reqURL, err := url.Parse(c.serverURL)
- if err != nil {
- return nil
- }
- reqURL.Path = path
- q := reqURL.Query()
- for _, queryGroup := range query {
- for key, val := range queryGroup {
- q.Add(key, val)
- }
- }
- reqURL.RawQuery = q.Encode()
- req, err := http.NewRequest(
- "GET",
- reqURL.String(),
- nil,
- )
- if err != nil {
- return err
- }
- req.Header.Set("Content-Type", "application/json; charset=utf-8")
- req.Header.Set("Accept", "application/json; charset=utf-8")
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
- res, err := c.httpClient.Do(req)
- if err != nil {
- return err
- }
- defer res.Body.Close()
- if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
- resBytes, err := ioutil.ReadAll(res.Body)
- if err != nil {
- return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
- }
- return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
- }
- if dst != nil {
- return json.NewDecoder(res.Body).Decode(dst)
- }
- return nil
- }
- func (c *Client) writeRequest(method, path string, data interface{}, dst interface{}) error {
- reqURL, err := url.Parse(c.serverURL)
- if err != nil {
- return nil
- }
- reqURL.Path = path
- var strData []byte
- if data != nil {
- strData, err = json.Marshal(data)
- if err != nil {
- return err
- }
- }
- req, err := http.NewRequest(
- method,
- reqURL.String(),
- strings.NewReader(string(strData)),
- )
- if err != nil {
- return err
- }
- req.Header.Set("Content-Type", "application/json; charset=utf-8")
- req.Header.Set("Accept", "application/json; charset=utf-8")
- req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))
- res, err := c.httpClient.Do(req)
- if err != nil {
- return err
- }
- defer res.Body.Close()
- if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
- resBytes, err := ioutil.ReadAll(res.Body)
- if err != nil {
- return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
- }
- return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
- }
- if dst != nil {
- return json.NewDecoder(res.Body).Decode(dst)
- }
- return nil
- }
- const (
- FeatureSlugCPU string = "cpu"
- FeatureSlugMemory string = "memory"
- FeatureSlugClusters string = "clusters"
- FeatureSlugUsers string = "users"
- )
- func (c *Client) ParseProjectUsageFromWebhook(payload []byte) (*cemodels.ProjectUsage, error) {
- // TODO: parse webhook model
- return nil, nil
- // subscription := &SubscriptionWebhookRequest{}
- // err := json.Unmarshal(payload, subscription)
- // if err != nil {
- // return nil, err
- // }
- // // if event type is not subscription, return wrong webhook event type error
- // if subscription.EventType != "subscription" {
- // return nil, nil
- // }
- // // get the project id linked to that team
- // projBilling, err := c.repo.ProjectBilling().ReadProjectBillingByTeamID(subscription.TeamID)
- // if err != nil {
- // return nil, err
- // }
- // usage := &cemodels.ProjectUsage{
- // ProjectID: projBilling.ProjectID,
- // }
- // for _, feature := range subscription.Plan.Features {
- // // look for slug of "cpus" and "memory"
- // maxLimit := uint(feature.FeatureSpec.MaxLimit)
- // switch feature.Feature.Slug {
- // case FeatureSlugCPU:
- // usage.ResourceCPU = maxLimit
- // case FeatureSlugMemory:
- // usage.ResourceMemory = 1000 * maxLimit
- // case FeatureSlugClusters:
- // usage.Clusters = maxLimit
- // case FeatureSlugUsers:
- // usage.Users = maxLimit
- // }
- // }
- // return usage, nil
- }
|