From 26f0ae93f45390c2e5174e06312034d01b587dcd Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Wed, 13 Apr 2022 18:23:03 +0300 Subject: [PATCH] [#1] Add route to list containers Signed-off-by: Denis Kirillov --- cmd/neofs-rest-gw/integration_test.go | 33 ++- gen/models/container_base_info.go | 74 +++++++ gen/models/container_list.go | 136 ++++++++++++ gen/restapi/embedded_spec.go | 201 +++++++++++++++--- gen/restapi/operations/list_containers.go | 56 +++++ .../operations/list_containers_parameters.go | 196 +++++++++++++++++ .../operations/list_containers_responses.go | 100 +++++++++ gen/restapi/operations/neofs_rest_gw_api.go | 12 ++ handlers/api.go | 1 + handlers/containers.go | 72 +++++++ handlers/util.go | 4 + spec/rest.yaml | 58 ++++- 12 files changed, 913 insertions(+), 30 deletions(-) create mode 100644 gen/models/container_base_info.go create mode 100644 gen/models/container_list.go create mode 100644 gen/restapi/operations/list_containers.go create mode 100644 gen/restapi/operations/list_containers_parameters.go create mode 100644 gen/restapi/operations/list_containers_responses.go diff --git a/cmd/neofs-rest-gw/integration_test.go b/cmd/neofs-rest-gw/integration_test.go index 8cab9b6..a4352ba 100644 --- a/cmd/neofs-rest-gw/integration_test.go +++ b/cmd/neofs-rest-gw/integration_test.go @@ -43,6 +43,7 @@ const ( testListenAddress = "localhost:8082" testHost = "http://" + testListenAddress testNode = "localhost:8080" + containerName = "test-container" // XNeofsTokenSignature header contains base64 encoded signature of the token body. XNeofsTokenSignature = "X-Neofs-Token-Signature" @@ -72,7 +73,7 @@ func TestIntegration(t *testing.T) { aioContainer := createDockerContainer(ctx, t, aioImage+version) cancel := runServer(ctx, t) clientPool := getPool(ctx, t, key) - cnrID := createContainer(ctx, t, clientPool, "test-container") + cnrID := createContainer(ctx, t, clientPool, containerName) t.Run("rest put object "+version, func(t *testing.T) { restObjectPut(ctx, t, clientPool, cnrID) }) t.Run("rest get object "+version, func(t *testing.T) { restObjectGet(ctx, t, clientPool, cnrID) }) @@ -82,6 +83,7 @@ func TestIntegration(t *testing.T) { t.Run("rest delete container"+version, func(t *testing.T) { restContainerDelete(ctx, t, clientPool) }) t.Run("rest put container eacl "+version, func(t *testing.T) { restContainerEACLPut(ctx, t, clientPool) }) t.Run("rest get container eacl "+version, func(t *testing.T) { restContainerEACLGet(ctx, t, clientPool) }) + t.Run("rest list containers "+version, func(t *testing.T) { restContainerList(ctx, t, clientPool, cnrID) }) cancel() err = aioContainer.Terminate(ctx) @@ -407,6 +409,35 @@ func restContainerEACLGet(ctx context.Context, t *testing.T, clientPool *pool.Po require.True(t, eacl.EqualTables(*expectedTable, *actualTable)) } +func restContainerList(ctx context.Context, t *testing.T, p *pool.Pool, cnrID *cid.ID) { + var prm pool.PrmContainerList + prm.SetOwnerID(*p.OwnerID()) + + ids, err := p.ListContainers(ctx, prm) + require.NoError(t, err) + + httpClient := defaultHTTPClient() + + query := make(url.Values) + query.Add("ownerId", p.OwnerID().String()) + + request, err := http.NewRequest(http.MethodGet, testHost+"/v1/containers?"+query.Encode(), nil) + require.NoError(t, err) + request = request.WithContext(ctx) + + list := &models.ContainerList{} + doRequest(t, httpClient, request, http.StatusOK, list) + + require.Equal(t, len(ids), int(*list.Size)) + + expected := &models.ContainerBaseInfo{ + ContainerID: handlers.NewString(cnrID.String()), + Name: containerName, + } + + require.Contains(t, list.Containers, expected) +} + func makeAuthContainerTokenRequest(ctx context.Context, t *testing.T, bearer *models.Bearer, httpClient *http.Client) *handlers.BearerToken { return makeAuthTokenRequest(ctx, t, bearer, httpClient, models.TokenTypeContainer) } diff --git a/gen/models/container_base_info.go b/gen/models/container_base_info.go new file mode 100644 index 0000000..18342a6 --- /dev/null +++ b/gen/models/container_base_info.go @@ -0,0 +1,74 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// ContainerBaseInfo container base info +// +// swagger:model ContainerBaseInfo +type ContainerBaseInfo struct { + + // container Id + // Required: true + ContainerID *string `json:"containerId"` + + // name + Name string `json:"name,omitempty"` +} + +// Validate validates this container base info +func (m *ContainerBaseInfo) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateContainerID(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ContainerBaseInfo) validateContainerID(formats strfmt.Registry) error { + + if err := validate.Required("containerId", "body", m.ContainerID); err != nil { + return err + } + + return nil +} + +// ContextValidate validates this container base info based on context it is used +func (m *ContainerBaseInfo) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *ContainerBaseInfo) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ContainerBaseInfo) UnmarshalBinary(b []byte) error { + var res ContainerBaseInfo + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/gen/models/container_list.go b/gen/models/container_list.go new file mode 100644 index 0000000..c268088 --- /dev/null +++ b/gen/models/container_list.go @@ -0,0 +1,136 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "strconv" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// ContainerList container list +// +// swagger:model ContainerList +type ContainerList struct { + + // containers + // Required: true + Containers []*ContainerBaseInfo `json:"containers"` + + // size + // Required: true + Size *int64 `json:"size"` +} + +// Validate validates this container list +func (m *ContainerList) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateContainers(formats); err != nil { + res = append(res, err) + } + + if err := m.validateSize(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ContainerList) validateContainers(formats strfmt.Registry) error { + + if err := validate.Required("containers", "body", m.Containers); err != nil { + return err + } + + for i := 0; i < len(m.Containers); i++ { + if swag.IsZero(m.Containers[i]) { // not required + continue + } + + if m.Containers[i] != nil { + if err := m.Containers[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("containers" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("containers" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +func (m *ContainerList) validateSize(formats strfmt.Registry) error { + + if err := validate.Required("size", "body", m.Size); err != nil { + return err + } + + return nil +} + +// ContextValidate validate this container list based on the context it is used +func (m *ContainerList) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateContainers(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ContainerList) contextValidateContainers(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.Containers); i++ { + + if m.Containers[i] != nil { + if err := m.Containers[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("containers" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("containers" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *ContainerList) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ContainerList) UnmarshalBinary(b []byte) error { + var res ContainerList + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/gen/restapi/embedded_spec.go b/gen/restapi/embedded_spec.go index ef23a16..e33fe14 100644 --- a/gen/restapi/embedded_spec.go +++ b/gen/restapi/embedded_spec.go @@ -94,10 +94,60 @@ func init() { } }, "/containers": { + "get": { + "security": [], + "summary": "Get list of containers", + "operationId": "listContainers", + "parameters": [ + { + "type": "string", + "description": "Base58 encoded owner id", + "name": "ownerId", + "in": "query", + "required": true + }, + { + "type": "integer", + "default": 0, + "description": "The number of containers to skip before starting to collect the result set.", + "name": "offset", + "in": "query" + }, + { + "maximum": 10000, + "minimum": 1, + "type": "integer", + "default": 100, + "description": "The numbers of containers to return.", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Containers info", + "schema": { + "$ref": "#/definitions/ContainerList" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, "put": { "summary": "Create new container in NeoFS", "operationId": "putContainer", "parameters": [ + { + "$ref": "#/parameters/signatureParam" + }, + { + "$ref": "#/parameters/signatureKeyParam" + }, { "type": "boolean", "default": false, @@ -159,15 +209,7 @@ func init() { } } } - }, - "parameters": [ - { - "$ref": "#/parameters/signatureParam" - }, - { - "$ref": "#/parameters/signatureKeyParam" - } - ] + } }, "/containers/{containerId}": { "get": { @@ -433,6 +475,20 @@ func init() { } } }, + "ContainerBaseInfo": { + "type": "object", + "required": [ + "containerId" + ], + "properties": { + "containerId": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "ContainerInfo": { "type": "object", "required": [ @@ -484,6 +540,24 @@ func init() { "version": "2.11" } }, + "ContainerList": { + "type": "object", + "required": [ + "size", + "containers" + ], + "properties": { + "containers": { + "type": "array", + "items": { + "$ref": "#/definitions/ContainerBaseInfo" + } + }, + "size": { + "type": "integer" + } + } + }, "Eacl": { "type": "object", "required": [ @@ -853,10 +927,69 @@ func init() { } }, "/containers": { + "get": { + "security": [], + "summary": "Get list of containers", + "operationId": "listContainers", + "parameters": [ + { + "type": "string", + "description": "Base58 encoded owner id", + "name": "ownerId", + "in": "query", + "required": true + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "The number of containers to skip before starting to collect the result set.", + "name": "offset", + "in": "query" + }, + { + "maximum": 10000, + "minimum": 1, + "type": "integer", + "default": 100, + "description": "The numbers of containers to return.", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Containers info", + "schema": { + "$ref": "#/definitions/ContainerList" + } + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, "put": { "summary": "Create new container in NeoFS", "operationId": "putContainer", "parameters": [ + { + "type": "string", + "description": "Base64 encoded signature for bearer token", + "name": "X-Neofs-Token-Signature", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Hex encoded the public part of the key that signed the bearer token", + "name": "X-Neofs-Token-Signature-Key", + "in": "header", + "required": true + }, { "type": "boolean", "default": false, @@ -918,23 +1051,7 @@ func init() { } } } - }, - "parameters": [ - { - "type": "string", - "description": "Base64 encoded signature for bearer token", - "name": "X-Neofs-Token-Signature", - "in": "header", - "required": true - }, - { - "type": "string", - "description": "Hex encoded the public part of the key that signed the bearer token", - "name": "X-Neofs-Token-Signature-Key", - "in": "header", - "required": true - } - ] + } }, "/containers/{containerId}": { "get": { @@ -1244,6 +1361,20 @@ func init() { } } }, + "ContainerBaseInfo": { + "type": "object", + "required": [ + "containerId" + ], + "properties": { + "containerId": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "ContainerInfo": { "type": "object", "required": [ @@ -1295,6 +1426,24 @@ func init() { "version": "2.11" } }, + "ContainerList": { + "type": "object", + "required": [ + "size", + "containers" + ], + "properties": { + "containers": { + "type": "array", + "items": { + "$ref": "#/definitions/ContainerBaseInfo" + } + }, + "size": { + "type": "integer" + } + } + }, "Eacl": { "type": "object", "required": [ diff --git a/gen/restapi/operations/list_containers.go b/gen/restapi/operations/list_containers.go new file mode 100644 index 0000000..317b7a6 --- /dev/null +++ b/gen/restapi/operations/list_containers.go @@ -0,0 +1,56 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// ListContainersHandlerFunc turns a function with the right signature into a list containers handler +type ListContainersHandlerFunc func(ListContainersParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn ListContainersHandlerFunc) Handle(params ListContainersParams) middleware.Responder { + return fn(params) +} + +// ListContainersHandler interface for that can handle valid list containers params +type ListContainersHandler interface { + Handle(ListContainersParams) middleware.Responder +} + +// NewListContainers creates a new http.Handler for the list containers operation +func NewListContainers(ctx *middleware.Context, handler ListContainersHandler) *ListContainers { + return &ListContainers{Context: ctx, Handler: handler} +} + +/* ListContainers swagger:route GET /containers listContainers + +Get list of containers + +*/ +type ListContainers struct { + Context *middleware.Context + Handler ListContainersHandler +} + +func (o *ListContainers) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewListContainersParams() + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/gen/restapi/operations/list_containers_parameters.go b/gen/restapi/operations/list_containers_parameters.go new file mode 100644 index 0000000..2ab1a7b --- /dev/null +++ b/gen/restapi/operations/list_containers_parameters.go @@ -0,0 +1,196 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// NewListContainersParams creates a new ListContainersParams object +// with the default values initialized. +func NewListContainersParams() ListContainersParams { + + var ( + // initialize parameters with default values + + limitDefault = int64(100) + offsetDefault = int64(0) + ) + + return ListContainersParams{ + Limit: &limitDefault, + + Offset: &offsetDefault, + } +} + +// ListContainersParams contains all the bound params for the list containers operation +// typically these are obtained from a http.Request +// +// swagger:parameters listContainers +type ListContainersParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /*The numbers of containers to return. + Maximum: 10000 + Minimum: 1 + In: query + Default: 100 + */ + Limit *int64 + /*The number of containers to skip before starting to collect the result set. + Minimum: 0 + In: query + Default: 0 + */ + Offset *int64 + /*Base58 encoded owner id + Required: true + In: query + */ + OwnerID string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewListContainersParams() beforehand. +func (o *ListContainersParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + qs := runtime.Values(r.URL.Query()) + + qLimit, qhkLimit, _ := qs.GetOK("limit") + if err := o.bindLimit(qLimit, qhkLimit, route.Formats); err != nil { + res = append(res, err) + } + + qOffset, qhkOffset, _ := qs.GetOK("offset") + if err := o.bindOffset(qOffset, qhkOffset, route.Formats); err != nil { + res = append(res, err) + } + + qOwnerID, qhkOwnerID, _ := qs.GetOK("ownerId") + if err := o.bindOwnerID(qOwnerID, qhkOwnerID, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindLimit binds and validates parameter Limit from query. +func (o *ListContainersParams) bindLimit(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewListContainersParams() + return nil + } + + value, err := swag.ConvertInt64(raw) + if err != nil { + return errors.InvalidType("limit", "query", "int64", raw) + } + o.Limit = &value + + if err := o.validateLimit(formats); err != nil { + return err + } + + return nil +} + +// validateLimit carries on validations for parameter Limit +func (o *ListContainersParams) validateLimit(formats strfmt.Registry) error { + + if err := validate.MinimumInt("limit", "query", *o.Limit, 1, false); err != nil { + return err + } + + if err := validate.MaximumInt("limit", "query", *o.Limit, 10000, false); err != nil { + return err + } + + return nil +} + +// bindOffset binds and validates parameter Offset from query. +func (o *ListContainersParams) bindOffset(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + // Default values have been previously initialized by NewListContainersParams() + return nil + } + + value, err := swag.ConvertInt64(raw) + if err != nil { + return errors.InvalidType("offset", "query", "int64", raw) + } + o.Offset = &value + + if err := o.validateOffset(formats); err != nil { + return err + } + + return nil +} + +// validateOffset carries on validations for parameter Offset +func (o *ListContainersParams) validateOffset(formats strfmt.Registry) error { + + if err := validate.MinimumInt("offset", "query", *o.Offset, 0, false); err != nil { + return err + } + + return nil +} + +// bindOwnerID binds and validates parameter OwnerID from query. +func (o *ListContainersParams) bindOwnerID(rawData []string, hasKey bool, formats strfmt.Registry) error { + if !hasKey { + return errors.Required("ownerId", "query", rawData) + } + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // AllowEmptyValue: false + + if err := validate.RequiredString("ownerId", "query", raw); err != nil { + return err + } + o.OwnerID = raw + + return nil +} diff --git a/gen/restapi/operations/list_containers_responses.go b/gen/restapi/operations/list_containers_responses.go new file mode 100644 index 0000000..3ef0789 --- /dev/null +++ b/gen/restapi/operations/list_containers_responses.go @@ -0,0 +1,100 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/nspcc-dev/neofs-rest-gw/gen/models" +) + +// ListContainersOKCode is the HTTP code returned for type ListContainersOK +const ListContainersOKCode int = 200 + +/*ListContainersOK Containers info + +swagger:response listContainersOK +*/ +type ListContainersOK struct { + + /* + In: Body + */ + Payload *models.ContainerList `json:"body,omitempty"` +} + +// NewListContainersOK creates ListContainersOK with default headers values +func NewListContainersOK() *ListContainersOK { + + return &ListContainersOK{} +} + +// WithPayload adds the payload to the list containers o k response +func (o *ListContainersOK) WithPayload(payload *models.ContainerList) *ListContainersOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the list containers o k response +func (o *ListContainersOK) SetPayload(payload *models.ContainerList) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ListContainersOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +// ListContainersBadRequestCode is the HTTP code returned for type ListContainersBadRequest +const ListContainersBadRequestCode int = 400 + +/*ListContainersBadRequest Bad request + +swagger:response listContainersBadRequest +*/ +type ListContainersBadRequest struct { + + /* + In: Body + */ + Payload models.Error `json:"body,omitempty"` +} + +// NewListContainersBadRequest creates ListContainersBadRequest with default headers values +func NewListContainersBadRequest() *ListContainersBadRequest { + + return &ListContainersBadRequest{} +} + +// WithPayload adds the payload to the list containers bad request response +func (o *ListContainersBadRequest) WithPayload(payload models.Error) *ListContainersBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the list containers bad request response +func (o *ListContainersBadRequest) SetPayload(payload models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *ListContainersBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} diff --git a/gen/restapi/operations/neofs_rest_gw_api.go b/gen/restapi/operations/neofs_rest_gw_api.go index 54912c5..8fa1ed2 100644 --- a/gen/restapi/operations/neofs_rest_gw_api.go +++ b/gen/restapi/operations/neofs_rest_gw_api.go @@ -59,6 +59,9 @@ func NewNeofsRestGwAPI(spec *loads.Document) *NeofsRestGwAPI { GetObjectInfoHandler: GetObjectInfoHandlerFunc(func(params GetObjectInfoParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation GetObjectInfo has not yet been implemented") }), + ListContainersHandler: ListContainersHandlerFunc(func(params ListContainersParams) middleware.Responder { + return middleware.NotImplemented("operation ListContainers has not yet been implemented") + }), PutContainerHandler: PutContainerHandlerFunc(func(params PutContainerParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation PutContainer has not yet been implemented") }), @@ -128,6 +131,8 @@ type NeofsRestGwAPI struct { GetContainerEACLHandler GetContainerEACLHandler // GetObjectInfoHandler sets the operation handler for the get object info operation GetObjectInfoHandler GetObjectInfoHandler + // ListContainersHandler sets the operation handler for the list containers operation + ListContainersHandler ListContainersHandler // PutContainerHandler sets the operation handler for the put container operation PutContainerHandler PutContainerHandler // PutContainerEACLHandler sets the operation handler for the put container e ACL operation @@ -230,6 +235,9 @@ func (o *NeofsRestGwAPI) Validate() error { if o.GetObjectInfoHandler == nil { unregistered = append(unregistered, "GetObjectInfoHandler") } + if o.ListContainersHandler == nil { + unregistered = append(unregistered, "ListContainersHandler") + } if o.PutContainerHandler == nil { unregistered = append(unregistered, "PutContainerHandler") } @@ -358,6 +366,10 @@ func (o *NeofsRestGwAPI) initHandlerCache() { o.handlers["GET"] = make(map[string]http.Handler) } o.handlers["GET"]["/objects/{containerId}/{objectId}"] = NewGetObjectInfo(o.context, o.GetObjectInfoHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } + o.handlers["GET"]["/containers"] = NewListContainers(o.context, o.ListContainersHandler) if o.handlers["PUT"] == nil { o.handlers["PUT"] = make(map[string]http.Handler) } diff --git a/handlers/api.go b/handlers/api.go index 8963b5e..cf8eafb 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -71,6 +71,7 @@ func (a *API) Configure(api *operations.NeofsRestGwAPI) http.Handler { api.DeleteContainerHandler = operations.DeleteContainerHandlerFunc(a.DeleteContainer) api.PutContainerEACLHandler = operations.PutContainerEACLHandlerFunc(a.PutContainerEACL) api.GetContainerEACLHandler = operations.GetContainerEACLHandlerFunc(a.GetContainerEACL) + api.ListContainersHandler = operations.ListContainersHandlerFunc(a.ListContainer) api.BearerAuthAuth = func(s string) (*models.Principal, error) { if !strings.HasPrefix(s, BearerPrefix) { diff --git a/handlers/containers.go b/handlers/containers.go index 52cc317..bb75d72 100644 --- a/handlers/containers.go +++ b/handlers/containers.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "fmt" + "github.com/nspcc-dev/neofs-sdk-go/owner" "net/http" "strconv" "strings" @@ -124,6 +125,57 @@ func (a *API) GetContainerEACL(params operations.GetContainerEACLParams) middlew return operations.NewGetContainerEACLOK().WithPayload(resp) } +// ListContainer handler that returns containers. +func (a *API) ListContainer(params operations.ListContainersParams) middleware.Responder { + ctx := params.HTTPRequest.Context() + + var ownerID owner.ID + if err := ownerID.Parse(params.OwnerID); err != nil { + a.log.Error("invalid owner id", zap.Error(err)) + return operations.NewListContainersBadRequest().WithPayload("invalid owner id") + } + + var prm pool.PrmContainerList + prm.SetOwnerID(ownerID) + + ids, err := a.pool.ListContainers(ctx, prm) + if err != nil { + a.log.Error("list containers", zap.Error(err)) + return operations.NewListContainersBadRequest().WithPayload("failed to get containers") + } + + offset := int(*params.Offset) + size := int(*params.Limit) + + if offset > len(ids)-1 { + res := &models.ContainerList{ + Size: NewInteger(0), + Containers: []*models.ContainerBaseInfo{}, + } + return operations.NewListContainersOK().WithPayload(res) + } + + if offset+size > len(ids) { + size = len(ids) - offset + } + + res := &models.ContainerList{ + Size: NewInteger(int64(size)), + Containers: make([]*models.ContainerBaseInfo, 0, size), + } + + for _, id := range ids[offset : offset+size] { + baseInfo, err := getContainerBaseInfo(ctx, a.pool, id) + if err != nil { + a.log.Error("get container", zap.String("cid", id.String()), zap.Error(err)) + return operations.NewListContainersBadRequest().WithPayload("failed to get container") + } + res.Containers = append(res.Containers, baseInfo) + } + + return operations.NewListContainersOK().WithPayload(res) +} + // DeleteContainer handler that returns container info. func (a *API) DeleteContainer(params operations.DeleteContainerParams, principal *models.Principal) middleware.Responder { bt := &BearerToken{ @@ -155,6 +207,26 @@ func (a *API) DeleteContainer(params operations.DeleteContainerParams, principal return operations.NewDeleteContainerNoContent() } +func getContainerBaseInfo(ctx context.Context, p *pool.Pool, cnrID cid.ID) (*models.ContainerBaseInfo, error) { + var prm pool.PrmContainerGet + prm.SetContainerID(cnrID) + + cnr, err := p.GetContainer(ctx, prm) + if err != nil { + return nil, err + } + + baseInfo := &models.ContainerBaseInfo{ContainerID: NewString(cnrID.String())} + + for _, attr := range cnr.Attributes() { + if attr.Key() == container.AttributeName { + baseInfo.Name = attr.Value() + } + } + + return baseInfo, nil +} + func prepareUserAttributes(header http.Header) map[string]string { filtered := filterHeaders(header) delete(filtered, container.AttributeName) diff --git a/handlers/util.go b/handlers/util.go index 766b977..dc4bede 100644 --- a/handlers/util.go +++ b/handlers/util.go @@ -219,6 +219,10 @@ func NewString(val string) *string { return &val } +func NewInteger(val int64) *int64 { + return &val +} + func NewError(err error) models.Error { return models.Error(err.Error()) } diff --git a/spec/rest.yaml b/spec/rest.yaml index 7d87094..4102dfe 100644 --- a/spec/rest.yaml +++ b/spec/rest.yaml @@ -168,13 +168,12 @@ paths: $ref: '#/definitions/Error' /containers: - parameters: - - $ref: '#/parameters/signatureParam' - - $ref: '#/parameters/signatureKeyParam' put: operationId: putContainer summary: Create new container in NeoFS parameters: + - $ref: '#/parameters/signatureParam' + - $ref: '#/parameters/signatureKeyParam' - in: query name: skip-native-name description: Provide this parameter to skip registration container name in NNS service @@ -215,6 +214,38 @@ paths: description: Bad request schema: $ref: '#/definitions/Error' + get: + operationId: listContainers + summary: Get list of containers + security: [ ] + parameters: + - in: query + name: ownerId + required: true + type: string + description: Base58 encoded owner id + - in: query + name: offset + type: integer + default: 0 + minimum: 0 + description: The number of containers to skip before starting to collect the result set. + - in: query + name: limit + type: integer + default: 100 + minimum: 1 + maximum: 10000 + description: The numbers of containers to return. + responses: + 200: + description: Containers info + schema: + $ref: '#/definitions/ContainerList' + 400: + description: Bad request + schema: + $ref: '#/definitions/Error' /containers/{containerId}: parameters: @@ -457,6 +488,27 @@ definitions: value: "1648810072" - key: Name value: container + ContainerList: + type: object + properties: + size: + type: integer + containers: + type: array + items: + $ref: '#/definitions/ContainerBaseInfo' + required: + - size + - containers + ContainerBaseInfo: + type: object + properties: + containerId: + type: string + name: + type: string + required: + - containerId ObjectInfo: type: object properties: