Skip to content

Commit

Permalink
satellite/console: add endpoint to get bucket placements
Browse files Browse the repository at this point in the history
add endpoint to get all bucket names and placements, so they can be used
in the UI.

issue: #6685

Change-Id: Ie80f513a5dd80ebfec53ff9f942b004f1e36f2a8
  • Loading branch information
cam-a authored and Storj Robot committed Feb 6, 2024
1 parent 003ce95 commit d380c21
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 2 deletions.
46 changes: 46 additions & 0 deletions satellite/console/consoleweb/consoleapi/buckets.go
Expand Up @@ -89,6 +89,52 @@ func (b *Buckets) AllBucketNames(w http.ResponseWriter, r *http.Request) {
}
}

// GetBucketPlacements returns all bucket names and placements for a specific project.
func (b *Buckets) GetBucketPlacements(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)

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

projectIDString := r.URL.Query().Get("projectID")
publicIDString := r.URL.Query().Get("publicID")

var projectID uuid.UUID
if projectIDString != "" {
projectID, err = uuid.FromString(projectIDString)
if err != nil {
b.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
} else if publicIDString != "" {
projectID, err = uuid.FromString(publicIDString)
if err != nil {
b.serveJSONError(ctx, w, http.StatusBadRequest, err)
return
}
} else {
b.serveJSONError(ctx, w, http.StatusBadRequest, errs.New("Project ID was not provided."))
return
}

bucketNames, err := b.service.GetBucketPlacements(ctx, projectID)
if err != nil {
if console.ErrUnauthorized.Has(err) {
b.serveJSONError(ctx, w, http.StatusUnauthorized, err)
return
}

b.serveJSONError(ctx, w, http.StatusInternalServerError, err)
return
}

err = json.NewEncoder(w).Encode(bucketNames)
if err != nil {
b.log.Error("failed to write json all bucket names response", zap.Error(ErrBucketsAPI.Wrap(err)))
}
}

// GetBucketTotals returns a page of bucket usage totals since project creation.
func (b *Buckets) GetBucketTotals(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
Expand Down
84 changes: 84 additions & 0 deletions satellite/console/consoleweb/consoleapi/buckets_test.go
Expand Up @@ -5,6 +5,7 @@ package consoleapi_test

import (
"encoding/json"
"fmt"
"net/http"
"testing"

Expand All @@ -17,6 +18,7 @@ import (
"storj.io/storj/satellite"
"storj.io/storj/satellite/buckets"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/nodeselection"
)

func Test_AllBucketNames(t *testing.T) {
Expand Down Expand Up @@ -82,3 +84,85 @@ func Test_AllBucketNames(t *testing.T) {
testRequest("?publicID=" + project.PublicID.String())
})
}

func Test_BucketPlacements(t *testing.T) {
placements := make(map[int]string)
for i := 0; i < 2; i++ {
placements[i] = fmt.Sprintf("loc-%d", i)
}
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.OpenRegistrationEnabled = true
config.Console.RateLimit.Burst = 10
var plcStr string
for k, v := range placements {
plcStr += fmt.Sprintf(`%d:annotation("location", "%s"); `, k, v)
}
config.Placement = nodeselection.ConfigurablePlacementRule{PlacementRules: plcStr}
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]

newUser := console.CreateUser{
FullName: "Jack-bucket",
ShortName: "",
Email: "bucketest@test.test",
}

user, err := sat.AddUser(ctx, newUser, 1)
require.NoError(t, err)

project, err := sat.AddProject(ctx, user.ID, "buckettest")
require.NoError(t, err)

bucket1 := buckets.Bucket{
ID: testrand.UUID(),
Name: "testBucket1",
ProjectID: project.ID,
Placement: 0,
}

bucket2 := buckets.Bucket{
ID: testrand.UUID(),
Name: "testBucket2",
ProjectID: project.ID,
Placement: 1,
}

_, err = sat.API.Buckets.Service.CreateBucket(ctx, bucket1)
require.NoError(t, err)

_, err = sat.API.Buckets.Service.CreateBucket(ctx, bucket2)
require.NoError(t, err)

testRequest := func(endpointSuffix string) {
body, status, err := doRequestWithAuth(ctx, t, sat, user, http.MethodGet, "buckets/bucket-placements"+endpointSuffix, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, status)

var output []console.BucketPlacement

err = json.Unmarshal(body, &output)
require.NoError(t, err)

require.Equal(t, bucket1.Name, output[0].Name)
require.Equal(t, bucket1.Placement, output[0].Placement.DefaultPlacement)
require.NotEqual(t, "", output[0].Placement.Location)
require.Equal(t, placements[0], output[0].Placement.Location)

require.Equal(t, bucket2.Name, output[1].Name)
require.Equal(t, bucket2.Placement, output[1].Placement.DefaultPlacement)
require.NotEqual(t, "", output[1].Placement.Location)
require.Equal(t, placements[1], output[1].Placement.Location)
}

// test using Project.ID
testRequest("?projectID=" + project.ID.String())

// test using Project.PublicID
testRequest("?publicID=" + project.PublicID.String())
})
}
1 change: 1 addition & 0 deletions satellite/console/consoleweb/server.go
Expand Up @@ -363,6 +363,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
bucketsRouter.Use(server.withCORS)
bucketsRouter.Use(server.withAuth)
bucketsRouter.HandleFunc("/bucket-names", bucketsController.AllBucketNames).Methods(http.MethodGet, http.MethodOptions)
bucketsRouter.HandleFunc("/bucket-placements", bucketsController.GetBucketPlacements).Methods(http.MethodGet, http.MethodOptions)
bucketsRouter.HandleFunc("/usage-totals", bucketsController.GetBucketTotals).Methods(http.MethodGet, http.MethodOptions)

