diff --git a/satellite/console/consoleweb/consoleapi/buckets.go b/satellite/console/consoleweb/consoleapi/buckets.go index 45ef74b29df6..55e00eb256b0 100644 --- a/satellite/console/consoleweb/consoleapi/buckets.go +++ b/satellite/console/consoleweb/consoleapi/buckets.go @@ -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() diff --git a/satellite/console/consoleweb/consoleapi/buckets_test.go b/satellite/console/consoleweb/consoleapi/buckets_test.go index 592a559ce153..5243c2d4a8c9 100644 --- a/satellite/console/consoleweb/consoleapi/buckets_test.go +++ b/satellite/console/consoleweb/consoleapi/buckets_test.go @@ -5,6 +5,7 @@ package consoleapi_test import ( "encoding/json" + "fmt" "net/http" "testing" @@ -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) { @@ -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()) + }) +} diff --git a/satellite/console/consoleweb/server.go b/satellite/console/consoleweb/server.go index 28f40cc45a85..1761924c3d0b 100644 --- a/satellite/console/consoleweb/server.go +++ b/satellite/console/consoleweb/server.go @@ -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) diff --git a/satellite/console/placements.go b/satellite/console/placements.go index f1fc7724cbd4..3c315d0480ec 100644 --- a/satellite/console/placements.go +++ b/satellite/console/placements.go @@ -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"` } diff --git a/satellite/console/service.go b/satellite/console/service.go index da4bf6b126d2..c5282d1dd508 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -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 diff --git a/satellite/console/service_test.go b/satellite/console/service_test.go index 0f0ca7b827e3..19d0e27b437f 100644 --- a/satellite/console/service_test.go +++ b/satellite/console/service_test.go @@ -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)