From 63fdb08f1446db736410f30239f0442ac162f77a Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Wed, 13 Apr 2022 11:41:04 +0300 Subject: [PATCH] [#1] Add route to delete container Signed-off-by: Denis Kirillov --- cmd/neofs-rest-gw/integration_test.go | 184 ++++++++++++------ gen/restapi/embedded_spec.go | 108 ++++++++-- gen/restapi/operations/delete_container.go | 71 +++++++ .../operations/delete_container_parameters.go | 130 +++++++++++++ .../operations/delete_container_responses.go | 80 ++++++++ gen/restapi/operations/neofs_rest_gw_api.go | 12 ++ .../operations/put_container_parameters.go | 50 ++++- go.mod | 2 +- go.sum | 4 +- handlers/api.go | 3 +- handlers/containers.go | 59 +++++- spec/rest.yaml | 30 ++- 12 files changed, 637 insertions(+), 96 deletions(-) create mode 100644 gen/restapi/operations/delete_container.go create mode 100644 gen/restapi/operations/delete_container_parameters.go create mode 100644 gen/restapi/operations/delete_container_responses.go diff --git a/cmd/neofs-rest-gw/integration_test.go b/cmd/neofs-rest-gw/integration_test.go index 5451a01..1a7d778 100644 --- a/cmd/neofs-rest-gw/integration_test.go +++ b/cmd/neofs-rest-gw/integration_test.go @@ -13,6 +13,7 @@ import ( "fmt" "io" "net/http" + "net/url" "strconv" "testing" "time" @@ -55,7 +56,13 @@ const ( func TestIntegration(t *testing.T) { rootCtx := context.Background() aioImage := "nspccdev/neofs-aio-testcontainer:" - versions := []string{"0.24.0", "0.25.1", "0.27.5", "latest"} + versions := []string{ + "0.24.0", + "0.25.1", + "0.26.1", + "0.27.5", + "latest", + } key, err := keys.NewPrivateKeyFromHex(devenvPrivateKey) require.NoError(t, err) @@ -65,12 +72,13 @@ func TestIntegration(t *testing.T) { aioContainer := createDockerContainer(ctx, t, aioImage+version) cancel := runServer(ctx, t) clientPool := getPool(ctx, t, key) - CID, err := createContainer(ctx, t, clientPool) - require.NoError(t, err, version) + cnrID := createContainer(ctx, t, clientPool, "test-container") + + t.Run("rest put object "+version, func(t *testing.T) { restObjectPut(ctx, t, clientPool, cnrID) }) - t.Run("rest put object "+version, func(t *testing.T) { restObjectPut(ctx, t, clientPool, CID) }) t.Run("rest put container"+version, func(t *testing.T) { restContainerPut(ctx, t, clientPool) }) - t.Run("rest get container"+version, func(t *testing.T) { restContainerGet(ctx, t, clientPool, CID) }) + t.Run("rest get container"+version, func(t *testing.T) { restContainerGet(ctx, t, clientPool, cnrID) }) + t.Run("rest delete container"+version, func(t *testing.T) { restContainerDelete(ctx, t, clientPool) }) cancel() err = aioContainer.Terminate(ctx) @@ -132,6 +140,7 @@ func getDefaultConfig() *viper.Viper { v.SetDefault(cfgPeers+".0.weight", 1) v.SetDefault(cfgPeers+".0.priority", 1) v.SetDefault(restapi.FlagListenAddress, testListenAddress) + v.SetDefault(restapi.FlagWriteTimeout, 60*time.Second) return v } @@ -292,8 +301,98 @@ func restContainerGet(ctx context.Context, t *testing.T, clientPool *pool.Pool, err = json.NewDecoder(resp.Body).Decode(cnrInfo) require.NoError(t, err) - require.Equal(t, cnrID.String(), cnrInfo.ContainerID) - require.Equal(t, clientPool.OwnerID().String(), cnrInfo.OwnerID) + require.Equal(t, cnrID.String(), *cnrInfo.ContainerID) + require.Equal(t, clientPool.OwnerID().String(), *cnrInfo.OwnerID) +} + +func restContainerDelete(ctx context.Context, t *testing.T, clientPool *pool.Pool) { + cnrID := createContainer(ctx, t, clientPool, "for-delete") + + bearer := &models.Bearer{ + Container: &models.Rule{ + Verb: models.NewVerb(models.VerbDELETE), + }, + } + + httpClient := &http.Client{Timeout: 60 * time.Second} + + bearerToken := makeAuthContainerTokenRequest(ctx, t, bearer, httpClient) + + request, err := http.NewRequest(http.MethodDelete, testHost+"/v1/containers/"+cnrID.String(), nil) + require.NoError(t, err) + request = request.WithContext(ctx) + prepareBearerHeaders(request.Header, bearerToken) + + resp, err := httpClient.Do(request) + require.NoError(t, err) + defer func() { + err := resp.Body.Close() + require.NoError(t, err) + }() + if resp.StatusCode != http.StatusNoContent { + fmt.Println("resp") + } + require.Equal(t, http.StatusNoContent, resp.StatusCode) + + var prm pool.PrmContainerGet + prm.SetContainerID(*cnrID) + + _, err = clientPool.GetContainer(ctx, prm) + require.Error(t, err) + require.Contains(t, err.Error(), "not found") +} + +func makeAuthContainerTokenRequest(ctx context.Context, t *testing.T, bearer *models.Bearer, httpClient *http.Client) *handlers.BearerToken { + key, err := keys.NewPrivateKeyFromHex(devenvPrivateKey) + require.NoError(t, err) + + hexPubKey := hex.EncodeToString(key.PublicKey().Bytes()) + + data, err := json.Marshal(bearer) + require.NoError(t, err) + + request, err := http.NewRequest(http.MethodPost, testHost+"/v1/auth", bytes.NewReader(data)) + require.NoError(t, err) + request = request.WithContext(ctx) + request.Header.Add("Content-Type", "application/json") + request.Header.Add(XNeofsTokenScope, string(models.TokenTypeContainer)) + request.Header.Add(XNeofsTokenSignatureKey, hexPubKey) + + resp, err := httpClient.Do(request) + require.NoError(t, err) + defer func() { + err := resp.Body.Close() + require.NoError(t, err) + }() + + rr, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + if resp.StatusCode != http.StatusOK { + fmt.Println("auth response", string(rr)) + } + require.Equal(t, http.StatusOK, resp.StatusCode) + + stokenResp := &models.TokenResponse{} + err = json.Unmarshal(rr, stokenResp) + require.NoError(t, err) + + require.Equal(t, *stokenResp.Type, models.TokenTypeContainer) + + binaryData, err := base64.StdEncoding.DecodeString(*stokenResp.Token) + require.NoError(t, err) + + signatureData := signData(t, key, binaryData) + signature := base64.StdEncoding.EncodeToString(signatureData) + + bt := handlers.BearerToken{ + Token: *stokenResp.Token, + Signature: signature, + Key: hexPubKey, + } + + fmt.Printf("container token:\n%+v\n", bt) + return &bt } func signData(t *testing.T, key *keys.PrivateKey, data []byte) []byte { @@ -304,48 +403,15 @@ func signData(t *testing.T, key *keys.PrivateKey, data []byte) []byte { } func restContainerPut(ctx context.Context, t *testing.T, clientPool *pool.Pool) { - key, err := keys.NewPrivateKeyFromHex(devenvPrivateKey) - require.NoError(t, err) - - b := models.Bearer{ + bearer := &models.Bearer{ Container: &models.Rule{ Verb: models.NewVerb(models.VerbPUT), }, } - data, err := json.Marshal(&b) - require.NoError(t, err) + httpClient := &http.Client{Timeout: 30 * time.Second} - request0, err := http.NewRequest(http.MethodPost, testHost+"/v1/auth", bytes.NewReader(data)) - require.NoError(t, err) - request0.Header.Add("Content-Type", "application/json") - request0.Header.Add(XNeofsTokenScope, "container") - request0.Header.Add(XNeofsTokenSignatureKey, hex.EncodeToString(key.PublicKey().Bytes())) - - httpClient := http.Client{ - Timeout: 30 * time.Second, - } - - resp, err := httpClient.Do(request0) - require.NoError(t, err) - defer resp.Body.Close() - - rr, err := io.ReadAll(resp.Body) - require.NoError(t, err) - fmt.Println(string(rr)) - require.Equal(t, http.StatusOK, resp.StatusCode) - - stokenResp := &models.TokenResponse{} - err = json.Unmarshal(rr, stokenResp) - require.NoError(t, err) - - require.Equal(t, *stokenResp.Type, models.TokenTypeContainer) - - bearerBase64 := stokenResp.Token - binaryData, err := base64.StdEncoding.DecodeString(*bearerBase64) - require.NoError(t, err) - - signatureData := signData(t, key, binaryData) + bearerToken := makeAuthContainerTokenRequest(ctx, t, bearer, httpClient) attrKey, attrValue := "User-Attribute", "user value" @@ -360,15 +426,18 @@ func restContainerPut(ctx context.Context, t *testing.T, clientPool *pool.Pool) body, err := json.Marshal(&req) require.NoError(t, err) - fmt.Println(base64.StdEncoding.EncodeToString(signatureData)) - fmt.Println(hex.EncodeToString(key.PublicKey().Bytes())) + reqURL, err := url.Parse(testHost + "/v1/containers") + require.NoError(t, err) - request, err := http.NewRequest(http.MethodPut, testHost+"/v1/containers", bytes.NewReader(body)) + query := reqURL.Query() + query.Add("skip-native-name", "true") + + reqURL.RawQuery = query.Encode() + + request, err := http.NewRequest(http.MethodPut, reqURL.String(), bytes.NewReader(body)) require.NoError(t, err) request.Header.Add("Content-Type", "application/json") - request.Header.Add(XNeofsTokenSignature, base64.StdEncoding.EncodeToString(signatureData)) - request.Header.Add("Authorization", "Bearer "+*bearerBase64) - request.Header.Add(XNeofsTokenSignatureKey, hex.EncodeToString(key.PublicKey().Bytes())) + prepareBearerHeaders(request.Header, bearerToken) request.Header.Add("X-Attribute-"+attrKey, attrValue) resp2, err := httpClient.Do(request) @@ -406,14 +475,20 @@ func restContainerPut(ctx context.Context, t *testing.T, clientPool *pool.Pool) } } -func createContainer(ctx context.Context, t *testing.T, clientPool *pool.Pool) (*cid.ID, error) { +func prepareBearerHeaders(header http.Header, bearerToken *handlers.BearerToken) { + header.Add(XNeofsTokenSignature, bearerToken.Signature) + header.Add("Authorization", "Bearer "+bearerToken.Token) + header.Add(XNeofsTokenSignatureKey, bearerToken.Key) +} + +func createContainer(ctx context.Context, t *testing.T, clientPool *pool.Pool, name string) *cid.ID { pp, err := policy.Parse("REP 1") require.NoError(t, err) cnr := container.New( container.WithPolicy(pp), container.WithCustomBasicACL(0x0FFFFFFF), - container.WithAttribute(container.AttributeName, "friendlyName"), + container.WithAttribute(container.AttributeName, name), container.WithAttribute(container.AttributeTimestamp, strconv.FormatInt(time.Now().Unix(), 10))) cnr.SetOwnerID(clientPool.OwnerID()) @@ -426,12 +501,9 @@ func createContainer(ctx context.Context, t *testing.T, clientPool *pool.Pool) ( prm.SetWaitParams(waitPrm) CID, err := clientPool.PutContainer(ctx, prm) - if err != nil { - return nil, err - } - fmt.Println(CID.String()) + require.NoError(t, err) - return CID, err + return CID } func restrictByEACL(ctx context.Context, t *testing.T, clientPool *pool.Pool, cnrID *cid.ID) { diff --git a/gen/restapi/embedded_spec.go b/gen/restapi/embedded_spec.go index a60e76f..79df4df 100644 --- a/gen/restapi/embedded_spec.go +++ b/gen/restapi/embedded_spec.go @@ -98,6 +98,13 @@ func init() { "summary": "Create new container in NeoFS", "operationId": "putContainer", "parameters": [ + { + "type": "boolean", + "default": false, + "description": "Provide this parameter to skip registration container name in NNS service", + "name": "skip-native-name", + "in": "query" + }, { "description": "Container info", "name": "container", @@ -167,15 +174,6 @@ func init() { "security": [], "summary": "Get container by id", "operationId": "getContainer", - "parameters": [ - { - "type": "string", - "description": "Base58 encoded container id", - "name": "containerId", - "in": "path", - "required": true - } - ], "responses": { "200": { "description": "Container info", @@ -190,7 +188,39 @@ func init() { } } } - } + }, + "delete": { + "summary": "Delete container by id", + "operationId": "deleteContainer", + "parameters": [ + { + "$ref": "#/parameters/signatureParam" + }, + { + "$ref": "#/parameters/signatureKeyParam" + } + ], + "responses": { + "204": { + "description": "Successul deletion" + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "parameters": [ + { + "type": "string", + "description": "Base58 encoded container id", + "name": "containerId", + "in": "path", + "required": true + } + ] }, "/objects": { "put": { @@ -745,6 +775,13 @@ func init() { "summary": "Create new container in NeoFS", "operationId": "putContainer", "parameters": [ + { + "type": "boolean", + "default": false, + "description": "Provide this parameter to skip registration container name in NNS service", + "name": "skip-native-name", + "in": "query" + }, { "description": "Container info", "name": "container", @@ -822,15 +859,6 @@ func init() { "security": [], "summary": "Get container by id", "operationId": "getContainer", - "parameters": [ - { - "type": "string", - "description": "Base58 encoded container id", - "name": "containerId", - "in": "path", - "required": true - } - ], "responses": { "200": { "description": "Container info", @@ -845,7 +873,47 @@ func init() { } } } - } + }, + "delete": { + "summary": "Delete container by id", + "operationId": "deleteContainer", + "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 + } + ], + "responses": { + "204": { + "description": "Successul deletion" + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "parameters": [ + { + "type": "string", + "description": "Base58 encoded container id", + "name": "containerId", + "in": "path", + "required": true + } + ] }, "/objects": { "put": { diff --git a/gen/restapi/operations/delete_container.go b/gen/restapi/operations/delete_container.go new file mode 100644 index 0000000..7b5a326 --- /dev/null +++ b/gen/restapi/operations/delete_container.go @@ -0,0 +1,71 @@ +// 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" + + "github.com/nspcc-dev/neofs-rest-gw/gen/models" +) + +// DeleteContainerHandlerFunc turns a function with the right signature into a delete container handler +type DeleteContainerHandlerFunc func(DeleteContainerParams, *models.Principal) middleware.Responder + +// Handle executing the request and returning a response +func (fn DeleteContainerHandlerFunc) Handle(params DeleteContainerParams, principal *models.Principal) middleware.Responder { + return fn(params, principal) +} + +// DeleteContainerHandler interface for that can handle valid delete container params +type DeleteContainerHandler interface { + Handle(DeleteContainerParams, *models.Principal) middleware.Responder +} + +// NewDeleteContainer creates a new http.Handler for the delete container operation +func NewDeleteContainer(ctx *middleware.Context, handler DeleteContainerHandler) *DeleteContainer { + return &DeleteContainer{Context: ctx, Handler: handler} +} + +/* DeleteContainer swagger:route DELETE /containers/{containerId} deleteContainer + +Delete container by id + +*/ +type DeleteContainer struct { + Context *middleware.Context + Handler DeleteContainerHandler +} + +func (o *DeleteContainer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + *r = *rCtx + } + var Params = NewDeleteContainerParams() + uprinc, aCtx, err := o.Context.Authorize(r, route) + if err != nil { + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + if aCtx != nil { + *r = *aCtx + } + var principal *models.Principal + if uprinc != nil { + principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise + } + + 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, principal) // actually handle the request + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/gen/restapi/operations/delete_container_parameters.go b/gen/restapi/operations/delete_container_parameters.go new file mode 100644 index 0000000..3665b2c --- /dev/null +++ b/gen/restapi/operations/delete_container_parameters.go @@ -0,0 +1,130 @@ +// 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/middleware" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/validate" +) + +// NewDeleteContainerParams creates a new DeleteContainerParams object +// +// There are no default values defined in the spec. +func NewDeleteContainerParams() DeleteContainerParams { + + return DeleteContainerParams{} +} + +// DeleteContainerParams contains all the bound params for the delete container operation +// typically these are obtained from a http.Request +// +// swagger:parameters deleteContainer +type DeleteContainerParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /*Base64 encoded signature for bearer token + Required: true + In: header + */ + XNeofsTokenSignature string + /*Hex encoded the public part of the key that signed the bearer token + Required: true + In: header + */ + XNeofsTokenSignatureKey string + /*Base58 encoded container id + Required: true + In: path + */ + ContainerID 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 NewDeleteContainerParams() beforehand. +func (o *DeleteContainerParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if err := o.bindXNeofsTokenSignature(r.Header[http.CanonicalHeaderKey("X-Neofs-Token-Signature")], true, route.Formats); err != nil { + res = append(res, err) + } + + if err := o.bindXNeofsTokenSignatureKey(r.Header[http.CanonicalHeaderKey("X-Neofs-Token-Signature-Key")], true, route.Formats); err != nil { + res = append(res, err) + } + + rContainerID, rhkContainerID, _ := route.Params.GetOK("containerId") + if err := o.bindContainerID(rContainerID, rhkContainerID, route.Formats); err != nil { + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindXNeofsTokenSignature binds and validates parameter XNeofsTokenSignature from header. +func (o *DeleteContainerParams) bindXNeofsTokenSignature(rawData []string, hasKey bool, formats strfmt.Registry) error { + if !hasKey { + return errors.Required("X-Neofs-Token-Signature", "header", rawData) + } + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + + if err := validate.RequiredString("X-Neofs-Token-Signature", "header", raw); err != nil { + return err + } + o.XNeofsTokenSignature = raw + + return nil +} + +// bindXNeofsTokenSignatureKey binds and validates parameter XNeofsTokenSignatureKey from header. +func (o *DeleteContainerParams) bindXNeofsTokenSignatureKey(rawData []string, hasKey bool, formats strfmt.Registry) error { + if !hasKey { + return errors.Required("X-Neofs-Token-Signature-Key", "header", rawData) + } + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + + if err := validate.RequiredString("X-Neofs-Token-Signature-Key", "header", raw); err != nil { + return err + } + o.XNeofsTokenSignatureKey = raw + + return nil +} + +// bindContainerID binds and validates parameter ContainerID from path. +func (o *DeleteContainerParams) bindContainerID(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + o.ContainerID = raw + + return nil +} diff --git a/gen/restapi/operations/delete_container_responses.go b/gen/restapi/operations/delete_container_responses.go new file mode 100644 index 0000000..08ecd32 --- /dev/null +++ b/gen/restapi/operations/delete_container_responses.go @@ -0,0 +1,80 @@ +// 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" +) + +// DeleteContainerNoContentCode is the HTTP code returned for type DeleteContainerNoContent +const DeleteContainerNoContentCode int = 204 + +/*DeleteContainerNoContent Successul deletion + +swagger:response deleteContainerNoContent +*/ +type DeleteContainerNoContent struct { +} + +// NewDeleteContainerNoContent creates DeleteContainerNoContent with default headers values +func NewDeleteContainerNoContent() *DeleteContainerNoContent { + + return &DeleteContainerNoContent{} +} + +// WriteResponse to the client +func (o *DeleteContainerNoContent) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.Header().Del(runtime.HeaderContentType) //Remove Content-Type on empty responses + + rw.WriteHeader(204) +} + +// DeleteContainerBadRequestCode is the HTTP code returned for type DeleteContainerBadRequest +const DeleteContainerBadRequestCode int = 400 + +/*DeleteContainerBadRequest Bad request + +swagger:response deleteContainerBadRequest +*/ +type DeleteContainerBadRequest struct { + + /* + In: Body + */ + Payload models.Error `json:"body,omitempty"` +} + +// NewDeleteContainerBadRequest creates DeleteContainerBadRequest with default headers values +func NewDeleteContainerBadRequest() *DeleteContainerBadRequest { + + return &DeleteContainerBadRequest{} +} + +// WithPayload adds the payload to the delete container bad request response +func (o *DeleteContainerBadRequest) WithPayload(payload models.Error) *DeleteContainerBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the delete container bad request response +func (o *DeleteContainerBadRequest) SetPayload(payload models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *DeleteContainerBadRequest) 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 bd9d25d..d4ff810 100644 --- a/gen/restapi/operations/neofs_rest_gw_api.go +++ b/gen/restapi/operations/neofs_rest_gw_api.go @@ -47,6 +47,9 @@ func NewNeofsRestGwAPI(spec *loads.Document) *NeofsRestGwAPI { AuthHandler: AuthHandlerFunc(func(params AuthParams) middleware.Responder { return middleware.NotImplemented("operation Auth has not yet been implemented") }), + DeleteContainerHandler: DeleteContainerHandlerFunc(func(params DeleteContainerParams, principal *models.Principal) middleware.Responder { + return middleware.NotImplemented("operation DeleteContainer has not yet been implemented") + }), GetContainerHandler: GetContainerHandlerFunc(func(params GetContainerParams) middleware.Responder { return middleware.NotImplemented("operation GetContainer has not yet been implemented") }), @@ -111,6 +114,8 @@ type NeofsRestGwAPI struct { // AuthHandler sets the operation handler for the auth operation AuthHandler AuthHandler + // DeleteContainerHandler sets the operation handler for the delete container operation + DeleteContainerHandler DeleteContainerHandler // GetContainerHandler sets the operation handler for the get container operation GetContainerHandler GetContainerHandler // GetObjectInfoHandler sets the operation handler for the get object info operation @@ -203,6 +208,9 @@ func (o *NeofsRestGwAPI) Validate() error { if o.AuthHandler == nil { unregistered = append(unregistered, "AuthHandler") } + if o.DeleteContainerHandler == nil { + unregistered = append(unregistered, "DeleteContainerHandler") + } if o.GetContainerHandler == nil { unregistered = append(unregistered, "GetContainerHandler") } @@ -318,6 +326,10 @@ func (o *NeofsRestGwAPI) initHandlerCache() { o.handlers["POST"] = make(map[string]http.Handler) } o.handlers["POST"]["/auth"] = NewAuth(o.context, o.AuthHandler) + if o.handlers["DELETE"] == nil { + o.handlers["DELETE"] = make(map[string]http.Handler) + } + o.handlers["DELETE"]["/containers/{containerId}"] = NewDeleteContainer(o.context, o.DeleteContainerHandler) if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) } diff --git a/gen/restapi/operations/put_container_parameters.go b/gen/restapi/operations/put_container_parameters.go index 461d1f9..82cb3ab 100644 --- a/gen/restapi/operations/put_container_parameters.go +++ b/gen/restapi/operations/put_container_parameters.go @@ -14,15 +14,23 @@ import ( "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" ) // NewPutContainerParams creates a new PutContainerParams object -// -// There are no default values defined in the spec. +// with the default values initialized. func NewPutContainerParams() PutContainerParams { - return PutContainerParams{} + var ( + // initialize parameters with default values + + skipNativeNameDefault = bool(false) + ) + + return PutContainerParams{ + SkipNativeName: &skipNativeNameDefault, + } } // PutContainerParams contains all the bound params for the put container operation @@ -49,6 +57,11 @@ type PutContainerParams struct { In: body */ Container PutContainerBody + /*Provide this parameter to skip registration container name in NNS service + In: query + Default: false + */ + SkipNativeName *bool } // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface @@ -60,6 +73,8 @@ func (o *PutContainerParams) BindRequest(r *http.Request, route *middleware.Matc o.HTTPRequest = r + qs := runtime.Values(r.URL.Query()) + if err := o.bindXNeofsTokenSignature(r.Header[http.CanonicalHeaderKey("X-Neofs-Token-Signature")], true, route.Formats); err != nil { res = append(res, err) } @@ -95,6 +110,11 @@ func (o *PutContainerParams) BindRequest(r *http.Request, route *middleware.Matc } else { res = append(res, errors.Required("container", "body", "")) } + + qSkipNativeName, qhkSkipNativeName, _ := qs.GetOK("skip-native-name") + if err := o.bindSkipNativeName(qSkipNativeName, qhkSkipNativeName, route.Formats); err != nil { + res = append(res, err) + } if len(res) > 0 { return errors.CompositeValidationError(res...) } @@ -140,3 +160,27 @@ func (o *PutContainerParams) bindXNeofsTokenSignatureKey(rawData []string, hasKe return nil } + +// bindSkipNativeName binds and validates parameter SkipNativeName from query. +func (o *PutContainerParams) bindSkipNativeName(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 NewPutContainerParams() + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("skip-native-name", "query", "bool", raw) + } + o.SkipNativeName = &value + + return nil +} diff --git a/go.mod b/go.mod index 0c50663..8eb1fa7 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/google/uuid v1.3.0 github.com/nspcc-dev/neo-go v0.98.2 github.com/nspcc-dev/neofs-api-go/v2 v2.12.1 - github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.3.0.20220407103316-e50e6d28280d + github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.3.0.20220412151250-3e75660802ae github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.0 github.com/testcontainers/testcontainers-go v0.13.0 diff --git a/go.sum b/go.sum index 3695fd4..cb09ac5 100644 --- a/go.sum +++ b/go.sum @@ -750,8 +750,8 @@ github.com/nspcc-dev/neofs-crypto v0.3.0 h1:zlr3pgoxuzrmGCxc5W8dGVfA9Rro8diFvVnB github.com/nspcc-dev/neofs-crypto v0.3.0/go.mod h1:8w16GEJbH6791ktVqHN9YRNH3s9BEEKYxGhlFnp0cDw= github.com/nspcc-dev/neofs-sdk-go v0.0.0-20211201182451-a5b61c4f6477/go.mod h1:dfMtQWmBHYpl9Dez23TGtIUKiFvCIxUZq/CkSIhEpz4= github.com/nspcc-dev/neofs-sdk-go v0.0.0-20220113123743-7f3162110659/go.mod h1:/jay1lr3w7NQd/VDBkEhkJmDmyPNsu4W+QV2obsUV40= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.3.0.20220407103316-e50e6d28280d h1:OHyq8+zyQtARFWj3quRPabcfQWJZEiU7HYp6QGCSjaM= -github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.3.0.20220407103316-e50e6d28280d/go.mod h1:Hl7a1l0ntZ4b1ZABpGX6fuAuFS3c6+hyMCUNVvZv/w4= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.3.0.20220412151250-3e75660802ae h1:xcoEwEwZXu784Re1PPE35vm1A4+sUCVgMuGFqQPnN1Q= +github.com/nspcc-dev/neofs-sdk-go v1.0.0-rc.3.0.20220412151250-3e75660802ae/go.mod h1:Hl7a1l0ntZ4b1ZABpGX6fuAuFS3c6+hyMCUNVvZv/w4= github.com/nspcc-dev/rfc6979 v0.1.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso= github.com/nspcc-dev/rfc6979 v0.2.0 h1:3e1WNxrN60/6N0DW7+UYisLeZJyfqZTNOjeV/toYvOE= github.com/nspcc-dev/rfc6979 v0.2.0/go.mod h1:exhIh1PdpDC5vQmyEsGvc4YDM/lyQp/452QxGq/UEso= diff --git a/handlers/api.go b/handlers/api.go index 94e5f6f..c6c11e7 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -68,6 +68,7 @@ func (a *API) Configure(api *operations.NeofsRestGwAPI) http.Handler { api.PutContainerHandler = operations.PutContainerHandlerFunc(a.PutContainers) api.GetContainerHandler = operations.GetContainerHandlerFunc(a.GetContainer) + api.DeleteContainerHandler = operations.DeleteContainerHandlerFunc(a.DeleteContainer) api.BearerAuthAuth = func(s string) (*models.Principal, error) { if !strings.HasPrefix(s, BearerPrefix) { @@ -99,7 +100,7 @@ func (a *API) setupGlobalMiddleware(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestID := uuid.NewString() a.log.Info("request", zap.String("remote", r.RemoteAddr), - zap.String("method", r.Method), zap.String("uri", r.RequestURI), + zap.String("method", r.Method), zap.String("url", r.URL.String()), zap.String("id", requestID)) ctx := context.WithValue(r.Context(), ContextKeyRequestID, requestID) diff --git a/handlers/containers.go b/handlers/containers.go index 3406ec4..e270112 100644 --- a/handlers/containers.go +++ b/handlers/containers.go @@ -21,6 +21,7 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/policy" "github.com/nspcc-dev/neofs-sdk-go/pool" "github.com/nspcc-dev/neofs-sdk-go/session" + "go.uber.org/zap" ) const ( @@ -42,7 +43,7 @@ func (a *API) PutContainers(params operations.PutContainerParams, principal *mod userAttributes := prepareUserAttributes(params.HTTPRequest.Header) - cnrID, err := createContainer(params.HTTPRequest.Context(), a.pool, stoken, ¶ms.Container, userAttributes) + cnrID, err := createContainer(params.HTTPRequest.Context(), a.pool, stoken, ¶ms, userAttributes) if err != nil { return wrapError(err) } @@ -80,6 +81,37 @@ func (a *API) GetContainer(params operations.GetContainerParams) middleware.Resp return operations.NewGetContainerOK().WithPayload(resp) } +// DeleteContainer handler that returns container info. +func (a *API) DeleteContainer(params operations.DeleteContainerParams, principal *models.Principal) middleware.Responder { + bt := &BearerToken{ + Token: string(*principal), + Signature: params.XNeofsTokenSignature, + Key: params.XNeofsTokenSignatureKey, + } + stoken, err := prepareSessionToken(bt) + if err != nil { + a.log.Error("failed parse session token", zap.Error(err)) + return operations.NewDeleteContainerBadRequest().WithPayload(NewError(err)) + } + + cnrID, err := parseContainerID(params.ContainerID) + if err != nil { + a.log.Error("failed get container id", zap.Error(err)) + return operations.NewDeleteContainerBadRequest().WithPayload(NewError(err)) + } + + var prm pool.PrmContainerDelete + prm.SetContainerID(*cnrID) + prm.SetSessionToken(*stoken) + + if err = a.pool.DeleteContainer(params.HTTPRequest.Context(), prm); err != nil { + a.log.Error("failed delete container", zap.String("container", params.ContainerID), zap.Error(err)) + return operations.NewDeleteContainerBadRequest().WithPayload(NewError(err)) + } + + return operations.NewDeleteContainerNoContent() +} + func prepareUserAttributes(header http.Header) map[string]string { filtered := filterHeaders(header) delete(filtered, container.AttributeName) @@ -88,18 +120,29 @@ func prepareUserAttributes(header http.Header) map[string]string { } func getContainer(ctx context.Context, p *pool.Pool, containerID string) (*container.Container, error) { + cnrID, err := parseContainerID(containerID) + if err != nil { + return nil, err + } + + var prm pool.PrmContainerGet + prm.SetContainerID(*cnrID) + + return p.GetContainer(ctx, prm) +} + +func parseContainerID(containerID string) (*cid.ID, error) { var cnrID cid.ID if err := cnrID.Parse(containerID); err != nil { return nil, fmt.Errorf("parse container id '%s': %w", containerID, err) } - var prm pool.PrmContainerGet - prm.SetContainerID(cnrID) - - return p.GetContainer(ctx, prm) + return &cnrID, nil } -func createContainer(ctx context.Context, p *pool.Pool, stoken *session.Token, request *operations.PutContainerBody, userAttrs map[string]string) (*cid.ID, error) { +func createContainer(ctx context.Context, p *pool.Pool, stoken *session.Token, params *operations.PutContainerParams, userAttrs map[string]string) (*cid.ID, error) { + request := params.Container + if request.PlacementPolicy == "" { request.PlacementPolicy = defaultPlacementPolicy } @@ -131,7 +174,9 @@ func createContainer(ctx context.Context, p *pool.Pool, stoken *session.Token, r cnr.SetOwnerID(stoken.OwnerID()) cnr.SetSessionToken(stoken) - container.SetNativeName(cnr, *request.ContainerName) + if !*params.SkipNativeName { // we don't check for nil because there is default false value + container.SetNativeName(cnr, *request.ContainerName) + } var prm pool.PrmContainerPut prm.SetContainer(*cnr) diff --git a/spec/rest.yaml b/spec/rest.yaml index 839f272..762929a 100644 --- a/spec/rest.yaml +++ b/spec/rest.yaml @@ -167,6 +167,11 @@ paths: operationId: putContainer summary: Create new container in NeoFS parameters: + - in: query + name: skip-native-name + description: Provide this parameter to skip registration container name in NNS service + type: boolean + default: false - in: body name: container required: true @@ -204,16 +209,16 @@ paths: $ref: '#/definitions/Error' /containers/{containerId}: + parameters: + - in: path + name: containerId + type: string + required: true + description: Base58 encoded container id get: operationId: getContainer summary: Get container by id security: [ ] - parameters: - - in: path - name: containerId - type: string - required: true - description: Base58 encoded container id responses: 200: description: Container info @@ -223,6 +228,19 @@ paths: description: Bad request schema: $ref: '#/definitions/Error' + delete: + operationId: deleteContainer + summary: Delete container by id + parameters: + - $ref: '#/parameters/signatureParam' + - $ref: '#/parameters/signatureKeyParam' + responses: + 204: + description: Successul deletion + 400: + description: Bad request + schema: + $ref: '#/definitions/Error' definitions: Bearer: