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
This commit is contained in:
Moby von Briesen 2021-07-01 01:13:45 +02:00
parent 3b0b0ba3c4
commit e36001b7cf
11 changed files with 200 additions and 16 deletions

View File

@ -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()})

View File

@ -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
},
},

View File

@ -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,
)
}

View File

@ -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.

View File

@ -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 {

View File

@ -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)
})
}

View File

@ -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)
}

View File

@ -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) {

View File

@ -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[:]),

View File

@ -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)

View File

@ -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: ""