From e36001b7cfb1a7eb9e8cfbb04f646a37377cdd8c Mon Sep 17 00:00:00 2001 From: Moby von Briesen Date: Thu, 1 Jul 2021 01:13:45 +0200 Subject: [PATCH] satellite/console: Implement paid tier When a user adds a credit card, switch them to the paid tier and update their projects with new bandwidth/storage limits. New projects for the paid tier user will also have the updated limits. The new limits are: * storage per project - 50 GB free/25 TB paid * bandwidth per project - 50 GB free/100 TB paid Change-Id: I7d6467d077e8bb2bbe4bcf88ab8d75490f83165e --- .../accounting/projectlimitcache_test.go | 4 +- satellite/accounting/projectusage_test.go | 8 +-- satellite/api.go | 4 +- satellite/console/projects.go | 16 ++++- satellite/console/service.go | 38 ++++++++++- satellite/console/service_test.go | 67 +++++++++++++++++++ satellite/console/users.go | 2 + satellite/console/users_test.go | 42 ++++++++++++ satellite/satellitedb/projects.go | 6 ++ satellite/satellitedb/users.go | 15 +++++ scripts/testdata/satellite-config.yaml.lock | 14 ++-- 11 files changed, 200 insertions(+), 16 deletions(-) diff --git a/satellite/accounting/projectlimitcache_test.go b/satellite/accounting/projectlimitcache_test.go index f319d8b04..aede6106e 100644 --- a/satellite/accounting/projectlimitcache_test.go +++ b/satellite/accounting/projectlimitcache_test.go @@ -60,8 +60,8 @@ func TestProjectLimitCache(t *testing.T) { projectUsageSvc := saPeer.Accounting.ProjectUsage accountingDB := saPeer.DB.ProjectAccounting() projectLimitCache := saPeer.ProjectLimits.Cache - defaultUsageLimit := saPeer.Config.Console.UsageLimits.DefaultStorageLimit.Int64() - defaultBandwidthLimit := saPeer.Config.Console.UsageLimits.DefaultBandwidthLimit.Int64() + defaultUsageLimit := saPeer.Config.Console.UsageLimits.Storage.Free.Int64() + defaultBandwidthLimit := saPeer.Config.Console.UsageLimits.Bandwidth.Free.Int64() dbDefaultLimits := accounting.ProjectLimits{Usage: &defaultUsageLimit, Bandwidth: &defaultBandwidthLimit} testProject, err := saPeer.DB.Console().Projects().Insert(ctx, &console.Project{Name: "test", OwnerID: testrand.UUID()}) diff --git a/satellite/accounting/projectusage_test.go b/satellite/accounting/projectusage_test.go index b3c8a73f5..fe8a0d33d 100644 --- a/satellite/accounting/projectusage_test.go +++ b/satellite/accounting/projectusage_test.go @@ -38,8 +38,8 @@ func TestProjectUsageStorage(t *testing.T) { SatelliteCount: 1, StorageNodeCount: 4, UplinkCount: 1, Reconfigure: testplanet.Reconfigure{ Satellite: func(log *zap.Logger, index int, config *satellite.Config) { - config.Console.UsageLimits.DefaultStorageLimit = 1 * memory.MB - config.Console.UsageLimits.DefaultBandwidthLimit = 1 * memory.MB + config.Console.UsageLimits.Storage.Free = 1 * memory.MB + config.Console.UsageLimits.Bandwidth.Free = 1 * memory.MB }, }, }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { @@ -632,8 +632,8 @@ func TestProjectUsageBandwidthResetAfter3days(t *testing.T) { SatelliteCount: 1, StorageNodeCount: 4, UplinkCount: 1, Reconfigure: testplanet.Reconfigure{ Satellite: func(log *zap.Logger, index int, config *satellite.Config) { - config.Console.UsageLimits.DefaultStorageLimit = 1 * memory.MB - config.Console.UsageLimits.DefaultBandwidthLimit = 1 * memory.MB + config.Console.UsageLimits.Storage.Free = 1 * memory.MB + config.Console.UsageLimits.Bandwidth.Free = 1 * memory.MB config.LiveAccounting.AsOfSystemInterval = -time.Millisecond }, }, diff --git a/satellite/api.go b/satellite/api.go index 38ceec989..d06b0b69d 100644 --- a/satellite/api.go +++ b/satellite/api.go @@ -292,8 +292,8 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB, { // setup project limits peer.ProjectLimits.Cache = accounting.NewProjectLimitCache(peer.DB.ProjectAccounting(), - config.Console.Config.UsageLimits.DefaultStorageLimit, - config.Console.Config.UsageLimits.DefaultBandwidthLimit, + config.Console.Config.UsageLimits.Storage.Free, + config.Console.Config.UsageLimits.Bandwidth.Free, config.ProjectLimit, ) } diff --git a/satellite/console/projects.go b/satellite/console/projects.go index 8034b5f81..cf84f3566 100644 --- a/satellite/console/projects.go +++ b/satellite/console/projects.go @@ -48,8 +48,20 @@ type Projects interface { // UsageLimitsConfig is a configuration struct for default per-project usage limits. type UsageLimitsConfig struct { - DefaultStorageLimit memory.Size `help:"the default storage usage limit" default:"50.00GB" testDefault:"25.00 GB"` - DefaultBandwidthLimit memory.Size `help:"the default bandwidth usage limit" default:"50.00GB" testDefault:"25.00 GB"` + Storage StorageLimitConfig + Bandwidth BandwidthLimitConfig +} + +// StorageLimitConfig is a configuration struct for default storage per-project usage limits. +type StorageLimitConfig struct { + Free memory.Size `help:"the default free-tier storage usage limit" default:"50.00GB" testDefault:"25.00 GB"` + Paid memory.Size `help:"the default paid-tier storage usage limit" default:"25.00TB" testDefault:"25.00 GB"` +} + +// BandwidthLimitConfig is a configuration struct for default bandwidth per-project usage limits. +type BandwidthLimitConfig struct { + Free memory.Size `help:"the default free-tier bandwidth usage limit" default:"50.00GB" testDefault:"25.00 GB"` + Paid memory.Size `help:"the default paid-tier bandwidth usage limit" default:"100.00TB" testDefault:"25.00 GB"` } // Project is a database object that describes Project entity. diff --git a/satellite/console/service.go b/satellite/console/service.go index bc916b71c..1b16739f8 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -21,6 +21,7 @@ import ( "golang.org/x/crypto/bcrypt" "storj.io/common/macaroon" + "storj.io/common/memory" "storj.io/common/storj" "storj.io/common/uuid" "storj.io/private/cfgstruct" @@ -244,6 +245,33 @@ func (paymentService PaymentsService) AddCreditCard(ctx context.Context, creditC return Error.Wrap(err) } + if !auth.User.PaidTier { + // put this user into the paid tier and convert projects to upgraded limits. + err = paymentService.service.store.Users().UpdatePaidTier(ctx, auth.User.ID, true) + if err != nil { + return Error.Wrap(err) + } + + projects, err := paymentService.service.store.Projects().GetOwn(ctx, auth.User.ID) + if err != nil { + return Error.Wrap(err) + } + for _, project := range projects { + if project.StorageLimit == nil || *project.StorageLimit < paymentService.service.config.UsageLimits.Storage.Paid { + project.StorageLimit = new(memory.Size) + *project.StorageLimit = paymentService.service.config.UsageLimits.Storage.Paid + } + if project.BandwidthLimit == nil || *project.BandwidthLimit < paymentService.service.config.UsageLimits.Bandwidth.Paid { + project.BandwidthLimit = new(memory.Size) + *project.BandwidthLimit = paymentService.service.config.UsageLimits.Bandwidth.Paid + } + err = paymentService.service.store.Projects().Update(ctx, &project) + if err != nil { + return Error.Wrap(err) + } + } + } + return nil } @@ -979,14 +1007,20 @@ func (s *Service) CreateProject(ctx context.Context, projectInfo ProjectInfo) (p var projectID uuid.UUID err = s.store.WithTx(ctx, func(ctx context.Context, tx DBTx) error { + storageLimit := s.config.UsageLimits.Storage.Free + bandwidthLimit := s.config.UsageLimits.Bandwidth.Free + if auth.User.PaidTier { + storageLimit = s.config.UsageLimits.Storage.Paid + bandwidthLimit = s.config.UsageLimits.Bandwidth.Paid + } p, err = tx.Projects().Insert(ctx, &Project{ Description: projectInfo.Description, Name: projectInfo.Name, OwnerID: auth.User.ID, PartnerID: auth.User.PartnerID, - StorageLimit: &s.config.UsageLimits.DefaultStorageLimit, - BandwidthLimit: &s.config.UsageLimits.DefaultBandwidthLimit, + StorageLimit: &storageLimit, + BandwidthLimit: &bandwidthLimit, }, ) if err != nil { diff --git a/satellite/console/service_test.go b/satellite/console/service_test.go index 741c1d358..0fdce8ae8 100644 --- a/satellite/console/service_test.go +++ b/satellite/console/service_test.go @@ -7,13 +7,16 @@ import ( "testing" "github.com/stretchr/testify/require" + "go.uber.org/zap" "storj.io/common/macaroon" + "storj.io/common/memory" "storj.io/common/storj" "storj.io/common/testcontext" "storj.io/common/testrand" "storj.io/common/uuid" "storj.io/storj/private/testplanet" + "storj.io/storj/satellite" "storj.io/storj/satellite/console" ) @@ -211,3 +214,67 @@ func TestService(t *testing.T) { }) }) } + +func TestPaidTier(t *testing.T) { + usageConfig := console.UsageLimitsConfig{ + Storage: console.StorageLimitConfig{ + Free: memory.GB, + Paid: memory.TB, + }, + Bandwidth: console.BandwidthLimitConfig{ + Free: 2 * memory.GB, + Paid: 2 * memory.TB, + }, + } + + testplanet.Run(t, testplanet.Config{ + SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1, + Reconfigure: testplanet.Reconfigure{ + Satellite: func(log *zap.Logger, index int, config *satellite.Config) { + config.Console.UsageLimits = usageConfig + }, + }, + }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { + sat := planet.Satellites[0] + service := sat.API.Console.Service + + // project should have free tier usage limits + proj1, err := sat.API.DB.Console().Projects().Get(ctx, planet.Uplinks[0].Projects[0].ID) + require.NoError(t, err) + require.Equal(t, usageConfig.Storage.Free, *proj1.StorageLimit) + require.Equal(t, usageConfig.Bandwidth.Free, *proj1.BandwidthLimit) + + // user should be in free tier + user, err := service.GetUser(ctx, proj1.OwnerID) + require.NoError(t, err) + require.False(t, user.PaidTier) + + authCtx, err := sat.AuthenticatedContext(ctx, user.ID) + require.NoError(t, err) + + // add a credit card to the user + err = service.Payments().AddCreditCard(authCtx, "test-cc-token") + require.NoError(t, err) + + // expect user to be in paid tier + user, err = service.GetUser(ctx, user.ID) + require.NoError(t, err) + require.True(t, user.PaidTier) + + // update auth ctx + authCtx, err = sat.AuthenticatedContext(ctx, user.ID) + require.NoError(t, err) + + // expect project to be migrated to paid tier usage limits + proj1, err = service.GetProject(authCtx, proj1.ID) + require.NoError(t, err) + require.Equal(t, usageConfig.Storage.Paid, *proj1.StorageLimit) + require.Equal(t, usageConfig.Bandwidth.Paid, *proj1.BandwidthLimit) + + // expect new project to be created with paid tier usage limits + proj2, err := service.CreateProject(authCtx, console.ProjectInfo{Name: "Project 2"}) + require.NoError(t, err) + require.Equal(t, usageConfig.Storage.Paid, *proj2.StorageLimit) + require.Equal(t, usageConfig.Bandwidth.Paid, *proj2.BandwidthLimit) + }) +} diff --git a/satellite/console/users.go b/satellite/console/users.go index fa71d2430..a15b38bb5 100644 --- a/satellite/console/users.go +++ b/satellite/console/users.go @@ -25,6 +25,8 @@ type Users interface { Delete(ctx context.Context, id uuid.UUID) error // Update is a method for updating user entity. Update(ctx context.Context, user *User) error + // UpdatePaidTier sets whether the user is in the paid tier. + UpdatePaidTier(ctx context.Context, id uuid.UUID, paidTier bool) error // GetProjectLimit is a method to get the users project limit GetProjectLimit(ctx context.Context, id uuid.UUID) (limit int, err error) } diff --git a/satellite/console/users_test.go b/satellite/console/users_test.go index 4fa194fb0..d17e9e734 100644 --- a/satellite/console/users_test.go +++ b/satellite/console/users_test.go @@ -9,6 +9,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "storj.io/common/testcontext" "storj.io/common/testrand" @@ -116,6 +117,47 @@ func TestUserEmailCase(t *testing.T) { }) } +func TestUserUpdatePaidTier(t *testing.T) { + satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) { + email := "testemail@mail.test" + fullName := "first name last name" + shortName := "short name" + password := "password" + newUser := &console.User{ + ID: testrand.UUID(), + FullName: fullName, + ShortName: shortName, + Email: email, + Status: console.Active, + PasswordHash: []byte(password), + } + + createdUser, err := db.Console().Users().Insert(ctx, newUser) + require.NoError(t, err) + require.Equal(t, email, createdUser.Email) + require.Equal(t, fullName, createdUser.FullName) + require.Equal(t, shortName, createdUser.ShortName) + require.False(t, createdUser.PaidTier) + + err = db.Console().Users().UpdatePaidTier(ctx, createdUser.ID, true) + require.NoError(t, err) + + retrievedUser, err := db.Console().Users().Get(ctx, createdUser.ID) + require.NoError(t, err) + require.Equal(t, email, retrievedUser.Email) + require.Equal(t, fullName, retrievedUser.FullName) + require.Equal(t, shortName, retrievedUser.ShortName) + require.True(t, retrievedUser.PaidTier) + + err = db.Console().Users().UpdatePaidTier(ctx, createdUser.ID, false) + require.NoError(t, err) + + retrievedUser, err = db.Console().Users().Get(ctx, createdUser.ID) + require.NoError(t, err) + require.False(t, retrievedUser.PaidTier) + }) +} + func testUsers(ctx context.Context, t *testing.T, repository console.Users, user *console.User) { t.Run("User insertion success", func(t *testing.T) { diff --git a/satellite/satellitedb/projects.go b/satellite/satellitedb/projects.go index 3ed3c2398..c08f845d3 100644 --- a/satellite/satellitedb/projects.go +++ b/satellite/satellitedb/projects.go @@ -139,6 +139,12 @@ func (projects *projects) Update(ctx context.Context, project *console.Project) Description: dbx.Project_Description(project.Description), RateLimit: dbx.Project_RateLimit_Raw(project.RateLimit), } + if project.StorageLimit != nil { + updateFields.UsageLimit = dbx.Project_UsageLimit(project.StorageLimit.Int64()) + } + if project.BandwidthLimit != nil { + updateFields.BandwidthLimit = dbx.Project_BandwidthLimit(project.BandwidthLimit.Int64()) + } _, err = projects.db.Update_Project_By_Id(ctx, dbx.Project_Id(project.ID[:]), diff --git a/satellite/satellitedb/users.go b/satellite/satellitedb/users.go index 757e4c7e0..e8b7c590a 100644 --- a/satellite/satellitedb/users.go +++ b/satellite/satellitedb/users.go @@ -108,6 +108,21 @@ func (users *users) Update(ctx context.Context, user *console.User) (err error) return err } +// UpdatePaidTier sets whether the user is in the paid tier. +func (users *users) UpdatePaidTier(ctx context.Context, id uuid.UUID, paidTier bool) (err error) { + defer mon.Task()(&ctx)(&err) + + _, err = users.db.Update_User_By_Id( + ctx, + dbx.User_Id(id[:]), + dbx.User_Update_Fields{ + PaidTier: dbx.User_PaidTier(paidTier), + }, + ) + + return err +} + // GetProjectLimit is a method to get the users project limit. func (users *users) GetProjectLimit(ctx context.Context, id uuid.UUID) (limit int, err error) { defer mon.Task()(&ctx)(&err) diff --git a/scripts/testdata/satellite-config.yaml.lock b/scripts/testdata/satellite-config.yaml.lock index fcfe8be59..5e0080fce 100755 --- a/scripts/testdata/satellite-config.yaml.lock +++ b/scripts/testdata/satellite-config.yaml.lock @@ -163,11 +163,17 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0 # url link to terms and conditions page # console.terms-and-conditions-url: https://storj.io/storage-sla/ -# the default bandwidth usage limit -# console.usage-limits.default-bandwidth-limit: 50.00 GB +# the default free-tier bandwidth usage limit +# console.usage-limits.bandwidth.free: 50.00 GB -# the default storage usage limit -# console.usage-limits.default-storage-limit: 50.00 GB +# the default paid-tier bandwidth usage limit +# console.usage-limits.bandwidth.paid: 100.00 TB + +# the default free-tier storage usage limit +# console.usage-limits.storage.free: 50.00 GB + +# the default paid-tier storage usage limit +# console.usage-limits.storage.paid: 25.00 TB # the public address of the node, useful for nodes behind NAT contact.external-address: ""