Explorar o código

Merge branch 'belanger/por-83-usage-enforcement' of github.com:porter-dev/porter into belanger/por-83-usage-enforcement

jnfrati %!s(int64=4) %!d(string=hai) anos
pai
achega
3e91759f77

+ 14 - 0
api/server/handlers/project/create.go

@@ -51,6 +51,20 @@ func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		return
 	}
 
+	// create default project usage restriction
+	_, err = p.Repo().ProjectUsage().CreateProjectUsage(&models.ProjectUsage{
+		ProjectID:      proj.ID,
+		ResourceCPU:    types.BasicPlan.ResourceCPU,
+		ResourceMemory: types.BasicPlan.ResourceMemory,
+		Clusters:       types.BasicPlan.Clusters,
+		Users:          types.BasicPlan.Users,
+	})
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
 	p.WriteResult(w, r, proj.ToProjectType())
 
 	// add project to billing team

+ 7 - 0
api/server/handlers/project/delete.go

@@ -35,4 +35,11 @@ func (p *ProjectDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	}
 
 	p.WriteResult(w, r, proj.ToProjectType())
+
+	// delete the billing team
+	if err := p.Config().BillingManager.DeleteTeam(proj); err != nil {
+		// we do not write error response, since setting up billing error can be
+		// resolved later and may not be fatal
+		p.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(err))
+	}
 }

+ 37 - 0
api/server/handlers/project/get_billing.go

@@ -0,0 +1,37 @@
+package project
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type ProjectGetBillingHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewProjectGetBillingHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ProjectGetBillingHandler {
+	return &ProjectGetBillingHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *ProjectGetBillingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	res := &types.GetProjectBillingResponse{}
+
+	// determine if the project has usage attached; if so, set has_billing to true
+	usage, _ := p.Repo().ProjectUsage().ReadProjectUsage(proj.ID)
+
+	res.HasBilling = usage != nil
+
+	p.WriteResult(w, r, res)
+}

+ 27 - 0
api/server/router/project.go

