diff --git a/cmd/neofs-rest-gw/integration_test.go b/cmd/neofs-rest-gw/integration_test.go index a58c062..ff63817 100644 --- a/cmd/neofs-rest-gw/integration_test.go +++ b/cmd/neofs-rest-gw/integration_test.go @@ -322,7 +322,7 @@ func mixTokens(ctx context.Context, t *testing.T, cnrID cid.ID) { func checkPutContainerWithError(t *testing.T, httpClient *http.Client, token *handlers.BearerToken) { reqURL, err := url.Parse(testHost + "/v1/containers") require.NoError(t, err) - body, err := json.Marshal(&operations.PutContainerBody{ContainerName: "container"}) + body, err := json.Marshal(&models.ContainerPutInfo{ContainerName: "container"}) require.NoError(t, err) request, err := http.NewRequest(http.MethodPut, reqURL.String(), bytes.NewReader(body)) require.NoError(t, err) @@ -692,6 +692,7 @@ func restContainerGet(ctx context.Context, t *testing.T, owner user.ID, cnrID ci require.Equal(t, cnrID.EncodeToString(), *cnrInfo.ContainerID) require.Equal(t, owner.EncodeToString(), *cnrInfo.OwnerID) + require.Equal(t, containerName, *cnrInfo.ContainerName) } func restContainerDelete(ctx context.Context, t *testing.T, clientPool *pool.Pool, owner user.ID) { @@ -953,7 +954,7 @@ func restContainerPutInvalid(ctx context.Context, t *testing.T) { query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) reqURL.RawQuery = query.Encode() - body, err := json.Marshal(&operations.PutContainerBody{ContainerName: "nameWithCapitalLetters"}) + body, err := json.Marshal(&models.ContainerPutInfo{ContainerName: "nameWithCapitalLetters"}) require.NoError(t, err) request, err := http.NewRequest(http.MethodPut, reqURL.String(), bytes.NewReader(body)) require.NoError(t, err) @@ -982,7 +983,7 @@ func restContainerPut(ctx context.Context, t *testing.T, clientPool *pool.Pool) } // try to create container without name but with name-scope-global - body, err := json.Marshal(&operations.PutContainerBody{}) + body, err := json.Marshal(&models.ContainerPutInfo{}) require.NoError(t, err) reqURL, err := url.Parse(testHost + "/v1/containers") @@ -999,7 +1000,13 @@ func restContainerPut(ctx context.Context, t *testing.T, clientPool *pool.Pool) doRequest(t, httpClient, request, http.StatusBadRequest, nil) // create container with name in local scope - body, err = json.Marshal(&operations.PutContainerBody{}) + containerPutInfo := &models.ContainerPutInfo{ + Attributes: []*models.Attribute{{ + Key: util.NewString(attrKey), + Value: util.NewString(attrValue), + }}, + } + body, err = json.Marshal(containerPutInfo) require.NoError(t, err) reqURL, err = url.Parse(testHost + "/v1/containers") @@ -1011,7 +1018,6 @@ func restContainerPut(ctx context.Context, t *testing.T, clientPool *pool.Pool) request, err = http.NewRequest(http.MethodPut, reqURL.String(), bytes.NewReader(body)) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) - request.Header.Add("X-Attribute-"+attrKey, attrValue) addr := &operations.PutContainerOKBody{} doRequest(t, httpClient, request, http.StatusOK, addr) diff --git a/gen/models/container_info.go b/gen/models/container_info.go index 21ca821..31e9823 100644 --- a/gen/models/container_info.go +++ b/gen/models/container_info.go @@ -33,6 +33,10 @@ type ContainerInfo struct { // Required: true ContainerID *string `json:"containerId"` + // container name + // Required: true + ContainerName *string `json:"containerName"` + // owner Id // Required: true OwnerID *string `json:"ownerId"` @@ -62,6 +66,10 @@ func (m *ContainerInfo) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateContainerName(formats); err != nil { + res = append(res, err) + } + if err := m.validateOwnerID(formats); err != nil { res = append(res, err) } @@ -125,6 +133,15 @@ func (m *ContainerInfo) validateContainerID(formats strfmt.Registry) error { return nil } +func (m *ContainerInfo) validateContainerName(formats strfmt.Registry) error { + + if err := validate.Required("containerName", "body", m.ContainerName); err != nil { + return err + } + + return nil +} + func (m *ContainerInfo) validateOwnerID(formats strfmt.Registry) error { if err := validate.Required("ownerId", "body", m.OwnerID); err != nil { diff --git a/gen/models/container_put_info.go b/gen/models/container_put_info.go new file mode 100644 index 0000000..8e0b349 --- /dev/null +++ b/gen/models/container_put_info.go @@ -0,0 +1,126 @@ +// 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" +) + +// ContainerPutInfo Represent request body to create container. To specify container name use appropriate property (name provided in attributes will be ignored). +// Example: {"attributes":[{"key":"Custom-Attribute","value":"value"}],"basicAcl":"public-read-write","containerName":"container","placementPolicy":"REP 3"} +// +// swagger:model ContainerPutInfo +type ContainerPutInfo struct { + + // attributes + Attributes []*Attribute `json:"attributes"` + + // basic Acl + BasicACL string `json:"basicAcl,omitempty"` + + // container name + ContainerName string `json:"containerName,omitempty"` + + // placement policy + PlacementPolicy string `json:"placementPolicy,omitempty"` +} + +// Validate validates this container put info +func (m *ContainerPutInfo) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateAttributes(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ContainerPutInfo) validateAttributes(formats strfmt.Registry) error { + if swag.IsZero(m.Attributes) { // not required + return nil + } + + for i := 0; i < len(m.Attributes); i++ { + if swag.IsZero(m.Attributes[i]) { // not required + continue + } + + if m.Attributes[i] != nil { + if err := m.Attributes[i].Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("attributes" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("attributes" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// ContextValidate validate this container put info based on the context it is used +func (m *ContainerPutInfo) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateAttributes(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *ContainerPutInfo) contextValidateAttributes(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.Attributes); i++ { + + if m.Attributes[i] != nil { + if err := m.Attributes[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("attributes" + "." + strconv.Itoa(i)) + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("attributes" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + +// MarshalBinary interface implementation +func (m *ContainerPutInfo) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *ContainerPutInfo) UnmarshalBinary(b []byte) error { + var res ContainerPutInfo + 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 542ab34..e8435fa 100644 --- a/gen/restapi/embedded_spec.go +++ b/gen/restapi/embedded_spec.go @@ -159,23 +159,7 @@ func init() { "in": "body", "required": true, "schema": { - "type": "object", - "properties": { - "basicAcl": { - "type": "string" - }, - "containerName": { - "type": "string" - }, - "placementPolicy": { - "type": "string" - } - }, - "example": { - "basicAcl": "public-read-write", - "containerName": "container", - "placementPolicy": "REP 3" - } + "$ref": "#/definitions/ContainerPutInfo" } } ], @@ -574,6 +558,7 @@ func init() { "type": "object", "required": [ "containerId", + "containerName", "version", "ownerId", "basicAcl", @@ -593,6 +578,9 @@ func init() { "containerId": { "type": "string" }, + "containerName": { + "type": "string" + }, "ownerId": { "type": "string" }, @@ -639,6 +627,38 @@ func init() { } } }, + "ContainerPutInfo": { + "description": "Represent request body to create container. To specify container name use appropriate property (name provided in attributes will be ignored).", + "type": "object", + "properties": { + "attributes": { + "type": "array", + "items": { + "$ref": "#/definitions/Attribute" + } + }, + "basicAcl": { + "type": "string" + }, + "containerName": { + "type": "string" + }, + "placementPolicy": { + "type": "string" + } + }, + "example": { + "attributes": [ + { + "key": "Custom-Attribute", + "value": "value" + } + ], + "basicAcl": "public-read-write", + "containerName": "container", + "placementPolicy": "REP 3" + } + }, "Eacl": { "type": "object", "required": [ @@ -1252,23 +1272,7 @@ func init() { "in": "body", "required": true, "schema": { - "type": "object", - "properties": { - "basicAcl": { - "type": "string" - }, - "containerName": { - "type": "string" - }, - "placementPolicy": { - "type": "string" - } - }, - "example": { - "basicAcl": "public-read-write", - "containerName": "container", - "placementPolicy": "REP 3" - } + "$ref": "#/definitions/ContainerPutInfo" } } ], @@ -1750,6 +1754,7 @@ func init() { "type": "object", "required": [ "containerId", + "containerName", "version", "ownerId", "basicAcl", @@ -1769,6 +1774,9 @@ func init() { "containerId": { "type": "string" }, + "containerName": { + "type": "string" + }, "ownerId": { "type": "string" }, @@ -1815,6 +1823,38 @@ func init() { } } }, + "ContainerPutInfo": { + "description": "Represent request body to create container. To specify container name use appropriate property (name provided in attributes will be ignored).", + "type": "object", + "properties": { + "attributes": { + "type": "array", + "items": { + "$ref": "#/definitions/Attribute" + } + }, + "basicAcl": { + "type": "string" + }, + "containerName": { + "type": "string" + }, + "placementPolicy": { + "type": "string" + } + }, + "example": { + "attributes": [ + { + "key": "Custom-Attribute", + "value": "value" + } + ], + "basicAcl": "public-read-write", + "containerName": "container", + "placementPolicy": "REP 3" + } + }, "Eacl": { "type": "object", "required": [ diff --git a/gen/restapi/operations/put_container.go b/gen/restapi/operations/put_container.go index 7092413..21522c2 100644 --- a/gen/restapi/operations/put_container.go +++ b/gen/restapi/operations/put_container.go @@ -75,50 +75,6 @@ func (o *PutContainer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { } -// PutContainerBody put container body -// Example: {"basicAcl":"public-read-write","containerName":"container","placementPolicy":"REP 3"} -// -// swagger:model PutContainerBody -type PutContainerBody struct { - - // basic Acl - BasicACL string `json:"basicAcl,omitempty"` - - // container name - ContainerName string `json:"containerName,omitempty"` - - // placement policy - PlacementPolicy string `json:"placementPolicy,omitempty"` -} - -// Validate validates this put container body -func (o *PutContainerBody) Validate(formats strfmt.Registry) error { - return nil -} - -// ContextValidate validates this put container body based on context it is used -func (o *PutContainerBody) ContextValidate(ctx context.Context, formats strfmt.Registry) error { - return nil -} - -// MarshalBinary interface implementation -func (o *PutContainerBody) MarshalBinary() ([]byte, error) { - if o == nil { - return nil, nil - } - return swag.WriteJSON(o) -} - -// UnmarshalBinary interface implementation -func (o *PutContainerBody) UnmarshalBinary(b []byte) error { - var res PutContainerBody - if err := swag.ReadJSON(b, &res); err != nil { - return err - } - *o = res - return nil -} - // PutContainerOKBody put container o k body // Example: {"containerId":"5HZTn5qkRnmgSz9gSrw22CEdPPk6nQhkwf2Mgzyvkikv"} // diff --git a/gen/restapi/operations/put_container_parameters.go b/gen/restapi/operations/put_container_parameters.go index 76e141a..718aaa6 100644 --- a/gen/restapi/operations/put_container_parameters.go +++ b/gen/restapi/operations/put_container_parameters.go @@ -16,6 +16,8 @@ import ( "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" + + "github.com/nspcc-dev/neofs-rest-gw/gen/models" ) // NewPutContainerParams creates a new PutContainerParams object @@ -59,7 +61,7 @@ type PutContainerParams struct { Required: true In: body */ - Container PutContainerBody + Container *models.ContainerPutInfo /*Provide this parameter to register container name in NNS service In: query Default: false @@ -93,7 +95,7 @@ func (o *PutContainerParams) BindRequest(r *http.Request, route *middleware.Matc if runtime.HasBody(r) { defer r.Body.Close() - var body PutContainerBody + var body models.ContainerPutInfo if err := route.Consumer.Consume(r.Body, &body); err != nil { if err == io.EOF { res = append(res, errors.Required("container", "body", "")) @@ -112,7 +114,7 @@ func (o *PutContainerParams) BindRequest(r *http.Request, route *middleware.Matc } if len(res) == 0 { - o.Container = body + o.Container = &body } } } else { diff --git a/handlers/containers.go b/handlers/containers.go index 2824f4e..89f0c06 100644 --- a/handlers/containers.go +++ b/handlers/containers.go @@ -6,12 +6,12 @@ import ( "encoding/hex" "errors" "fmt" - "net/http" "strings" "time" "github.com/go-openapi/runtime/middleware" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + containerv2 "github.com/nspcc-dev/neofs-api-go/v2/container" "github.com/nspcc-dev/neofs-api-go/v2/refs" sessionv2 "github.com/nspcc-dev/neofs-api-go/v2/session" "github.com/nspcc-dev/neofs-rest-gw/gen/models" @@ -50,9 +50,7 @@ func (a *API) PutContainers(params operations.PutContainerParams, principal *mod return operations.NewPutContainerBadRequest().WithPayload(resp) } - userAttributes := prepareUserAttributes(params.HTTPRequest.Header) - - cnrID, err := createContainer(params.HTTPRequest.Context(), a.pool, stoken, ¶ms, userAttributes) + cnrID, err := createContainer(params.HTTPRequest.Context(), a.pool, stoken, ¶ms) if err != nil { resp := a.logAndGetErrorResponse("create container", err) return operations.NewPutContainerBadRequest().WithPayload(resp) @@ -237,6 +235,7 @@ func getContainerInfo(ctx context.Context, p *pool.Pool, cnrID cid.ID) (*models. return &models.ContainerInfo{ ContainerID: util.NewString(cnrID.String()), + ContainerName: util.NewString(container.Name(*cnr)), OwnerID: util.NewString(cnr.Owner().String()), BasicACL: util.NewString(cnr.BasicACL().EncodeToString()), PlacementPolicy: util.NewString(sb.String()), @@ -244,13 +243,6 @@ func getContainerInfo(ctx context.Context, p *pool.Pool, cnrID cid.ID) (*models. }, nil } -func prepareUserAttributes(header http.Header) map[string]string { - filtered := filterHeaders(header) - delete(filtered, attributeName) - delete(filtered, attributeTimestamp) - return filtered -} - func parseContainerID(containerID string) (cid.ID, error) { var cnrID cid.ID if err := cnrID.DecodeString(containerID); err != nil { @@ -300,7 +292,7 @@ func getContainerEACL(ctx context.Context, p *pool.Pool, cnrID cid.ID) (*models. return tableResp, nil } -func createContainer(ctx context.Context, p *pool.Pool, stoken session.Container, params *operations.PutContainerParams, userAttrs map[string]string) (cid.ID, error) { +func createContainer(ctx context.Context, p *pool.Pool, stoken session.Container, params *operations.PutContainerParams) (cid.ID, error) { request := params.Container if request.PlacementPolicy == "" { @@ -333,8 +325,13 @@ func createContainer(ctx context.Context, p *pool.Pool, stoken session.Container container.SetName(&cnr, request.ContainerName) } - for key, val := range userAttrs { - cnr.SetAttribute(key, val) + for _, attr := range request.Attributes { + switch *attr.Key { + case attributeName, attributeTimestamp, + containerv2.SysAttributeName, containerv2.SysAttributeZone: + default: + cnr.SetAttribute(*attr.Key, *attr.Value) + } } if *params.NameScopeGlobal { // we don't check for nil because there is default false value diff --git a/handlers/util.go b/handlers/util.go index 310df4e..2233249 100644 --- a/handlers/util.go +++ b/handlers/util.go @@ -3,9 +3,7 @@ package handlers import ( "context" "fmt" - "net/http" "strconv" - "strings" "time" objectv2 "github.com/nspcc-dev/neofs-api-go/v2/object" @@ -27,62 +25,13 @@ type epochDurations struct { } const ( - UserAttributeHeaderPrefix = "X-Attribute-" - SystemAttributePrefix = "__NEOFS__" + SystemAttributePrefix = "__NEOFS__" ExpirationDurationAttr = SystemAttributePrefix + "EXPIRATION_DURATION" ExpirationTimestampAttr = SystemAttributePrefix + "EXPIRATION_TIMESTAMP" ExpirationRFC3339Attr = SystemAttributePrefix + "EXPIRATION_RFC3339" ) -var neofsAttributeHeaderPrefixes = [...]string{"Neofs-", "NEOFS-", "neofs-"} - -func systemTranslator(key, prefix string) string { - // replace specified prefix with `__NEOFS__` - key = strings.Replace(key, prefix, SystemAttributePrefix, 1) - - // replace `-` with `_` - key = strings.ReplaceAll(key, "-", "_") - - // replace with uppercase - return strings.ToUpper(key) -} - -func filterHeaders(header http.Header) map[string]string { - result := make(map[string]string) - prefix := UserAttributeHeaderPrefix - - for key, vals := range header { - if len(key) == 0 || len(vals) == 0 || len(vals[0]) == 0 { - continue - } - // checks that key has attribute prefix - if !strings.HasPrefix(key, prefix) { - continue - } - - // removing attribute prefix - key = strings.TrimPrefix(key, prefix) - - // checks that it's a system NeoFS header - for _, system := range neofsAttributeHeaderPrefixes { - if strings.HasPrefix(key, system) { - key = systemTranslator(key, system) - break - } - } - - // checks that attribute key not empty - if len(key) == 0 { - continue - } - - result[key] = vals[0] - } - - return result -} - // GetObjectAttributes forms object attributes from request headers. func GetObjectAttributes(ctx context.Context, pool *pool.Pool, attrs []*models.Attribute, prm PrmAttributes) ([]object.Attribute, error) { headers := make(map[string]string, len(attrs)) diff --git a/spec/rest.yaml b/spec/rest.yaml index 866abea..baaaec1 100644 --- a/spec/rest.yaml +++ b/spec/rest.yaml @@ -229,18 +229,7 @@ paths: required: true description: Container info schema: - type: object - properties: - containerName: - type: string - placementPolicy: - type: string - basicAcl: - type: string - example: - containerName: container - placementPolicy: "REP 3" - basicAcl: public-read-write + $ref: '#/definitions/ContainerPutInfo' responses: 200: description: Address of uploaded objects @@ -506,11 +495,34 @@ definitions: enum: - object - container + ContainerPutInfo: + description: Represent request body to create container. To specify container name use appropriate property (name provided in attributes will be ignored). + type: object + properties: + containerName: + type: string + placementPolicy: + type: string + basicAcl: + type: string + attributes: + type: array + items: + $ref: '#/definitions/Attribute' + example: + containerName: container + placementPolicy: "REP 3" + basicAcl: public-read-write + attributes: + - key: Custom-Attribute + value: value ContainerInfo: type: object properties: containerId: type: string + containerName: + type: string version: type: string ownerId: @@ -525,6 +537,7 @@ definitions: $ref: '#/definitions/Attribute' required: - containerId + - containerName - version - ownerId - basicAcl