Просмотр исходного кода

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

jnfrati 4 лет назад
Родитель
Сommit
881e4059b2

+ 1 - 1
.air.toml

@@ -7,7 +7,7 @@ tmp_dir = "tmp"
 
 [build]
 # Just plain old shell command. You could use `make` as well.
-cmd = "go build -o ./tmp/app -tags ee -ldflags=\"-X 'github.com/porter-dev/porter/api/server/shared/config.Version=dev-ee'\" ./cmd/app"
+cmd = "go build -o ./tmp/app -tags ee -ldflags=\"-X 'main.Version=dev-ee'\" ./cmd/app"
 
 # Binary file yields from `cmd`.
 bin = "tmp/app"

+ 5 - 1
api/server/handlers/project/get_usage.go

@@ -30,7 +30,11 @@ func (p *ProjectGetUsageHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 
 	res := &types.GetProjectUsageResponse{}
 
-	currUsage, limit, usageCache, err := usage.GetUsage(p.Config(), proj)
+	currUsage, limit, usageCache, err := usage.GetUsage(&usage.GetUsageOpts{
+		Project: proj,
+		DOConf:  p.Config().DOConf,
+		Repo:    p.Repo(),
+	})
 
 	if err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 5 - 1
api/server/router/middleware/usage.go

@@ -27,7 +27,11 @@ func (b *UsageMiddleware) Middleware(next http.Handler) http.Handler {
 		proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
 		// get the project usage limits
-		currentUsage, limit, _, err := usage.GetUsage(b.config, proj)
+		currentUsage, limit, _, err := usage.GetUsage(&usage.GetUsageOpts{
+			Project: proj,
+			DOConf:  b.config.DOConf,
+			Repo:    b.config.Repo,
+		})
 
 		if err != nil {
 			apierrors.HandleAPIError(

+ 0 - 2
api/server/shared/config/config.go

@@ -17,8 +17,6 @@ import (
 	"gorm.io/gorm"
 )
 
-var Version string = "dev-ce"
-
 type Config struct {
 	// Logger for logging
 	Logger *logger.Logger

+ 6 - 4
api/server/shared/config/loader/loader.go

@@ -25,10 +25,12 @@ import (
 	lr "github.com/porter-dev/porter/internal/logger"
 )
 
-type EnvConfigLoader struct{}
+type EnvConfigLoader struct {
+	version string
+}
 
-func NewEnvLoader() config.ConfigLoader {
-	return &EnvConfigLoader{}
+func NewEnvLoader(version string) config.ConfigLoader {
+	return &EnvConfigLoader{version}
 }
 
 func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
@@ -47,7 +49,7 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		RedisConf:  envConf.RedisConf,
 	}
 
-	res.Metadata = config.MetadataFromConf(envConf.ServerConf)
+	res.Metadata = config.MetadataFromConf(envConf.ServerConf, e.version)
 
 	db, err := adapter.New(envConf.DBConf)
 

+ 2 - 2
api/server/shared/config/metadata.go

@@ -14,7 +14,7 @@ type Metadata struct {
 	Version            string `json:"version"`
 }
 
-func MetadataFromConf(sc *env.ServerConf) *Metadata {
+func MetadataFromConf(sc *env.ServerConf, version string) *Metadata {
 	return &Metadata{
 		// note: provisioning is set in the metadata after the loader is called
 		Provisioning:       false,
@@ -25,7 +25,7 @@ func MetadataFromConf(sc *env.ServerConf) *Metadata {
 		SlackNotifications: sc.SlackClientID != "" && sc.SlackClientSecret != "",
 		Email:              sc.SendgridAPIKey != "",
 		Analytics:          sc.SegmentClientKey != "",
-		Version:            Version,
+		Version:            version,
 	}
 }
 

+ 2 - 2
cmd/app/main.go

@@ -14,7 +14,7 @@ import (
 )
 
 // Version will be linked by an ldflag during build
-var Version string = "dev"
+var Version string = "dev-ce"
 
 func main() {
 	var versionFlag bool
@@ -27,7 +27,7 @@ func main() {
 		os.Exit(0)
 	}
 
-	cl := loader.NewEnvLoader()
+	cl := loader.NewEnvLoader(Version)
 
 	config, err := cl.LoadConfig()
 

+ 75 - 0
ee/docker/ee.Dockerfile

@@ -0,0 +1,75 @@
+# syntax=docker/dockerfile:1.1.7-experimental
+
+# Base Go environment
+# -------------------
+FROM golang:1.15-alpine as base
+WORKDIR /porter
+
+RUN apk update && apk add --no-cache gcc musl-dev git
+
+COPY go.mod go.sum ./
+COPY /cmd ./cmd
+COPY /internal ./internal
+COPY /api ./api
+COPY /ee ./ee
+
+RUN --mount=type=cache,target=$GOPATH/pkg/mod \
+    go mod download
+
+# Go build environment
+# --------------------
+FROM base AS build-go
+
+ARG version=production
+
+RUN --mount=type=cache,target=/root/.cache/go-build \
+    --mount=type=cache,target=$GOPATH/pkg/mod \
+    go build -ldflags="-w -s -X 'main.Version=${version}'" -tags ee -a -o ./bin/app ./cmd/app && \
+    go build -ldflags '-w -s' -a -tags ee -o ./bin/migrate ./cmd/migrate && \
+    go build -ldflags '-w -s' -a -tags ee -o ./bin/ready ./cmd/ready
+
+# Go test environment
+# -------------------
+FROM base AS porter-test
+
+RUN --mount=type=cache,target=/root/.cache/go-build \
+    --mount=type=cache,target=$GOPATH/pkg/mod \
+    go test ./...
+
+# Webpack build environment
+# -------------------------
+FROM node:latest as build-webpack
+WORKDIR /webpack
+
+COPY ./dashboard ./
+
+RUN npm i
+
+ENV NODE_ENV=production
+
+RUN npm run build
+
+# Deployment environment
+# ----------------------
+FROM alpine
+RUN apk update
+
+COPY --from=build-go /porter/bin/app /porter/
+COPY --from=build-go /porter/bin/migrate /porter/
+COPY --from=build-go /porter/bin/ready /porter/
+COPY --from=build-webpack /webpack/build /porter/static
+
+ENV DEBUG=false
+ENV STATIC_FILE_PATH=/porter/static
+ENV SERVER_PORT=8080
+ENV SERVER_TIMEOUT_READ=5s
+ENV SERVER_TIMEOUT_WRITE=10s
+ENV SERVER_TIMEOUT_IDLE=15s
+
+ENV COOKIE_SECRETS=secret
+
+ENV SQL_LITE=true
+ENV ADMIN_INIT=false
+
+EXPOSE 8080
+CMD /porter/migrate && /porter/app

+ 3 - 3
ee/usage/limit.go

@@ -5,15 +5,15 @@ package usage
 import (
 	"errors"
 
-	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
 	"gorm.io/gorm"
 )
 
-func GetLimit(config *config.Config, proj *models.Project) (limit *types.ProjectUsage, err error) {
+func GetLimit(repo repository.Repository, proj *models.Project) (limit *types.ProjectUsage, err error) {
 	// query for the project limit; if not found, default to basic
-	limitModel, err := config.Repo.ProjectUsage().ReadProjectUsage(proj.ID)
+	limitModel, err := repo.ProjectUsage().ReadProjectUsage(proj.ID)
 
 	if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
 		copyBasic := types.BasicPlan

+ 2 - 2
internal/usage/limit_ce.go

@@ -3,12 +3,12 @@
 package usage
 
 import (
-	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
 )
 
-func GetLimit(config *config.Config, proj *models.Project) (limit *types.ProjectUsage, err error) {
+func GetLimit(repo repository.Repository, proj *models.Project) (limit *types.ProjectUsage, err error) {
 	copyLimit := types.BasicPlan
 
 	return &copyLimit, nil

+ 2 - 2
internal/usage/limit_ee.go

@@ -3,13 +3,13 @@
 package usage
 
 import (
-	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/ee/usage"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
 )
 
-var GetLimit func(config *config.Config, proj *models.Project) (limit *types.ProjectUsage, err error)
+var GetLimit func(repo repository.Repository, proj *models.Project) (limit *types.ProjectUsage, err error)
 
 func init() {
 	GetLimit = usage.GetLimit

+ 23 - 15
internal/usage/usage.go

@@ -5,42 +5,48 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/porter-dev/porter/api/server/authz"
-	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/nodes"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"golang.org/x/oauth2"
 	"gorm.io/gorm"
 )
 
+type GetUsageOpts struct {
+	Repo    repository.Repository
+	DOConf  *oauth2.Config
+	Project *models.Project
+}
+
 // GetUsage gets a project's current usage and usage limit
-func GetUsage(config *config.Config, proj *models.Project) (
+func GetUsage(opts *GetUsageOpts) (
 	current, limit *types.ProjectUsage,
 	resourceUse *models.ProjectUsageCache,
 	err error,
 ) {
-	limit, err = GetLimit(config, proj)
+	limit, err = GetLimit(opts.Repo, opts.Project)
 
 	if err != nil {
 		return nil, nil, nil, err
 	}
 
 	// query for the linked cluster counts
-	clusters, err := config.Repo.Cluster().ListClustersByProjectID(proj.ID)
+	clusters, err := opts.Repo.Cluster().ListClustersByProjectID(opts.Project.ID)
 
 	if err != nil {
 		return nil, nil, nil, err
 	}
 
 	// query for the linked user counts
-	roles, err := config.Repo.Project().ListProjectRoles(proj.ID)
+	roles, err := opts.Repo.Project().ListProjectRoles(opts.Project.ID)
 
 	if err != nil {
 		return nil, nil, nil, err
 	}
 
-	usageCache, err := config.Repo.ProjectUsage().ReadProjectUsageCache(proj.ID)
+	usageCache, err := opts.Repo.ProjectUsage().ReadProjectUsageCache(opts.Project.ID)
 	isCacheFound := true
 
 	if isCacheFound = !errors.Is(err, gorm.ErrRecordNotFound); err != nil && isCacheFound {
@@ -50,7 +56,7 @@ func GetUsage(config *config.Config, proj *models.Project) (
 	// if the usage cache is 24 hours old, was not found, or usage is over limit,
 	// re-query for the usage
 	if !isCacheFound || usageCache.Is24HrOld() || usageCache.ResourceMemory > limit.ResourceMemory || usageCache.ResourceCPU > limit.ResourceCPU {
-		cpu, memory, err := getResourceUsage(config, clusters)
+		cpu, memory, err := getResourceUsage(opts, clusters)
 
 		if err != nil {
 			return nil, nil, nil, err
@@ -58,7 +64,7 @@ func GetUsage(config *config.Config, proj *models.Project) (
 
 		if !isCacheFound {
 			usageCache = &models.ProjectUsageCache{
-				ProjectID:      proj.ID,
+				ProjectID:      opts.Project.ID,
 				ResourceCPU:    cpu,
 				ResourceMemory: memory,
 			}
@@ -78,9 +84,9 @@ func GetUsage(config *config.Config, proj *models.Project) (
 		usageCache.Exceeded = isExceeded
 
 		if !isCacheFound {
-			usageCache, err = config.Repo.ProjectUsage().CreateProjectUsageCache(usageCache)
+			usageCache, err = opts.Repo.ProjectUsage().CreateProjectUsageCache(usageCache)
 		} else {
-			usageCache, err = config.Repo.ProjectUsage().UpdateProjectUsageCache(usageCache)
+			usageCache, err = opts.Repo.ProjectUsage().UpdateProjectUsageCache(usageCache)
 		}
 	}
 
@@ -93,13 +99,15 @@ func GetUsage(config *config.Config, proj *models.Project) (
 }
 
 // gets the total resource usage across all nodes in all clusters
-func getResourceUsage(config *config.Config, clusters []*models.Cluster) (uint, uint, error) {
-	// TODO; pass this in?
+func getResourceUsage(opts *GetUsageOpts, clusters []*models.Cluster) (uint, uint, error) {
 	var totCPU, totMem uint = 0, 0
-	getter := authz.NewOutOfClusterAgentGetter(config)
 
 	for _, cluster := range clusters {
-		ooc := getter.GetOutOfClusterConfig(cluster)
+		ooc := &kubernetes.OutOfClusterConfig{
+			Cluster:           cluster,
+			Repo:              opts.Repo,
+			DigitalOceanOAuth: opts.DOConf,
+		}
 
 		agent, err := kubernetes.GetAgentOutOfClusterConfig(ooc)
 

+ 106 - 0
services/usage/usage.go

@@ -0,0 +1,106 @@
+package usage
+
+import (
+	"time"
+
+	"github.com/porter-dev/porter/api/server/shared/config/env"
+	"github.com/porter-dev/porter/internal/adapter"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/oauth"
+	"github.com/porter-dev/porter/internal/repository"
+	"github.com/porter-dev/porter/internal/usage"
+	"golang.org/x/oauth2"
+	"gorm.io/gorm"
+
+	rgorm "github.com/porter-dev/porter/internal/repository/gorm"
+)
+
+type UsageTracker struct {
+	db     *gorm.DB
+	repo   repository.Repository
+	doConf *oauth2.Config
+}
+
+type UsageTrackerOpts struct {
+	DBConf         *env.DBConf
+	DOClientID     string
+	DOClientSecret string
+	DOScopes       []string
+	ServerURL      string
+}
+
+const stepSize = 100
+
+func NewUsageTracker(opts *UsageTrackerOpts) (*UsageTracker, error) {
+	db, err := adapter.New(opts.DBConf)
+
+	if err != nil {
+		return nil, err
+	}
+
+	var key [32]byte
+
+	for i, b := range []byte(opts.DBConf.EncryptionKey) {
+		key[i] = b
+	}
+
+	repo := rgorm.NewRepository(db, &key)
+
+	doConf := oauth.NewDigitalOceanClient(&oauth.Config{
+		ClientID:     opts.DOClientID,
+		ClientSecret: opts.DOClientSecret,
+		Scopes:       opts.DOScopes,
+		BaseURL:      opts.ServerURL,
+	})
+
+	return &UsageTracker{db, repo, doConf}, nil
+}
+
+type UsageTrackerResponse struct {
+	ResourceCPU    uint
+	ResourceMemory uint
+	Exceeded       bool
+	ExceededSince  *time.Time
+}
+
+func (u *UsageTracker) GetProjectUsage() (map[uint]*UsageTrackerResponse, error) {
+	res := make(map[uint]*UsageTrackerResponse)
+
+	// get the count of the projects
+	var count int64
+
+	if err := u.db.Model(&models.Project{}).Count(&count).Error; err != nil {
+		return nil, err
+	}
+
+	// iterate (count / stepSize) + 1 times using Limit and Offset
+	for i := 0; i < (int(count)/stepSize)+1; i++ {
+		projects := []*models.Project{}
+
+		if err := u.db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&projects).Error; err != nil {
+			return nil, err
+		}
+
+		// go through each project
+		for _, project := range projects {
+			_, _, cache, err := usage.GetUsage(&usage.GetUsageOpts{
+				Repo:    u.repo,
+				DOConf:  u.doConf,
+				Project: project,
+			})
+
+			if err != nil {
+				continue
+			}
+
+			res[project.ID] = &UsageTrackerResponse{
+				ResourceCPU:    cache.ResourceCPU,
+				ResourceMemory: cache.ResourceMemory,
+				Exceeded:       cache.Exceeded,
+				ExceededSince:  cache.ExceededSince,
+			}
+		}
+	}
+
+	return res, nil
+}