Skip to content

Commit

Permalink
admin/back-office: implement project limits update endpoint
Browse files Browse the repository at this point in the history
Implement PUT method on back-office admin API endpoint
/projects/limits/{publicID} to enable updating project max buckets,
storage, bandwidth, segment, rate, and burst limits.

Change-Id: I66ddbbf8dada4fdde4c3ac1953a271eedbc206b6
  • Loading branch information
cam-a authored and Storj Robot committed Jan 10, 2024
1 parent d0149a7 commit 3e57292
Show file tree
Hide file tree
Showing 6 changed files with 330 additions and 0 deletions.
27 changes: 27 additions & 0 deletions satellite/admin/back-office/api-docs.gen.md
Expand Up @@ -10,6 +10,7 @@
* [Get user](#usermanagement-get-user)
* ProjectManagement
* [Get project](#projectmanagement-get-project)
* [Update project limits](#projectmanagement-update-project-limits)

<h3 id='placementmanagement-get-placements'>Get placements (<a href='#list-of-endpoints'>go to full list</a>)</h3>

Expand Down Expand Up @@ -113,3 +114,29 @@ Gets project by ID

```

<h3 id='projectmanagement-update-project-limits'>Update project limits (<a href='#list-of-endpoints'>go to full list</a>)</h3>

Updates project limits by ID

`PUT /back-office/api/v1/projects/limits/{publicID}`

**Path Params:**

| name | type | elaboration |
|---|---|---|
| `publicID` | `string` | UUID formatted as `00000000-0000-0000-0000-000000000000` |

**Request body:**

```typescript
{
maxBuckets: number
storageLimit: number
bandwidthLimit: number
segmentLimit: number
rateLimit: number
burstLimit: number
}

```

14 changes: 14 additions & 0 deletions satellite/admin/back-office/gen/main.go
Expand Up @@ -71,6 +71,20 @@ func main() {
},
})

group.Put("/limits/{publicID}", &apigen.Endpoint{
Name: "Update project limits",
Description: "Updates project limits by ID",
GoName: "UpdateProjectLimits",
TypeScriptName: "updateProjectLimits",
PathParams: []apigen.Param{
apigen.NewParam("publicID", uuid.UUID{}),
},
Request: backoffice.ProjectLimitsUpdate{},
Settings: map[any]any{
authPermsKey: []backoffice.Permission{backoffice.PermProjectSetLimits},
},
})

modroot := findModuleRootDir()
api.MustWriteGo(filepath.Join(modroot, "satellite", "admin", "back-office", "handlers.gen.go"))
api.MustWriteTS(filepath.Join(modroot, "satellite", "admin", "back-office", "ui", "src", "api", "client.gen.ts"))
Expand Down
37 changes: 37 additions & 0 deletions satellite/admin/back-office/handlers.gen.go
Expand Up @@ -31,6 +31,7 @@ type UserManagementService interface {

type ProjectManagementService interface {
GetProject(ctx context.Context, publicID uuid.UUID) (*Project, api.HTTPError)
UpdateProjectLimits(ctx context.Context, publicID uuid.UUID, request ProjectLimitsUpdate) api.HTTPError
}

// PlacementManagementHandler is an api handler that implements all PlacementManagement API endpoints functionality.
Expand Down Expand Up @@ -93,6 +94,7 @@ func NewProjectManagement(log *zap.Logger, mon *monkit.Scope, service ProjectMan

projectsRouter := router.PathPrefix("/back-office/api/v1/projects").Subrouter()
projectsRouter.HandleFunc("/{publicID}", handler.handleGetProject).Methods("GET")
projectsRouter.HandleFunc("/limits/{publicID}", handler.handleUpdateProjectLimits).Methods("PUT")

return handler
}
Expand Down Expand Up @@ -179,3 +181,38 @@ func (h *ProjectManagementHandler) handleGetProject(w http.ResponseWriter, r *ht
h.log.Debug("failed to write json GetProject response", zap.Error(ErrProjectsAPI.Wrap(err)))
}
}

func (h *ProjectManagementHandler) handleUpdateProjectLimits(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer h.mon.Task()(&ctx)(&err)

w.Header().Set("Content-Type", "application/json")

publicIDParam, ok := mux.Vars(r)["publicID"]
if !ok {
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("missing publicID route param"))
return
}

publicID, err := uuid.FromString(publicIDParam)
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}

payload := ProjectLimitsUpdate{}
if err = json.NewDecoder(r.Body).Decode(&payload); err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}

if h.auth.IsRejected(w, r, 16384) {
return
}

httpErr := h.service.UpdateProjectLimits(ctx, publicID, payload)
if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
}
}
57 changes: 57 additions & 0 deletions satellite/admin/back-office/projects.go
Expand Up @@ -51,6 +51,16 @@ type ProjectUsageLimits[T ~int64 | *int64] struct {
SegmentUsed *int64 `json:"segmentUsed"`
}

// ProjectLimitsUpdate contains all limit values to be updated.
type ProjectLimitsUpdate struct {
MaxBuckets int `json:"maxBuckets"`
StorageLimit int64 `json:"storageLimit"`
BandwidthLimit int64 `json:"bandwidthLimit"`
SegmentLimit int64 `json:"segmentLimit"`
RateLimit int `json:"rateLimit"`
BurstLimit int `json:"burstLimit"`
}

// GetProject gets the project info.
func (s *Service) GetProject(ctx context.Context, id uuid.UUID) (*Project, api.HTTPError) {
var err error
Expand Down Expand Up @@ -251,3 +261,50 @@ func (s *Service) getProjectUsage(

return bandwidt, storage, segment, api.HTTPError{}
}

// UpdateProjectLimits updates the project's max buckets, storage, bandwidth, segment, rate, and burst limits.
func (s *Service) UpdateProjectLimits(ctx context.Context, id uuid.UUID, req ProjectLimitsUpdate) api.HTTPError {
var err error
defer mon.Task()(&ctx)(&err)

p, err := s.consoleDB.Projects().GetByPublicID(ctx, id)
if err != nil {
status := http.StatusInternalServerError
if errors.Is(err, sql.ErrNoRows) {
status = http.StatusNotFound
}
return api.HTTPError{
Status: status,
Err: Error.Wrap(err),
}
}

buckets := &req.MaxBuckets
if req.MaxBuckets == s.defaults.MaxBuckets {
buckets = nil
}
rate := &req.RateLimit
if req.RateLimit == s.defaults.RateLimit {
rate = nil
}
burst := &req.BurstLimit
if req.BurstLimit == s.defaults.RateLimit {
burst = nil
}

// Note: usage_limit (storage), bandwidth_limit, and segment_limit columns are also nullable, but are never actually null in production.

err = s.consoleDB.Projects().UpdateAllLimits(ctx, p.ID, &req.StorageLimit, &req.BandwidthLimit, &req.SegmentLimit, buckets, rate, burst)
if err != nil {
status := http.StatusInternalServerError
if errors.Is(err, sql.ErrNoRows) {
status = http.StatusNotFound
}
return api.HTTPError{
Status: status,
Err: Error.Wrap(err),
}
}

return api.HTTPError{}
}
176 changes: 176 additions & 0 deletions satellite/admin/back-office/projects_test.go
Expand Up @@ -12,11 +12,13 @@ import (
"github.com/stretchr/testify/require"
"go.uber.org/zap"

"storj.io/common/memory"
"storj.io/common/pb"
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/storj/private/testplanet"
"storj.io/storj/satellite"
admin "storj.io/storj/satellite/admin/back-office"
"storj.io/storj/satellite/buckets"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/metabase"
Expand Down Expand Up @@ -178,3 +180,177 @@ func TestGetProject(t *testing.T) {
})
})
}

func TestUpdateProjectLimits(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,
Reconfigure: testplanet.Reconfigure{
Satellite: func(_ *zap.Logger, _ int, config *satellite.Config) {
config.LiveAccounting.AsOfSystemInterval = 0
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
t.Run("unexisting project", func(t *testing.T) {
sat := planet.Satellites[0]

service := sat.Admin.Admin.Service
apiErr := service.UpdateProjectLimits(ctx, testrand.UUID(), admin.ProjectLimitsUpdate{})
require.Error(t, apiErr.Err)
assert.Equal(t, http.StatusNotFound, apiErr.Status)
})

t.Run("existing user", func(t *testing.T) {
consoleUser := &console.User{
ID: testrand.UUID(),
FullName: "Test User",
Email: "test@storj.io",
PasswordHash: testrand.Bytes(8),
Status: console.Inactive,
UserAgent: []byte("agent"),
DefaultPlacement: 5,
}

sat := planet.Satellites[0]
consoleDB := sat.DB.Console()
_, err := consoleDB.Users().Insert(ctx, consoleUser)
require.NoError(t, err)

consoleUser.Status = console.Active
require.NoError(
t,
consoleDB.Users().Update(ctx, consoleUser.ID, console.UpdateUserRequest{Status: &consoleUser.Status}),
)

// Project with default limits
projID := testrand.UUID()
storage := memory.Size(1000)
bw := memory.Size(2000)
segment := int64(10000)
rate := 500
burst := 200
buckets := 500
consoleProject := &console.Project{
ID: projID,
Name: "project-free-account",
Description: "This is a project created at the time that owner's user account is a free account",
OwnerID: consoleUser.ID,
StorageLimit: &storage,
BandwidthLimit: &bw,
SegmentLimit: &segment,
RateLimit: &rate,
MaxBuckets: &buckets,
}

consoleProject, err = consoleDB.Projects().Insert(ctx, consoleProject)
require.NoError(t, err)
// Insert doesn't set burst limit.
require.NoError(t, consoleDB.Projects().UpdateBurstLimit(ctx, projID, &burst))

projPublicID := consoleProject.PublicID

service := sat.Admin.Admin.Service
project, err := sat.API.DB.Console().Projects().Get(ctx, projID)
require.NoError(t, err)

require.NotNil(t, project.RateLimit)
assert.Equal(t, *consoleProject.RateLimit, *project.RateLimit)
require.NotNil(t, project.BurstLimit)
assert.Equal(t, burst, *project.BurstLimit)
require.NotNil(t, project.MaxBuckets)
assert.Equal(t, *consoleProject.MaxBuckets, *project.MaxBuckets)

assert.EqualValues(t, consoleProject.BandwidthLimit, project.BandwidthLimit)
assert.EqualValues(t, consoleProject.StorageLimit, project.StorageLimit)
assert.EqualValues(t, consoleProject.SegmentLimit, project.SegmentLimit)

// basic test
expectStorage := project.StorageLimit.Int64() * 2
expectBandwidth := project.BandwidthLimit.Int64() * 2
expectSegment := *project.SegmentLimit * 2
expectBuckets := 100
expectRate := 2000
expectBurst := 500
apiErr := service.UpdateProjectLimits(ctx, projPublicID, admin.ProjectLimitsUpdate{
MaxBuckets: expectBuckets,
StorageLimit: expectStorage,
BandwidthLimit: expectBandwidth,
SegmentLimit: expectSegment,
RateLimit: expectRate,
BurstLimit: expectBurst,
})
require.NoError(t, apiErr.Err)

project, err = sat.API.DB.Console().Projects().Get(ctx, projID)
require.NoError(t, err)

require.NotNil(t, project.MaxBuckets)
require.Equal(t, expectBuckets, *project.MaxBuckets)
require.NotNil(t, project.StorageLimit)
require.Equal(t, expectStorage, project.StorageLimit.Int64())
require.NotNil(t, project.BandwidthLimit)
require.Equal(t, expectBandwidth, project.BandwidthLimit.Int64())
require.NotNil(t, project.SegmentLimit)
require.Equal(t, expectSegment, *project.SegmentLimit)
require.NotNil(t, project.RateLimit)
require.Equal(t, expectRate, *project.RateLimit)
require.NotNil(t, project.BurstLimit)
require.Equal(t, expectBurst, *project.BurstLimit)

// test updating max buckets, rate, and burst limits to default value sets null in DB.
// Storage, BW, and segment are still set explicitly even if default.
defaultStorage := sat.Config.Console.UsageLimits.Storage.Free.Int64()
defaultBandwidth := sat.Config.Console.UsageLimits.Bandwidth.Free.Int64()
defaultSegment := sat.Config.Console.UsageLimits.Segment.Free

apiErr = service.UpdateProjectLimits(ctx, projPublicID, admin.ProjectLimitsUpdate{
MaxBuckets: sat.Config.Metainfo.ProjectLimits.MaxBuckets,
RateLimit: int(sat.Config.Metainfo.RateLimiter.Rate),
BurstLimit: int(sat.Config.Metainfo.RateLimiter.Rate),
StorageLimit: defaultStorage,
BandwidthLimit: defaultBandwidth,
SegmentLimit: defaultSegment,
})
require.NoError(t, apiErr.Err)

project, err = sat.API.DB.Console().Projects().Get(ctx, projID)
require.NoError(t, err)

require.Nil(t, project.MaxBuckets)
require.Nil(t, project.RateLimit)
require.Nil(t, project.BurstLimit)
require.NotNil(t, project.StorageLimit)
require.Equal(t, defaultStorage, project.StorageLimit.Int64())
require.NotNil(t, project.BandwidthLimit)
require.Equal(t, defaultBandwidth, project.BandwidthLimit.Int64())
require.NotNil(t, project.SegmentLimit)
require.Equal(t, defaultSegment, *project.SegmentLimit)

// test setting to zero.
apiErr = service.UpdateProjectLimits(ctx, projPublicID, admin.ProjectLimitsUpdate{
MaxBuckets: 0,
RateLimit: 0,
BurstLimit: 0,
StorageLimit: 0,
BandwidthLimit: 0,
SegmentLimit: 0,
})
require.NoError(t, apiErr.Err)

project, err = sat.API.DB.Console().Projects().Get(ctx, projID)
require.NoError(t, err)

require.NotNil(t, project.MaxBuckets)
require.Zero(t, *project.MaxBuckets)
require.NotNil(t, project.RateLimit)
require.Zero(t, *project.RateLimit)
require.NotNil(t, project.BurstLimit)
require.Zero(t, *project.BurstLimit)
require.NotNil(t, project.StorageLimit)
require.Zero(t, *project.StorageLimit)
require.NotNil(t, project.BandwidthLimit)
require.Zero(t, *project.BandwidthLimit)
require.NotNil(t, project.SegmentLimit)
require.Zero(t, *project.SegmentLimit)
})
})
}

0 comments on commit 3e57292

Please sign in to comment.