diff --git a/satellite/admin.go b/satellite/admin.go index 53bc98eff..443f1d7a2 100644 --- a/satellite/admin.go +++ b/satellite/admin.go @@ -215,10 +215,28 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB, metabaseDB *m return nil, err } + placement, err := config.Placement.Parse() + if err != nil { + return nil, err + } + adminConfig := config.Admin adminConfig.AuthorizationToken = config.Console.AuthToken - peer.Admin.Server = admin.NewServer(log.Named("admin"), peer.Admin.Listener, peer.DB, peer.Buckets.Service, peer.REST.Keys, peer.FreezeAccounts.Service, peer.Analytics.Service, peer.Payments.Accounts, config.Console, adminConfig) + peer.Admin.Server = admin.NewServer( + log.Named("admin"), + peer.Admin.Listener, + peer.DB, + peer.Buckets.Service, + peer.REST.Keys, + peer.FreezeAccounts.Service, + peer.Analytics.Service, + peer.Payments.Accounts, + placement, + config.Console, + adminConfig, + ) + peer.Servers.Add(lifecycle.Item{ Name: "admin", Run: peer.Admin.Server.Run, diff --git a/satellite/admin/back-office/api-docs.gen.md b/satellite/admin/back-office/api-docs.gen.md index 2745decc6..cb5bf9871 100644 --- a/satellite/admin/back-office/api-docs.gen.md +++ b/satellite/admin/back-office/api-docs.gen.md @@ -6,20 +6,24 @@

List of Endpoints

-* Example - * [Get examples](#example-get-examples) +* PlacementManagement + * [Get placements](#placementmanagement-get-placements) -

Get examples (go to full list)

+

Get placements (go to full list)

-Get a list with the names of the all available examples +Gets placement rule IDs and their locations -`GET /back-office/api/v1/example/examples` +`GET /back-office/api/v1/placements/` **Response body:** ```typescript [ -string + { + id: number + location: string + } + ] ``` diff --git a/satellite/admin/back-office/gen/main.go b/satellite/admin/back-office/gen/main.go index 85a03ec2d..7ecf0d287 100644 --- a/satellite/admin/back-office/gen/main.go +++ b/satellite/admin/back-office/gen/main.go @@ -5,6 +5,8 @@ // source code of the API server handlers and clients and the documentation markdown document. package main +//go:generate go run $GOFILE + import ( "os" "path" @@ -22,24 +24,21 @@ func main() { BasePath: path.Join(backoffice.PathPrefix, "/api"), } - // This is an example and must be deleted when we define the first real endpoint. - group := api.Group("Example", "example") + group := api.Group("PlacementManagement", "placements") - group.Get("/examples", &apigen.Endpoint{ - Name: "Get examples", - Description: "Get a list with the names of the all available examples", - GoName: "GetExamples", - TypeScriptName: "getExamples", - Response: []string{}, - ResponseMock: []string{"example-1", "example-2", "example-3"}, - NoCookieAuth: false, - NoAPIAuth: false, + group.Get("/", &apigen.Endpoint{ + Name: "Get placements", + Description: "Gets placement rule IDs and their locations", + GoName: "GetPlacements", + TypeScriptName: "getPlacements", + Response: []backoffice.PlacementInfo{}, + NoCookieAuth: true, + NoAPIAuth: true, }) 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")) - api.MustWriteTSMock(filepath.Join(modroot, "satellite", "admin", "back-office", "ui", "src", "api", "client-mock.gen.ts")) api.MustWriteDocs(filepath.Join(modroot, "satellite", "admin", "back-office", "api-docs.gen.md")) } diff --git a/satellite/admin/back-office/handlers.gen.go b/satellite/admin/back-office/handlers.gen.go index 8e6702f94..5e6be6448 100644 --- a/satellite/admin/back-office/handlers.gen.go +++ b/satellite/admin/back-office/handlers.gen.go @@ -16,49 +16,42 @@ import ( "storj.io/storj/private/api" ) -var ErrExampleAPI = errs.Class("admin example api") +var ErrPlacementsAPI = errs.Class("admin placements api") -type ExampleService interface { - GetExamples(ctx context.Context) ([]string, api.HTTPError) +type PlacementManagementService interface { + GetPlacements(ctx context.Context) ([]PlacementInfo, api.HTTPError) } -// ExampleHandler is an api handler that implements all Example API endpoints functionality. -type ExampleHandler struct { +// PlacementManagementHandler is an api handler that implements all PlacementManagement API endpoints functionality. +type PlacementManagementHandler struct { log *zap.Logger mon *monkit.Scope - service ExampleService + service PlacementManagementService auth api.Auth } -func NewExample(log *zap.Logger, mon *monkit.Scope, service ExampleService, router *mux.Router, auth api.Auth) *ExampleHandler { - handler := &ExampleHandler{ +func NewPlacementManagement(log *zap.Logger, mon *monkit.Scope, service PlacementManagementService, router *mux.Router, auth api.Auth) *PlacementManagementHandler { + handler := &PlacementManagementHandler{ log: log, mon: mon, service: service, auth: auth, } - exampleRouter := router.PathPrefix("/back-office/api/v1/example").Subrouter() - exampleRouter.HandleFunc("/examples", handler.handleGetExamples).Methods("GET") + placementsRouter := router.PathPrefix("/back-office/api/v1/placements").Subrouter() + placementsRouter.HandleFunc("/", handler.handleGetPlacements).Methods("GET") return handler } -func (h *ExampleHandler) handleGetExamples(w http.ResponseWriter, r *http.Request) { +func (h *PlacementManagementHandler) handleGetPlacements(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") - ctx, err = h.auth.IsAuthenticated(ctx, r, true, true) - if err != nil { - h.auth.RemoveAuthCookie(w) - api.ServeError(h.log, w, http.StatusUnauthorized, err) - return - } - - retVal, httpErr := h.service.GetExamples(ctx) + retVal, httpErr := h.service.GetPlacements(ctx) if httpErr.Err != nil { api.ServeError(h.log, w, httpErr.Status, httpErr.Err) return @@ -66,6 +59,6 @@ func (h *ExampleHandler) handleGetExamples(w http.ResponseWriter, r *http.Reques err = json.NewEncoder(w).Encode(retVal) if err != nil { - h.log.Debug("failed to write json GetExamples response", zap.Error(ErrExampleAPI.Wrap(err))) + h.log.Debug("failed to write json GetPlacements response", zap.Error(ErrPlacementsAPI.Wrap(err))) } } diff --git a/satellite/admin/back-office/placements.go b/satellite/admin/back-office/placements.go new file mode 100644 index 000000000..a846734da --- /dev/null +++ b/satellite/admin/back-office/placements.go @@ -0,0 +1,36 @@ +// Copyright (C) 2023 Storj Labs, Inc. +// See LICENSE for copying information. + +package admin + +import ( + "context" + + "storj.io/common/storj" + "storj.io/storj/private/api" + "storj.io/storj/satellite/nodeselection" +) + +// PlacementInfo contains the ID and location of a placement rule. +type PlacementInfo struct { + ID storj.PlacementConstraint `json:"id"` + Location string `json:"location"` +} + +// GetPlacements returns IDs and locations of placement rules. +func (s *Server) GetPlacements(ctx context.Context) ([]PlacementInfo, api.HTTPError) { + var err error + defer mon.Task()(&ctx)(&err) + + placements := s.placement.SupportedPlacements() + infos := make([]PlacementInfo, 0, len(placements)) + for _, placement := range placements { + filter := s.placement.CreateFilters(placement) + infos = append(infos, PlacementInfo{ + ID: placement, + Location: nodeselection.GetAnnotation(filter, nodeselection.Location), + }) + } + + return infos, api.HTTPError{} +} diff --git a/satellite/admin/back-office/server.go b/satellite/admin/back-office/server.go index 730c7b4f4..057e84772 100644 --- a/satellite/admin/back-office/server.go +++ b/satellite/admin/back-office/server.go @@ -10,20 +10,26 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/spacemonkeygo/monkit/v3" "github.com/zeebo/errs" "go.uber.org/zap" "golang.org/x/sync/errgroup" "storj.io/common/errs2" ui "storj.io/storj/satellite/admin/back-office/ui" + "storj.io/storj/satellite/overlay" ) // PathPrefix is the path that will be prefixed to the router passed to the NewServer constructor. // This is temporary until this server will replace the storj.io/storj/satellite/admin/server.go. const PathPrefix = "/back-office/" -// Error is the error class that wraps all the errors returned by this package. -var Error = errs.Class("satellite-admin") +var ( + // Error is the error class that wraps all the errors returned by this package. + Error = errs.Class("satellite-admin") + + mon = monkit.Package() +) // Config defines configuration for the satellite administration server. type Config struct { @@ -38,23 +44,25 @@ type Config struct { // Server serves the API endpoints and the web application to allow preforming satellite // administration tasks. type Server struct { - log *zap.Logger - - listener net.Listener - server http.Server + log *zap.Logger + listener net.Listener + placement *overlay.PlacementDefinitions config Config + + server http.Server } // NewServer creates a satellite administration server instance with the provided dependencies and // configurations. // // When listener is nil, Server.Run is a noop. -func NewServer(log *zap.Logger, listener net.Listener, root *mux.Router, config Config) *Server { +func NewServer(log *zap.Logger, listener net.Listener, placement *overlay.PlacementDefinitions, root *mux.Router, config Config) *Server { server := &Server{ - log: log, - listener: listener, - config: config, + log: log, + listener: listener, + placement: placement, + config: config, } if root == nil { @@ -63,7 +71,8 @@ func NewServer(log *zap.Logger, listener net.Listener, root *mux.Router, config // API endpoints. // API generator already add the PathPrefix. - // _ := NewExample(log, mon, nil, root, nil) + auth := &apiAuth{server} + NewPlacementManagement(log, mon, server, root, auth) root = root.PathPrefix(PathPrefix).Subrouter() // Static assets for the web interface. @@ -108,3 +117,18 @@ func (server *Server) Run(ctx context.Context) error { func (server *Server) Close() error { return Error.Wrap(server.server.Close()) } + +// apiAuth exposes methods to control the authentication process for each generated API endpoint. +type apiAuth struct { + server *Server +} + +// IsAuthenticated checks if request is performed with all needed authorization credentials. +// This method is inert because the satellite admin back office uses neither cookies nor API keys to authenticate. +func (a *apiAuth) IsAuthenticated(ctx context.Context, r *http.Request, isCookieAuth, isKeyAuth bool) (_ context.Context, err error) { + return ctx, nil +} + +// RemoveAuthCookie indicates to the client that the authentication cookie should be removed. +// This method is inert because the satellite admin back office doesn't use authentication cookies. +func (a *apiAuth) RemoveAuthCookie(w http.ResponseWriter) {} diff --git a/satellite/admin/back-office/ui/src/api/client-mock.gen.ts b/satellite/admin/back-office/ui/src/api/client-mock.gen.ts deleted file mode 100644 index 88e6998d6..000000000 --- a/satellite/admin/back-office/ui/src/api/client-mock.gen.ts +++ /dev/null @@ -1,39 +0,0 @@ -// AUTOGENERATED BY private/apigen -// DO NOT EDIT. - -class APIError extends Error { - constructor( - public readonly msg: string, - public readonly responseStatusCode?: number, - ) { - super(msg); - } -} - -export class ExampleHttpApiV1 { - public readonly respStatusCode: number; - - // When respStatuscode is passed, the client throws an APIError on each method call - // with respStatusCode as HTTP status code. - // respStatuscode must be equal or greater than 400 - constructor(respStatusCode?: number) { - if (typeof respStatusCode === 'undefined') { - this.respStatusCode = 0; - return; - } - - if (respStatusCode < 400) { - throw new Error('invalid response status code for API Error, it must be greater or equal than 400'); - } - - this.respStatusCode = respStatusCode; - } - - public async getExamples(): Promise { - if (this.respStatusCode !== 0) { - throw new APIError('mock error message: ' + this.respStatusCode, this.respStatusCode); - } - - return JSON.parse('["example-1","example-2","example-3"]') as string[]; - } -} diff --git a/satellite/admin/back-office/ui/src/api/client.gen.ts b/satellite/admin/back-office/ui/src/api/client.gen.ts index 999bcc194..825f15e57 100644 --- a/satellite/admin/back-office/ui/src/api/client.gen.ts +++ b/satellite/admin/back-office/ui/src/api/client.gen.ts @@ -3,6 +3,11 @@ import { HttpClient } from '@/utils/httpClient'; +export class PlacementInfo { + id: number; + location: string; +} + class APIError extends Error { constructor( public readonly msg: string, @@ -12,15 +17,15 @@ class APIError extends Error { } } -export class ExampleHttpApiV1 { +export class PlacementManagementHttpApiV1 { private readonly http: HttpClient = new HttpClient(); - private readonly ROOT_PATH: string = '/back-office/api/v1/example'; + private readonly ROOT_PATH: string = '/back-office/api/v1/placements'; - public async getExamples(): Promise { - const fullPath = `${this.ROOT_PATH}/examples`; + public async getPlacements(): Promise { + const fullPath = `${this.ROOT_PATH}/`; const response = await this.http.get(fullPath); if (response.ok) { - return response.json().then((body) => body as string[]); + return response.json().then((body) => body as PlacementInfo[]); } const err = await response.json(); throw new APIError(err.error, response.status); diff --git a/satellite/admin/server.go b/satellite/admin/server.go index e5cb58704..64844345f 100644 --- a/satellite/admin/server.go +++ b/satellite/admin/server.go @@ -29,6 +29,7 @@ import ( "storj.io/storj/satellite/console/consoleweb" "storj.io/storj/satellite/console/restkeys" "storj.io/storj/satellite/oidc" + "storj.io/storj/satellite/overlay" "storj.io/storj/satellite/payments" "storj.io/storj/satellite/payments/stripe" ) @@ -38,6 +39,9 @@ const ( UnauthorizedNotInGroup = "User must be a member of one of these groups to conduct this operation: %s" // AuthorizationNotEnabled - message for when authorization is disabled. AuthorizationNotEnabled = "Authorization not enabled." + + // BackOfficePathPrefix is the path prefix used for the back office router. + BackOfficePathPrefix = "/back-office" ) // Config defines configuration for debug server. @@ -102,6 +106,7 @@ func NewServer( freezeAccounts *console.AccountFreezeService, analyticsService *analytics.Service, accounts payments.Accounts, + placement *overlay.PlacementDefinitions, console consoleweb.Config, config Config, ) *Server { @@ -178,7 +183,7 @@ func NewServer( // NewServer adds the backoffice.PahtPrefix for the static assets, but not for the API because the // generator already add the PathPrefix to router when the API handlers are hooked. - _ = backoffice.NewServer(log.Named("back-office"), nil, root, config.BackOffice) + _ = backoffice.NewServer(log.Named("back-office"), nil, placement, root, config.BackOffice) // This handler must be the last one because it uses the root as prefix, // otherwise will try to serve all the handlers set after this one.