@@ -165,6 +165,33 @@ func getProjectRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/billing -> project.NewProjectGetBillingHandler
+	getBillingEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/billing",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	getBillingHandler := project.NewProjectGetBillingHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: getBillingEndpoint,
+		Handler:  getBillingHandler,
+		Router:   r,
+	})
+
 	// GET /api/projects/{project_id}/billing/token -> billing.NewBillingGetTokenEndpoint
 	getBillingTokenEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{

+ 6 - 1
api/server/shared/config/loader/init_ee.go

@@ -28,8 +28,13 @@ func init() {
 	if InstanceEnvConf.ServerConf.IronPlansAPIKey != "" && InstanceEnvConf.ServerConf.IronPlansServerURL != "" {
 		serverURL := InstanceEnvConf.ServerConf.IronPlansServerURL
 		apiKey := InstanceEnvConf.ServerConf.IronPlansAPIKey
+		var err error
 
-		InstanceBillingManager = eeBilling.NewClient(serverURL, apiKey, eeRepo)
+		InstanceBillingManager, err = eeBilling.NewClient(serverURL, apiKey, eeRepo)
+
+		if err != nil {
+			panic(err)
+		}
 	} else {
 		InstanceBillingManager = &billing.NoopBillingManager{}
 	}

+ 4 - 0
api/types/project.go

@@ -65,3 +65,7 @@ type DeleteRoleResponse struct {
 type GetBillingTokenResponse struct {
 	Token string `json:"token"`
 }
+
+type GetProjectBillingResponse struct {
+	HasBilling bool `json:"has_billing"`
+}

+ 95 - 2
ee/billing/ironplans.go

@@ -31,15 +31,34 @@ type Client struct {
 	repo      repository.EERepository
 
 	httpClient *http.Client
+
+	defaultPlan *Plan
 }
 
 // NewClient creates a new billing API client
-func NewClient(serverURL, apiKey string, repo repository.EERepository) *Client {
+func NewClient(serverURL, apiKey string, repo repository.EERepository) (*Client, error) {
 	httpClient := &http.Client{
 		Timeout: time.Minute,
 	}
 
-	return &Client{apiKey, serverURL, repo, httpClient}
+	client := &Client{apiKey, serverURL, repo, httpClient, nil}
+
+	// get the default plans from the IronPlans API server
+	listResp := &ListPlansResponse{}
+	err := client.getRequest("/plans/v1", listResp)
+
+	if err != nil {
+		return nil, err
+	}
+
+	for _, plan := range listResp.Results {
+		if plan.Name == "Free" {
+			copyPlan := plan
+			client.defaultPlan = &copyPlan
+		}
+	}
+
+	return client, nil
 }
 
 func (c *Client) CreateTeam(proj *cemodels.Project) (string, error) {
@@ -52,6 +71,20 @@ func (c *Client) CreateTeam(proj *cemodels.Project) (string, error) {
 		return "", err
 	}
 
+	// put the user on the free plan, as the default behavior, if there is a default plan
+	if c.defaultPlan != nil {
+		err := c.postRequest("/subscriptions/v1", &CreateSubscriptionRequest{
+			PlanID:     c.defaultPlan.ID,
+			NextPlanID: c.defaultPlan.ID,
+			TeamID:     resp.ID,
+			IsPaused:   false,
+		}, nil)
+
+		if err != nil {
+			return "", fmt.Errorf("subscription creation failed: %s", err)
+		}
+	}
+
 	_, err = c.repo.ProjectBilling().CreateProjectBilling(&models.ProjectBilling{
 		ProjectID:     proj.ID,
 		BillingTeamID: resp.ID,
@@ -64,6 +97,16 @@ func (c *Client) CreateTeam(proj *cemodels.Project) (string, error) {
 	return resp.ID, err
 }
 
+func (c *Client) DeleteTeam(proj *cemodels.Project) error {
+	projBilling, err := c.repo.ProjectBilling().ReadProjectBillingByProjectID(proj.ID)
+
+	if err != nil {
+		return err
+	}
+
+	return c.deleteRequest(fmt.Sprintf("/teams/v1/%s", projBilling.BillingTeamID), nil, nil)
+}
+
 func (c *Client) GetTeamID(proj *cemodels.Project) (teamID string, err error) {
 	projBilling, err := c.repo.ProjectBilling().ReadProjectBillingByProjectID(proj.ID)
 
@@ -245,6 +288,54 @@ func (c *Client) deleteRequest(path string, data interface{}, dst interface{}) e
 	return c.writeRequest("DELETE", path, data, dst)
 }
 
+func (c *Client) getRequest(path string, dst interface{}) error {
+	reqURL, err := url.Parse(c.serverURL)
+
+	if err != nil {
+		return nil
+	}
+
+	reqURL.Path = path
+
+	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)
 
@@ -262,6 +353,8 @@ func (c *Client) writeRequest(method, path string, data interface{}, dst interfa
 		if err != nil {
 			return err
 		}
+
+		fmt.Println("STR DATA IS", string(strData))
 	}
 
 	req, err := http.NewRequest(

+ 11 - 0
ee/billing/types.go

@@ -38,6 +38,10 @@ type Plan struct {
 	Features   []PlanFeature `json:"features"`
 }
 
+type ListPlansResponse struct {
+	Results []Plan `json:"results"`
+}
+
 type PlanFeature struct {
 	ID          string      `json:"id"`
 	IsActive    bool        `json:"is_active"`
@@ -82,3 +86,10 @@ type SubscriptionWebhookRequest struct {
 	TeamID    string `json:"team_id"`
 	Plan      Plan   `json:"plan"`
 }
+
+type CreateSubscriptionRequest struct {
+	PlanID     string `json:"plan_id"`
+	TeamID     string `json:"team_id"`
+	IsPaused   bool   `json:"is_paused"`
+	NextPlanID string `json:"next_plan_id"`
+}

+ 7 - 0
internal/billing/billing.go

@@ -13,6 +13,9 @@ type BillingManager interface {
 	// per same team)
 	CreateTeam(proj *models.Project) (teamID string, err error)
 
+	// DeleteTeam deletes a billing team.
+	DeleteTeam(proj *models.Project) (err error)
+
 	// GetTeamID gets the billing team id for a project
 	GetTeamID(proj *models.Project) (teamID string, err error)
 
@@ -46,6 +49,10 @@ func (n *NoopBillingManager) CreateTeam(proj *models.Project) (teamID string, er
 	return fmt.Sprintf("%d", proj.ID), nil
 }
 
+func (n *NoopBillingManager) DeleteTeam(proj *models.Project) (err error) {
+	return nil
+}
+
 func (n *NoopBillingManager) GetTeamID(proj *models.Project) (teamID string, err error) {
 	return fmt.Sprintf("%d", proj.ID), nil
 }