Skip to content

Commit 6044d89

Browse files
wilfred-asomaniiStorj Robot
authored andcommitted
satellite/admin: add project buckets endpoint
This new endpoint will return a list of a project's buckets. Issue: #7660 Change-Id: Icc9ef6e7ea392f493a1df32dd92305f1f17f04ce
1 parent 4102447 commit 6044d89

File tree

11 files changed

+461
-6
lines changed

11 files changed

+461
-6
lines changed

satellite/accounting/db.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ type ProjectUsageByDay struct {
163163
type BucketUsage struct {
164164
ProjectID uuid.UUID `json:"projectID"`
165165
BucketName string `json:"bucketName"`
166+
UserAgent []byte `json:"-"`
166167

167168
DefaultPlacement storj.PlacementConstraint `json:"defaultPlacement"`
168169
Location string `json:"location"`

satellite/admin/back-office/api-docs.gen.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
* ProjectManagement
2424
* [Get project statuses](#projectmanagement-get-project-statuses)
2525
* [Get project](#projectmanagement-get-project)
26+
* [Get project buckets](#projectmanagement-get-project-buckets)
2627
* [Update project](#projectmanagement-update-project)
2728
* [Update project limits](#projectmanagement-update-project-limits)
2829
* Search
@@ -588,6 +589,54 @@ Gets project by ID
588589

589590
```
590591

592+
<h3 id='projectmanagement-get-project-buckets'>Get project buckets (<a href='#list-of-endpoints'>go to full list</a>)</h3>
593+
594+
Gets a project's buckets
595+
596+
`GET /back-office/api/v1/projects/{publicID}/buckets`
597+
598+
**Query Params:**
599+
600+
| name | type | elaboration |
601+
|---|---|---|
602+
| `search` | `string` | |
603+
| `page` | `string` | |
604+
| `limit` | `string` | |
605+
| `since` | `string` | Date timestamp formatted as `2006-01-02T15:00:00Z` |
606+
| `before` | `string` | Date timestamp formatted as `2006-01-02T15:00:00Z` |
607+
608+
**Path Params:**
609+
610+
| name | type | elaboration |
611+
|---|---|---|
612+
| `publicID` | `string` | UUID formatted as `00000000-0000-0000-0000-000000000000` |
613+
614+
**Response body:**
615+
616+
```typescript
617+
{
618+
items: [
619+
{
620+
name: string
621+
userAgent: string
622+
placement: string
623+
storage: number
624+
egress: number
625+
segmentCount: number
626+
createdAt: string // Date timestamp formatted as `2006-01-02T15:00:00Z`
627+
}
628+
629+
]
630+
631+
limit: number
632+
offset: number
633+
pageCount: number
634+
currentPage: number
635+
totalCount: number
636+
}
637+
638+
```
639+
591640
<h3 id='projectmanagement-update-project'>Update project (<a href='#list-of-endpoints'>go to full list</a>)</h3>
592641

593642
Updates project name, user agent and default placement by ID
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (C) 2025 Storj Labs, Inc.
2+
// See LICENSE for copying information.
3+
4+
package admin
5+
6+
import (
7+
"context"
8+
"database/sql"
9+
"errors"
10+
"net/http"
11+
"strconv"
12+
"time"
13+
14+
"github.com/zeebo/errs"
15+
16+
"storj.io/common/storj"
17+
"storj.io/common/uuid"
18+
"storj.io/storj/private/api"
19+
"storj.io/storj/satellite/accounting"
20+
)
21+
22+
// BucketInfo contains information about a bucket.
23+
type BucketInfo struct {
24+
Name string `json:"name"`
25+
UserAgent string `json:"userAgent"`
26+
Placement string `json:"placement"`
27+
Storage float64 `json:"storage"`
28+
Egress float64 `json:"egress"`
29+
SegmentCount int64 `json:"segmentCount"`
30+
CreatedAt time.Time `json:"createdAt"`
31+
}
32+
33+
// BucketInfoPage contains a paginated list of buckets.
34+
type BucketInfoPage struct {
35+
Items []BucketInfo `json:"items"`
36+
37+
Limit uint `json:"limit"`
38+
Offset uint64 `json:"offset"`
39+
40+
PageCount uint `json:"pageCount"`
41+
CurrentPage uint `json:"currentPage"`
42+
TotalCount uint64 `json:"totalCount"`
43+
}
44+
45+
// GetProjectBuckets retrieves all buckets for a given project public ID.
46+
func (s *Service) GetProjectBuckets(ctx context.Context, publicID uuid.UUID, search, pageStr, limitStr string, since, before time.Time) (*BucketInfoPage, api.HTTPError) {
47+
var err error
48+
defer mon.Task()(&ctx)(&err)
49+
50+
project, err := s.consoleDB.Projects().GetByPublicID(ctx, publicID)
51+
if err != nil {
52+
status := http.StatusInternalServerError
53+
if errors.Is(err, sql.ErrNoRows) {
54+
status = http.StatusNotFound
55+
err = errs.New("project not found")
56+
}
57+
return nil, api.HTTPError{
58+
Status: status,
59+
Err: Error.Wrap(err),
60+
}
61+
}
62+
// convert page and limit to uint
63+
limit, err := strconv.ParseUint(limitStr, 10, 32)
64+
if err != nil {
65+
return nil, api.HTTPError{
66+
Status: http.StatusBadRequest,
67+
Err: Error.New("invalid limit"),
68+
}
69+
}
70+
if limit == 0 || limit > 100 {
71+
limit = 100
72+
}
73+
74+
page, err := strconv.ParseUint(pageStr, 10, 32)
75+
if err != nil {
76+
return nil, api.HTTPError{
77+
Status: http.StatusBadRequest,
78+
Err: Error.New("invalid page"),
79+
}
80+
}
81+
if page == 0 {
82+
page = 1
83+
}
84+
85+
if search == "-" {
86+
// to avoid the gen API requiring that
87+
// a parameter be non-empty.
88+
search = ""
89+
}
90+
cursor := accounting.BucketUsageCursor{
91+
Search: search,
92+
Limit: uint(limit),
93+
Page: uint(page),
94+
}
95+
bucketPage, err := s.accountingDB.GetBucketTotals(ctx, project.ID, cursor, since, before)
96+
if err != nil {
97+
return nil, api.HTTPError{
98+
Status: http.StatusInternalServerError,
99+
Err: Error.Wrap(err),
100+
}
101+
}
102+
103+
infoPage := &BucketInfoPage{
104+
Items: make([]BucketInfo, len(bucketPage.BucketUsages)),
105+
Limit: uint(limit),
106+
Offset: bucketPage.Offset,
107+
PageCount: bucketPage.PageCount,
108+
CurrentPage: bucketPage.CurrentPage,
109+
TotalCount: bucketPage.TotalCount,
110+
}
111+
112+
if len(bucketPage.BucketUsages) == 0 {
113+
return infoPage, api.HTTPError{}
114+
}
115+
116+
getPlacementName := func(pc storj.PlacementConstraint) string {
117+
for id, p := range s.placement {
118+
if id == pc {
119+
return p.Name
120+
}
121+
}
122+
return "unknown placement"
123+
}
124+
125+
for i, bucket := range bucketPage.BucketUsages {
126+
infoPage.Items[i] = BucketInfo{
127+
Name: bucket.BucketName,
128+
UserAgent: string(bucket.UserAgent),
129+
Placement: getPlacementName(bucket.DefaultPlacement),
130+
Storage: bucket.Storage,
131+
Egress: bucket.Egress,
132+
SegmentCount: bucket.SegmentCount,
133+
CreatedAt: bucket.CreatedAt,
134+
}
135+
}
136+
137+
return infoPage, api.HTTPError{}
138+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright (C) 2025 Storj Labs, Inc.
2+
// See LICENSE for copying information.
3+
4+
package admin_test
5+
6+
import (
7+
"net/http"
8+
"testing"
9+
"time"
10+
11+
"github.com/stretchr/testify/require"
12+
13+
"storj.io/common/storj"
14+
"storj.io/common/testcontext"
15+
"storj.io/common/testrand"
16+
"storj.io/storj/private/testplanet"
17+
backoffice "storj.io/storj/satellite/admin/back-office"
18+
"storj.io/storj/satellite/buckets"
19+
"storj.io/storj/satellite/console"
20+
)
21+
22+
func TestGetProjectBuckets(t *testing.T) {
23+
testplanet.Run(t, testplanet.Config{
24+
SatelliteCount: 1,
25+
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
26+
sat := planet.Satellites[0]
27+
service := sat.Admin.Admin.Service
28+
bucketsDB := sat.DB.Buckets()
29+
30+
user, err := sat.AddUser(ctx, console.CreateUser{
31+
FullName: "Test User",
32+
Email: "test@test.test",
33+
}, 1)
34+
require.NoError(t, err)
35+
36+
project, err := sat.AddProject(ctx, user.ID, "test project")
37+
require.NoError(t, err)
38+
39+
now := time.Now()
40+
startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
41+
42+
_, apiErr := service.GetProjectBuckets(ctx, testrand.UUID(), "", "1", "100", startOfMonth, now)
43+
require.Error(t, apiErr.Err)
44+
require.Equal(t, http.StatusNotFound, apiErr.Status)
45+
46+
bucketsList, apiErr := service.GetProjectBuckets(ctx, project.PublicID, "", "1", "100", startOfMonth, now)
47+
require.NoError(t, apiErr.Err)
48+
require.Empty(t, bucketsList.Items)
49+
50+
// Create test buckets with different configurations
51+
bucket1 := buckets.Bucket{
52+
ID: testrand.UUID(),
53+
Name: "bucket1",
54+
ProjectID: project.ID,
55+
Placement: storj.DefaultPlacement,
56+
Versioning: buckets.Unversioned,
57+
UserAgent: []byte("test-agent-1"),
58+
ObjectLock: buckets.ObjectLockSettings{
59+
Enabled: false,
60+
},
61+
}
62+
63+
bucket2 := buckets.Bucket{
64+
ID: testrand.UUID(),
65+
Name: "bucket2",
66+
ProjectID: project.ID,
67+
Placement: storj.PlacementConstraint(10),
68+
Versioning: buckets.VersioningEnabled,
69+
UserAgent: []byte("test-agent-2"),
70+
ObjectLock: buckets.ObjectLockSettings{
71+
Enabled: true,
72+
DefaultRetentionMode: storj.GovernanceMode,
73+
DefaultRetentionDays: 30,
74+
},
75+
}
76+
77+
_, err = bucketsDB.CreateBucket(ctx, bucket1)
78+
require.NoError(t, err)
79+
80+
_, err = bucketsDB.CreateBucket(ctx, bucket2)
81+
require.NoError(t, err)
82+
83+
// Get buckets via service
84+
bucketsList, apiErr = service.GetProjectBuckets(ctx, project.PublicID, "", "1", "100", startOfMonth, now)
85+
require.NoError(t, apiErr.Err)
86+
require.Len(t, bucketsList.Items, 2)
87+
88+
// Verify bucket1
89+
bucket1Info := findBucketByName(bucketsList.Items, bucket1.Name)
90+
require.NotNil(t, bucket1Info)
91+
require.Equal(t, bucket1.Name, bucket1Info.Name)
92+
require.Equal(t, string(bucket1.UserAgent), bucket1Info.UserAgent)
93+
94+
// Verify bucket2
95+
bucket2Info := findBucketByName(bucketsList.Items, bucket2.Name)
96+
require.NotNil(t, bucket2Info)
97+
require.Equal(t, bucket2.Name, bucket2Info.Name)
98+
require.Equal(t, string(bucket2.UserAgent), bucket2Info.UserAgent)
99+
})
100+
}
101+
102+
// findBucketByName is a helper function to find a bucket by name in the list.
103+
func findBucketByName(buckets []backoffice.BucketInfo, name string) *backoffice.BucketInfo {
104+
for i := range buckets {
105+
if buckets[i].Name == name {
106+
return &buckets[i]
107+
}
108+
}
109+
return nil
110+
}

satellite/admin/back-office/gen/main.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"path"
1414
"path/filepath"
1515
"strings"
16+
"time"
1617

1718
"storj.io/common/uuid"
1819
"storj.io/storj/private/apigen"
@@ -244,6 +245,27 @@ func main() {
244245
},
245246
})
246247

248+
group.Get("/{publicID}/buckets", &apigen.Endpoint{
249+
Name: "Get project buckets",
250+
Description: "Gets a project's buckets",
251+
GoName: "GetProjectBuckets",
252+
TypeScriptName: "getProjectBuckets",
253+
PathParams: []apigen.Param{
254+
apigen.NewParam("publicID", uuid.UUID{}),
255+
},
256+
QueryParams: []apigen.Param{
257+
apigen.NewParam("search", ""),
258+
apigen.NewParam("page", ""),
259+
apigen.NewParam("limit", ""),
260+
apigen.NewParam("since", time.Time{}),
261+
apigen.NewParam("before", time.Time{}),
262+
},
263+
Response: backoffice.BucketInfoPage{},
264+
Settings: map[any]any{
265+
authPermsKey: []backoffice.Permission{backoffice.PermProjectView},
266+
},
267+
})
268+
247269
group.Patch("/{publicID}", &apigen.Endpoint{
248270
Name: "Update project",
249271
Description: "Updates project name, user agent and default placement by ID",

0 commit comments

Comments
 (0)