satellite/admin/back-office: add endpoint to get placement info
This change adds an endpoint to the back office API that returns placement IDs and their locations. References #6503 Change-Id: I20ee1c82dcb647d6d264317beceeb5e70f7a8e87
This commit is contained in:
parent
e67691d51c
commit
1ea81c8887
@ -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,
|
||||
|
@ -6,20 +6,24 @@
|
||||
|
||||
<h2 id='list-of-endpoints'>List of Endpoints</h2>
|
||||
|
||||
* Example
|
||||
* [Get examples](#example-get-examples)
|
||||
* PlacementManagement
|
||||
* [Get placements](#placementmanagement-get-placements)
|
||||
|
||||
<h3 id='example-get-examples'>Get examples (<a href='#list-of-endpoints'>go to full list</a>)</h3>
|
||||
<h3 id='placementmanagement-get-placements'>Get placements (<a href='#list-of-endpoints'>go to full list</a>)</h3>
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
```
|
||||
|
@ -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"))
|
||||
}
|
||||
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
36
satellite/admin/back-office/placements.go
Normal file
36
satellite/admin/back-office/placements.go
Normal file
@ -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{}
|
||||
}
|
@ -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) {}
|
||||
|
@ -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<string[]> {
|
||||
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[];
|
||||
}
|
||||
}
|
@ -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<string[]> {
|
||||
const fullPath = `${this.ROOT_PATH}/examples`;
|
||||
public async getPlacements(): Promise<PlacementInfo[]> {
|
||||
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);
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user