apiKeysController := consoleapi.NewAPIKeys(logger, service)
Expand Down
10 changes: 8 additions & 2 deletions satellite/console/placements.go
Expand Up @@ -9,6 +9,12 @@ import (

// Placement contains placement info.
type Placement struct {
ID storj.PlacementConstraint `json:"id"`
Location string `json:"location"`
DefaultPlacement storj.PlacementConstraint `json:"defaultPlacement"`
Location string `json:"location"`
}

// BucketPlacement contains bucket name and placement info.
type BucketPlacement struct {
Name string `json:"name"`
Placement Placement `json:"placement"`
}
42 changes: 42 additions & 0 deletions satellite/console/service.go
Expand Up @@ -2869,6 +2869,48 @@ func (s *Service) GetAllBucketNames(ctx context.Context, projectID uuid.UUID) (_
return list, nil
}

// GetBucketPlacements retrieves all bucket names and placements of a specific project.
// projectID here may be Project.ID or Project.PublicID.
func (s *Service) GetBucketPlacements(ctx context.Context, projectID uuid.UUID) (_ []BucketPlacement, err error) {
defer mon.Task()(&ctx)(&err)

user, err := s.getUserAndAuditLog(ctx, "get all bucket names and placements", zap.String("projectID", projectID.String()))
if err != nil {
return nil, Error.Wrap(err)
}

isMember, err := s.isProjectMember(ctx, user.ID, projectID)
if err != nil {
return nil, ErrUnauthorized.Wrap(err)
}

listOptions := buckets.ListOptions{
Direction: buckets.DirectionForward,
}

allowedBuckets := macaroon.AllowedBuckets{
All: true,
}

bucketsList, err := s.buckets.ListBuckets(ctx, isMember.project.ID, listOptions, allowedBuckets)
if err != nil {
return nil, Error.Wrap(err)
}

var list []BucketPlacement
for _, bucket := range bucketsList.Items {
list = append(list, BucketPlacement{
bucket.Name,
Placement{
DefaultPlacement: bucket.Placement,
Location: s.placements[bucket.Placement].Name,
},
})
}

return list, nil
}

// GetUsageReport retrieves usage rollups for every bucket of a single or all the user owned projects for a given period.
func (s *Service) GetUsageReport(ctx context.Context, since, before time.Time, projectID uuid.UUID) ([]accounting.ProjectReportItem, error) {
var err error
Expand Down
22 changes: 22 additions & 0 deletions satellite/console/service_test.go
Expand Up @@ -683,6 +683,28 @@ func TestService(t *testing.T) {
}
})

t.Run("GetBucketPlacements", func(t *testing.T) {
list, err := sat.DB.Buckets().ListBuckets(ctx, up2Proj.ID, buckets.ListOptions{Direction: buckets.DirectionForward}, macaroon.AllowedBuckets{All: true})
require.NoError(t, err)
bp, err := service.GetBucketPlacements(userCtx2, up2Proj.ID)
require.NoError(t, err)
for _, b := range bp {
var found bool
for _, item := range list.Items {
if item.Name == b.Name {
found = true
require.Equal(t, item.Placement, b.Placement.DefaultPlacement)
require.Equal(t, placements[int(item.Placement)], b.Placement.Location)
break
}
}
if found {
continue
}
require.Fail(t, "bucket name not in list", b.Name)
}
})

t.Run("DeleteAPIKeyByNameAndProjectID", func(t *testing.T) {
secret, err := macaroon.NewSecret()
require.NoError(t, err)
Expand Down

0 comments on commit d380c21

Please sign in to comment.