NFSSVC-27 Implement list-buckets

This commit is contained in:
Evgeniy Kulikov 2020-07-24 19:10:41 +03:00
parent 309179e9ba
commit e776e9c0cb
8 changed files with 352 additions and 25 deletions

View file

@ -195,7 +195,7 @@ func newSettings() *viper.Viper {
// logger:
v.SetDefault(cfgLoggerLevel, "debug")
v.SetDefault(cfgLoggerFormat, "console")
v.SetDefault(cfgLoggerTraceLevel, "fatal")
v.SetDefault(cfgLoggerTraceLevel, "panic")
v.SetDefault(cfgLoggerNoDisclaimer, true)
v.SetDefault(cfgLoggerSamplingInitial, 1000)
v.SetDefault(cfgLoggerSamplingThereafter, 1000)

View file

@ -66,10 +66,6 @@ func newApp(l *zap.Logger, v *viper.Viper) *App {
l.Fatal("failed to initialize auth center", zap.Error(err))
}
if caller, err = handler.New(); err != nil {
l.Fatal("could not initialize API handler", zap.Error(err))
}
if v.IsSet(cfgTLSKeyFile) && v.IsSet(cfgTLSCertFile) {
tls = &tlsConfig{
KeyFile: v.GetString(cfgTLSKeyFile),
@ -133,6 +129,21 @@ func newApp(l *zap.Logger, v *viper.Viper) *App {
l.Fatal("could not prepare ObjectLayer", zap.Error(err))
}
{ // should prepare api.Handler:
ctx, cancel := context.WithTimeout(context.Background(), conTimeout)
defer cancel()
apiParams := handler.Params{
Log: l,
Cli: cli,
Key: center.GetNeoFSPrivateKey(),
}
if caller, err = handler.New(ctx, apiParams); err != nil {
l.Fatal("could not initialize API handler", zap.Error(err))
}
}
return &App{
center: center,
cli: cli,

2
go.mod
View file

@ -9,6 +9,7 @@ require (
github.com/Shopify/sarama v1.24.1
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/alecthomas/participle v0.2.1
github.com/aws/aws-sdk-go v1.33.8
github.com/bcicen/jstream v0.0.0-20190220045926-16c1f8af81c2
github.com/beevik/ntp v0.2.0
github.com/cespare/xxhash/v2 v2.1.1
@ -110,5 +111,4 @@ require (
gopkg.in/olivere/elastic.v5 v5.0.80
gopkg.in/yaml.v2 v2.2.8
honnef.co/go/tools v0.0.1-2020.1.3 // indirect
github.com/aws/aws-sdk-go v1.33.8
)

View file

@ -1939,8 +1939,8 @@ func toAPIError(ctx context.Context, err error) Error {
return apiErr
}
// getAPIError provides API Error for input API error code.
func getAPIError(code ErrorCode) Error {
// GetAPIError provides API Error for input API error code.
func GetAPIError(code ErrorCode) Error {
if apiErr, ok := errorCodes[code]; ok {
return apiErr
}

131
neofs/api/handler/api.go Normal file
View file

@ -0,0 +1,131 @@
package handler
import (
"context"
"crypto/ecdsa"
"math"
"github.com/minio/minio/neofs/api"
"github.com/minio/minio/neofs/pool"
"github.com/nspcc-dev/neofs-api-go/refs"
"github.com/nspcc-dev/neofs-api-go/service"
"github.com/nspcc-dev/neofs-api-go/session"
crypto "github.com/nspcc-dev/neofs-crypto"
"github.com/pkg/errors"
"go.uber.org/zap"
)
type (
handler struct {
log *zap.Logger
cli pool.Client
uid refs.OwnerID
tkn *service.Token
key *ecdsa.PrivateKey
}
Params struct {
Cli pool.Client
Log *zap.Logger
Key *ecdsa.PrivateKey
}
queryParams struct {
key *ecdsa.PrivateKey
addr refs.Address
verb service.Token_Info_Verb
}
)
var _ api.Handler = (*handler)(nil)
func New(ctx context.Context, p Params) (api.Handler, error) {
var (
err error
uid refs.OwnerID
tkn *service.Token
)
switch {
case p.Key == nil:
return nil, errors.New("empty private key")
case p.Cli == nil:
return nil, errors.New("empty gRPC client")
case p.Log == nil:
return nil, errors.New("empty logger")
}
if uid, err = refs.NewOwnerID(&p.Key.PublicKey); err != nil {
return nil, errors.Wrap(err, "could not fetch OwnerID")
} else if tkn, err = generateToken(ctx, p.Cli, p.Key); err != nil {
return nil, errors.Wrap(err, "could not prepare session token")
}
return &handler{
uid: uid,
tkn: tkn,
key: p.Key,
log: p.Log,
cli: p.Cli,
}, nil
}
func generateToken(ctx context.Context, cli pool.Client, key *ecdsa.PrivateKey) (*service.Token, error) {
owner, err := refs.NewOwnerID(&key.PublicKey)
if err != nil {
return nil, err
}
token := new(service.Token)
token.SetOwnerID(owner)
token.SetExpirationEpoch(math.MaxUint64)
token.SetOwnerKey(crypto.MarshalPublicKey(&key.PublicKey))
conn, err := cli.GetConnection(ctx)
if err != nil {
return nil, err
}
creator, err := session.NewGRPCCreator(conn, key)
if err != nil {
return nil, err
}
res, err := creator.Create(ctx, token)
if err != nil {
return nil, err
}
token.SetID(res.GetID())
token.SetSessionKey(res.GetSessionKey())
return token, nil
}
func prepareToken(t *service.Token, p queryParams) (*service.Token, error) {
sig := make([]byte, len(t.Signature))
copy(sig, t.Signature)
token := &service.Token{
Token_Info: service.Token_Info{
ID: t.ID,
OwnerID: t.OwnerID,
Verb: t.Verb,
Address: t.Address,
TokenLifetime: t.TokenLifetime,
SessionKey: t.SessionKey,
OwnerKey: t.OwnerKey,
},
Signature: sig,
}
token.SetAddress(p.addr)
token.SetVerb(p.verb)
err := service.AddSignatureWithKey(p.key, service.NewSignedSessionToken(token))
if err != nil {
return nil, err
}
return token, nil
}

View file

@ -0,0 +1,193 @@
package handler
import (
"context"
"encoding/xml"
"net/http"
"time"
"github.com/minio/minio/neofs/api"
"github.com/nspcc-dev/neofs-api-go/container"
"github.com/nspcc-dev/neofs-api-go/refs"
"github.com/nspcc-dev/neofs-api-go/service"
"github.com/pkg/errors"
"go.uber.org/zap"
"google.golang.org/grpc"
)
type (
// Owner - bucket owner/principal
Owner struct {
ID string
DisplayName string
}
// Bucket container for bucket metadata
Bucket struct {
Name string
CreationDate string // time string of format "2006-01-02T15:04:05.000Z"
}
// ListBucketsResponse - format for list buckets response
ListBucketsResponse struct {
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListAllMyBucketsResult" json:"-"`
Owner Owner
// Container for one or more buckets.
Buckets struct {
Buckets []*Bucket `xml:"Bucket"`
} // Buckets are nested
}
cnrInfoParams struct {
cid refs.CID
con *grpc.ClientConn
tkn *service.BearerTokenMsg
}
)
// TODO should be replaced with auth.GetBearerToken
func getBearerToken(ctx context.Context) (*service.BearerTokenMsg, error) {
if val := ctx.Value("ctxBearerToken"); val == nil {
return nil, errors.New("empty bearer token")
} else if tkn, ok := val.(*service.BearerTokenMsg); ok {
return tkn, nil
}
return nil, errors.New("bad value for bearer token")
}
func (h *handler) getContainerInfo(ctx context.Context, p cnrInfoParams) (*Bucket, error) {
var (
err error
res *container.GetResponse
)
req := new(container.GetRequest)
req.SetCID(p.cid)
req.SetTTL(service.SingleForwardingTTL)
req.SetBearer(p.tkn)
if err = service.SignRequestData(h.key, req); err != nil {
return nil, errors.Wrap(err, "could not sign container info request")
} else if res, err = container.NewServiceClient(p.con).Get(ctx, req); err != nil {
return nil, errors.Wrap(err, "could not fetch container info")
}
// TODO should extract nice name
// and datetime from container info:
_ = res
return &Bucket{
Name: p.cid.String(),
CreationDate: new(time.Time).Format(time.RFC3339),
}, nil
}
func (h *handler) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
var (
err error
uid = h.uid
inf *Bucket
con *grpc.ClientConn
res *container.ListResponse
)
// TODO think about timeout
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
// TODO should be replaced with auth.GetBearerToken,
// than if we not received token, should call
// api.WriteErrorResponse
bearer, _ := getBearerToken(ctx)
// should be taken from BearerToken, to display only users containers
// in future
if bearer != nil {
uid = bearer.OwnerID
}
req := new(container.ListRequest)
req.OwnerID = uid
req.SetTTL(service.SingleForwardingTTL)
req.SetBearer(bearer)
// req.SetVersion(APIVersion) ??
if con, err = h.cli.GetConnection(ctx); err != nil {
h.log.Error("could not get connection",
zap.Error(err))
e := api.GetAPIError(api.ErrInternalError)
api.WriteErrorResponse(ctx, w, api.Error{
Code: e.Code,
Description: err.Error(),
HTTPStatusCode: e.HTTPStatusCode,
}, r.URL)
return
} else if err = service.SignRequestData(h.key, req); err != nil {
h.log.Error("could not prepare request",
zap.Error(err))
e := api.GetAPIError(api.ErrInternalError)
api.WriteErrorResponse(ctx, w, api.Error{
Code: e.Code,
Description: err.Error(),
HTTPStatusCode: e.HTTPStatusCode,
}, r.URL)
return
} else if res, err = container.NewServiceClient(con).List(ctx, req); err != nil {
h.log.Error("could not list buckets",
zap.Error(err))
e := api.GetAPIError(api.ErrInternalError)
api.WriteErrorResponse(ctx, w, api.Error{
Code: e.Code,
Description: err.Error(),
HTTPStatusCode: e.HTTPStatusCode,
}, r.URL)
return
}
result := &ListBucketsResponse{Owner: Owner{
ID: uid.String(),
DisplayName: uid.String(),
}}
params := cnrInfoParams{con: con, tkn: bearer}
for _, cid := range res.CID {
// should receive each container info (??):
params.cid = cid
if inf, err = h.getContainerInfo(ctx, params); err != nil {
h.log.Error("could not fetch bucket info",
zap.Error(err))
e := api.GetAPIError(api.ErrInternalError)
api.WriteErrorResponse(ctx, w, api.Error{
Code: e.Code,
Description: err.Error(),
HTTPStatusCode: e.HTTPStatusCode,
}, r.URL)
return
}
result.Buckets.Buckets = append(result.Buckets.Buckets, inf)
}
// Generate response.
encodedSuccessResponse := api.EncodeResponse(result)
// Write response.
api.WriteSuccessResponseXML(w, encodedSuccessResponse)
}

View file

@ -6,12 +6,6 @@ import (
"github.com/minio/minio/neofs/api"
)
type handler struct{}
var _ api.Handler = (*handler)(nil)
func New() (api.Handler, error) { return new(handler), nil }
func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) {
api.WriteErrorResponse(r.Context(), w, api.Error{
Code: "XNeoFSUnimplemented",
@ -491,11 +485,3 @@ func (h *handler) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
HTTPStatusCode: http.StatusNotImplemented,
}, r.URL)
}
func (h *handler) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
api.WriteErrorResponse(r.Context(), w, api.Error{
Code: "XNeoFSUnimplemented",
Description: "implement me",
HTTPStatusCode: http.StatusNotImplemented,
}, r.URL)
}

View file

@ -75,7 +75,7 @@ func WriteErrorResponse(ctx context.Context, w http.ResponseWriter, err Error, r
// Generate error response.
errorResponse := getAPIErrorResponse(ctx, err, reqURL.Path,
w.Header().Get(hdrAmzRequestID), deploymentID.String())
encodedErrorResponse := encodeResponse(errorResponse)
encodedErrorResponse := EncodeResponse(errorResponse)
writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeXML)
}
@ -119,8 +119,8 @@ func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType
}
}
// Encodes the response headers into XML format.
func encodeResponse(response interface{}) []byte {
// EncodeResponse encodes the response headers into XML format.
func EncodeResponse(response interface{}) []byte {
var bytesBuffer bytes.Buffer
bytesBuffer.WriteString(xml.Header)
_ = xml.
@ -128,3 +128,9 @@ func encodeResponse(response interface{}) []byte {
Encode(response)
return bytesBuffer.Bytes()
}
// WriteSuccessResponseXML writes success headers and response if any,
// with content-type set to `application/xml`.
func WriteSuccessResponseXML(w http.ResponseWriter, response []byte) {
writeResponse(w, http.StatusOK, response, mimeXML)
}