[#641] Rework CORS bucket behaviour
All checks were successful
/ DCO (pull_request) Successful in 40s
/ Builds (pull_request) Successful in 1m5s
/ Vulncheck (pull_request) Successful in 1m8s
/ OCI image (pull_request) Successful in 2m14s
/ Lint (pull_request) Successful in 2m22s
/ Tests (pull_request) Successful in 1m25s

Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
This commit is contained in:
Marina Biryukova 2025-02-19 19:09:19 +03:00
parent bfec3e0a5e
commit 90722d3901
16 changed files with 455 additions and 135 deletions

View file

@ -2,6 +2,7 @@ package data
import (
"encoding/xml"
"fmt"
"strings"
"time"
@ -20,6 +21,8 @@ const (
VersioningUnversioned = "Unversioned"
VersioningEnabled = "Enabled"
VersioningSuspended = "Suspended"
corsFilePathTemplate = "/%s.cors"
)
type (
@ -103,6 +106,11 @@ func (b *BucketInfo) CORSObjectName() string {
return b.CID.EncodeToString() + bktCORSConfigurationObject
}
// CORSObjectFilePath returns a FilePath for a bucket CORS configuration file.
func (b *BucketInfo) CORSObjectFilePath() string {
return fmt.Sprintf(corsFilePathTemplate, b.CID)
}
func (b *BucketInfo) LifecycleConfigurationObjectName() string {
return b.CID.EncodeToString() + bktLifecycleConfigurationObject
}

View file

@ -17,7 +17,7 @@ func TestHandler_ListBucketsHandler(t *testing.T) {
const defaultConstraint = "default"
region := "us-west-1"
hc := prepareHandlerContext(t)
hc := prepareWithoutCORSHandlerContext(t)
hc.config.putLocationConstraint(region)
props := []Bucket{

View file

@ -1,12 +1,19 @@
package handler
import (
"encoding/xml"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
"github.com/stretchr/testify/require"
)
@ -37,26 +44,14 @@ func TestCORSOriginWildcard(t *testing.T) {
hc.Handler().CreateBucketHandler(w, r)
assertStatus(t, w, http.StatusOK)
w, r = prepareTestPayloadRequest(hc, bktName, "", strings.NewReader(body))
ctx = middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
r = r.WithContext(ctx)
hc.Handler().PutBucketCorsHandler(w, r)
assertStatus(t, w, http.StatusOK)
putBucketCORS(hc, bktName, body)
w, r = prepareTestPayloadRequest(hc, bktName, "", nil)
hc.Handler().GetBucketCorsHandler(w, r)
assertStatus(t, w, http.StatusOK)
getBucketCORS(hc, bktName)
hc.config.useDefaultXMLNS = true
w, r = prepareTestPayloadRequest(hc, bktName, "", strings.NewReader(bodyNoXmlns))
ctx = middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
r = r.WithContext(ctx)
hc.Handler().PutBucketCorsHandler(w, r)
assertStatus(t, w, http.StatusOK)
putBucketCORS(hc, bktName, bodyNoXmlns)
w, r = prepareTestPayloadRequest(hc, bktName, "", nil)
hc.Handler().GetBucketCorsHandler(w, r)
assertStatus(t, w, http.StatusOK)
getBucketCORS(hc, bktName)
}
func TestPreflight(t *testing.T) {
@ -170,11 +165,7 @@ func TestPreflightWildcardOrigin(t *testing.T) {
hc.Handler().CreateBucketHandler(w, r)
assertStatus(t, w, http.StatusOK)
w, r = prepareTestPayloadRequest(hc, bktName, "", strings.NewReader(body))
ctx = middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
r = r.WithContext(ctx)
hc.Handler().PutBucketCorsHandler(w, r)
assertStatus(t, w, http.StatusOK)
putBucketCORS(hc, bktName, body)
for _, tc := range []struct {
name string
@ -236,3 +227,183 @@ func TestPreflightWildcardOrigin(t *testing.T) {
})
}
}
func TestDeleteAllCORSVersions(t *testing.T) {
body := `
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedOrigin>*</AllowedOrigin>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
`
hc := prepareHandlerContext(t)
bktName := "bucket-delete-all-cors-version"
createBucket(hc, bktName)
require.Len(t, hc.tp.Objects(), 0)
for range 5 {
putBucketCORS(hc, bktName, body)
}
require.Len(t, hc.tp.Objects(), 5)
deleteBucketCORS(hc, bktName)
require.Len(t, hc.tp.Objects(), 0)
}
func TestGetLatestCORSVersion(t *testing.T) {
bodyTree := `
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedOrigin>*</AllowedOrigin>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
`
body := `
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedMethod>DELETE</AllowedMethod>
<AllowedOrigin>*</AllowedOrigin>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
`
hc := prepareHandlerContextWithMinCache(t)
bktName := "bucket-get-latest-cors"
info := createBucket(hc, bktName)
addCORSToTree(hc, bodyTree, info.BktInfo, info.BktInfo.CID)
w := getBucketCORS(hc, bktName)
requireEqualCORS(hc.t, bodyTree, w.Body.String())
hc.tp.AddCORSObject(info.BktInfo, hc.corsCnrID, body)
w = getBucketCORS(hc, bktName)
requireEqualCORS(hc.t, body, w.Body.String())
hc.tp.AddCORSObject(info.BktInfo, hc.corsCnrID, bodyTree)
w = getBucketCORS(hc, bktName)
requireEqualCORS(hc.t, bodyTree, w.Body.String())
}
func TestDeleteTreeCORSVersions(t *testing.T) {
body := `
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedOrigin>*</AllowedOrigin>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
`
hc := prepareHandlerContext(t)
bktName := "bucket-delete-tree-cors-versions"
info := createBucket(hc, bktName)
addCORSToTree(hc, body, info.BktInfo, info.BktInfo.CID)
addCORSToTree(hc, body, info.BktInfo, hc.corsCnrID)
require.Len(t, hc.tp.Objects(), 2)
putBucketCORS(hc, bktName, body)
require.Len(t, hc.tp.Objects(), 1)
addCORSToTree(hc, body, info.BktInfo, info.BktInfo.CID)
addCORSToTree(hc, body, info.BktInfo, hc.corsCnrID)
require.Len(t, hc.tp.Objects(), 3)
deleteBucketCORS(hc, bktName)
require.Len(t, hc.tp.Objects(), 0)
}
func TestDeleteCORSInDeleteBucket(t *testing.T) {
body := `
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedOrigin>*</AllowedOrigin>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
`
hc := prepareHandlerContext(t)
bktName := "bucket-delete-cors-in-delete-bucket"
info := createBucket(hc, bktName)
addCORSToTree(hc, body, info.BktInfo, hc.corsCnrID)
addCORSToTree(hc, body, info.BktInfo, info.BktInfo.CID)
hc.tp.AddCORSObject(info.BktInfo, hc.corsCnrID, body)
require.Len(t, hc.tp.Objects(), 3)
hc.owner = info.BktInfo.Owner
deleteBucket(t, hc, bktName, http.StatusNoContent)
require.Len(t, hc.tp.Objects(), 1) // CORS object in bucket container is not deleted
}
func addCORSToTree(hc *handlerContext, cors string, bkt *data.BucketInfo, corsCnrID cid.ID) {
var addr oid.Address
addr.SetContainer(corsCnrID)
addr.SetObject(oidtest.ID())
var obj object.Object
obj.SetPayload([]byte(cors))
obj.SetPayloadSize(uint64(len(cors)))
hc.tp.SetObject(addr, &obj)
meta := make(map[string]string)
meta["FileName"] = "bucket-cors"
meta["OID"] = addr.Object().EncodeToString()
meta["CID"] = addr.Container().EncodeToString()
_, err := hc.treeMock.AddNode(hc.context, bkt, "system", 0, meta)
require.NoError(hc.t, err)
}
func requireEqualCORS(t *testing.T, expected string, actual string) {
expectedCORS := &data.CORSConfiguration{}
err := xml.NewDecoder(strings.NewReader(expected)).Decode(expectedCORS)
require.NoError(t, err)
actualCORS := &data.CORSConfiguration{}
err = xml.NewDecoder(strings.NewReader(actual)).Decode(actualCORS)
require.NoError(t, err)
require.Equal(t, expectedCORS, actualCORS)
}
func putBucketCORS(hc *handlerContext, bktName string, body string) {
w, r := prepareTestPayloadRequest(hc, bktName, "", strings.NewReader(body))
box, _ := createAccessBox(hc.t)
r = r.WithContext(middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box}))
hc.Handler().PutBucketCorsHandler(w, r)
assertStatus(hc.t, w, http.StatusOK)
}
func deleteBucketCORS(hc *handlerContext, bktName string) {
w, r := prepareTestPayloadRequest(hc, bktName, "", nil)
box, _ := createAccessBox(hc.t)
r = r.WithContext(middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box}))
hc.Handler().DeleteBucketCorsHandler(w, r)
assertStatus(hc.t, w, http.StatusNoContent)
}
func getBucketCORS(hc *handlerContext, bktName string) *httptest.ResponseRecorder {
w, r := prepareTestPayloadRequest(hc, bktName, "", nil)
hc.Handler().GetBucketCorsHandler(w, r)
assertStatus(hc.t, w, http.StatusOK)
return w
}

View file

@ -23,7 +23,9 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
@ -44,12 +46,13 @@ type handlerContext struct {
}
type handlerContextBase struct {
owner user.ID
h *handler
tp *layer.TestFrostFS
tree *tree.Tree
context context.Context
config *configMock
owner user.ID
h *handler
tp *layer.TestFrostFS
tree *tree.Tree
context context.Context
config *configMock
corsCnrID cid.ID
layerFeatures *layer.FeatureSettingsMock
treeMock *tree.ServiceClientMemory
@ -157,8 +160,25 @@ func (c *configMock) putLocationConstraint(constraint string) {
c.placementPolicies[constraint] = c.defaultPolicy
}
type handlerConfig struct {
cacheCfg *layer.CachesConfig
withoutCORS bool
}
func prepareHandlerContext(t *testing.T) *handlerContext {
hc, err := prepareHandlerContextBase(layer.DefaultCachesConfigs(zap.NewExample()))
hc, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: layer.DefaultCachesConfigs(zap.NewExample())})
require.NoError(t, err)
return &handlerContext{
handlerContextBase: hc,
t: t,
}
}
func prepareWithoutCORSHandlerContext(t *testing.T) *handlerContext {
hc, err := prepareHandlerContextBase(&handlerConfig{
cacheCfg: layer.DefaultCachesConfigs(zap.NewExample()),
withoutCORS: true,
})
require.NoError(t, err)
return &handlerContext{
handlerContextBase: hc,
@ -167,7 +187,7 @@ func prepareHandlerContext(t *testing.T) *handlerContext {
}
func prepareHandlerContextWithMinCache(t *testing.T) *handlerContext {
hc, err := prepareHandlerContextBase(getMinCacheConfig(zap.NewExample()))
hc, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: getMinCacheConfig(zap.NewExample())})
require.NoError(t, err)
return &handlerContext{
handlerContextBase: hc,
@ -175,7 +195,7 @@ func prepareHandlerContextWithMinCache(t *testing.T) *handlerContext {
}
}
func prepareHandlerContextBase(cacheCfg *layer.CachesConfig) (*handlerContextBase, error) {
func prepareHandlerContextBase(config *handlerConfig) (*handlerContextBase, error) {
key, err := keys.NewPrivateKey()
if err != nil {
return nil, err
@ -207,15 +227,23 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig) (*handlerContextBas
}
layerCfg := &layer.Config{
Cache: layer.NewCache(cacheCfg),
Cache: layer.NewCache(config.cacheCfg),
AnonKey: layer.AnonymousKey{Key: key},
Resolver: testResolver,
TreeService: treeMock,
Features: features,
GateOwner: owner,
GateKey: key,
WorkerPool: pool,
}
if !config.withoutCORS {
layerCfg.CORSCnrInfo, err = createCORSContainer(key, tp)
if err != nil {
return nil, err
}
}
var pp netmap.PlacementPolicy
err = pp.DecodeString("REP 1")
if err != nil {
@ -239,7 +267,7 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig) (*handlerContextBas
return nil, err
}
return &handlerContextBase{
hc := &handlerContextBase{
owner: owner,
h: h,
tp: tp,
@ -250,6 +278,44 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig) (*handlerContextBas
layerFeatures: features,
treeMock: memCli,
cache: layerCfg.Cache,
}
if layerCfg.CORSCnrInfo != nil {
hc.corsCnrID = layerCfg.CORSCnrInfo.CID
}
return hc, nil
}
func createCORSContainer(key *keys.PrivateKey, tp *layer.TestFrostFS) (*data.BucketInfo, error) {
bearerToken := bearertest.Token()
err := bearerToken.Sign(key.PrivateKey)
if err != nil {
return nil, err
}
bktName := "cors"
res, err := tp.CreateContainer(middleware.SetBox(context.Background(), &middleware.Box{AccessBox: &accessbox.Box{
Gate: &accessbox.GateData{
BearerToken: &bearerToken,
GateKey: key.PublicKey(),
},
}}), frostfs.PrmContainerCreate{
Name: bktName,
Policy: getPlacementPolicy(),
})
if err != nil {
return nil, err
}
var owner user.ID
user.IDFromKey(&owner, key.PrivateKey.PublicKey)
return &data.BucketInfo{
Name: bktName,
Owner: owner,
CID: res.ContainerID,
HomomorphicHashDisabled: res.HomomorphicHashDisabled,
}, nil
}

View file

@ -103,7 +103,7 @@ func TestListObjectsVersionsSkipLogTaggingNodesError(t *testing.T) {
loggerCore, observedLog := observer.New(zap.DebugLevel)
log := zap.New(loggerCore)
hcBase, err := prepareHandlerContextBase(layer.DefaultCachesConfigs(log))
hcBase, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: layer.DefaultCachesConfigs(log)})
require.NoError(t, err)
hc := &handlerContext{
handlerContextBase: hcBase,
@ -176,7 +176,7 @@ func TestListObjectsContextCanceled(t *testing.T) {
layerCfg.SessionList.Lifetime = time.Hour
layerCfg.SessionList.Size = 1
hcBase, err := prepareHandlerContextBase(layerCfg)
hcBase, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: layerCfg})
require.NoError(t, err)
hc := &handlerContext{
handlerContextBase: hcBase,

View file

@ -42,41 +42,36 @@ func (n *Layer) PutBucketCORS(ctx context.Context, p *PutCORSParams) error {
}
prm := frostfs.PrmObjectCreate{
Container: n.corsCnrInfo.CID,
Payload: &buf,
Filepath: p.BktInfo.CORSObjectName(),
Filepath: p.BktInfo.CORSObjectFilePath(),
CreationTime: TimeNow(ctx),
CopiesNumber: p.CopiesNumbers,
PrmAuth: frostfs.PrmAuth{
PrivateKey: &n.gateKey.PrivateKey,
},
}
var corsBkt *data.BucketInfo
if n.corsCnrInfo == nil {
corsBkt = p.BktInfo
prm.CopiesNumber = p.CopiesNumbers
} else {
corsBkt = n.corsCnrInfo
prm.PrmAuth.PrivateKey = &n.gateKey.PrivateKey
}
prm.Container = corsBkt.CID
createdObj, err := n.objectPutAndHash(ctx, prm, corsBkt)
_, err := n.objectPutAndHash(ctx, prm, n.corsCnrInfo)
if err != nil {
return fmt.Errorf("put cors object: %w", err)
}
objsToDelete, err := n.treeService.PutBucketCORS(ctx, p.BktInfo, newAddress(corsBkt.CID, createdObj.ID))
objToDeleteNotFound := errors.Is(err, tree.ErrNoNodeToRemove)
if err != nil && !objToDeleteNotFound {
return err
n.cache.PutCORS(n.BearerOwner(ctx), p.BktInfo, cors)
objs, err := n.treeService.DeleteBucketCORS(ctx, p.BktInfo)
objNotFound := errors.Is(err, tree.ErrNoNodeToRemove)
if err != nil && !objNotFound {
n.reqLogger(ctx).Error(logs.CouldntDeleteBucketCORS, zap.Error(err), logs.TagField(logs.TagExternalStorageTree))
return nil
}
if !objToDeleteNotFound {
for _, addr := range objsToDelete {
if !objNotFound {
for _, addr := range objs {
n.deleteCORSObject(ctx, p.BktInfo, addr)
}
}
n.cache.PutCORS(n.BearerOwner(ctx), p.BktInfo, cors)
return nil
}
@ -107,10 +102,22 @@ func (n *Layer) GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo, dec
}
func (n *Layer) DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) error {
corsVersions, err := n.getCORSVersions(ctx, bktInfo)
if err != nil {
return fmt.Errorf("get cors versions: %w", err)
}
sortedVersions := corsVersions.GetSorted()
for _, version := range sortedVersions {
if err = n.objectDeleteWithAuth(ctx, n.corsCnrInfo, version.ObjID, frostfs.PrmAuth{PrivateKey: &n.gateKey.PrivateKey}); err != nil {
return fmt.Errorf("delete cors object '%s': %w", version.VersionID(), err)
}
}
objs, err := n.treeService.DeleteBucketCORS(ctx, bktInfo)
objNotFound := errors.Is(err, tree.ErrNoNodeToRemove)
if err != nil && !objNotFound {
return err
return fmt.Errorf("delete cors from tree: %w", err)
}
if !objNotFound {
@ -124,6 +131,21 @@ func (n *Layer) DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo)
return nil
}
func (n *Layer) deleteCORSVersions(ctx context.Context, bktInfo *data.BucketInfo) {
corsVersions, err := n.getCORSVersions(ctx, bktInfo)
if err != nil {
n.reqLogger(ctx).Error(logs.CouldntGetCORSObjectVersions, zap.Error(err), logs.TagField(logs.TagExternalStorage))
}
var addr oid.Address
addr.SetContainer(n.corsCnrInfo.CID)
sortedVersions := corsVersions.GetSorted()
for _, version := range sortedVersions {
addr.SetObject(version.ObjID)
n.deleteCORSObject(ctx, bktInfo, addr)
}
}
func checkCORS(cors *data.CORSConfiguration) error {
for _, r := range cors.CORSRules {
for _, m := range r.AllowedMethods {

View file

@ -11,6 +11,7 @@ import (
"strings"
"time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
v2container "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/container"
@ -160,6 +161,34 @@ func (t *TestFrostFS) SetContainer(cnrID cid.ID, cnr *container.Container) {
t.containers[cnrID.EncodeToString()] = cnr
}
func (t *TestFrostFS) SetObject(addr oid.Address, obj *object.Object) {
t.objects[addr.EncodeToString()] = obj
}
func (t *TestFrostFS) AddCORSObject(bkt *data.BucketInfo, corsCnrID cid.ID, cors string) {
a := object.NewAttribute()
a.SetKey(object.AttributeFilePath)
a.SetValue(bkt.CORSObjectFilePath())
var owner user.ID
user.IDFromKey(&owner, t.key.PrivateKey.PublicKey)
objID := oidtest.ID()
obj := object.New()
obj.SetContainerID(corsCnrID)
obj.SetID(objID)
obj.SetPayloadSize(uint64(len(cors)))
obj.SetPayload([]byte(cors))
obj.SetAttributes(*a)
obj.SetCreationEpoch(t.currentEpoch)
obj.SetOwnerID(owner)
t.currentEpoch++
addr := newAddress(corsCnrID, objID)
t.objects[addr.EncodeToString()] = obj
}
func (t *TestFrostFS) CreateContainer(_ context.Context, prm frostfs.PrmContainerCreate) (*frostfs.ContainerCreateResult, error) {
var cnr container.Container
cnr.Init()

View file

@ -876,9 +876,9 @@ func (n *Layer) DeleteBucket(ctx context.Context, p *DeleteBucketParams) error {
n.cache.DeleteBucket(p.BktInfo)
corsObj, err := n.treeService.GetBucketCORS(ctx, p.BktInfo)
corsObjs, err := n.treeService.GetAllBucketCORS(ctx, p.BktInfo)
if err != nil {
n.reqLogger(ctx).Error(logs.GetBucketCorsFromTree, zap.Error(err), logs.TagField(logs.TagExternalStorageTree))
n.reqLogger(ctx).Error(logs.GetAllBucketCorsFromTree, zap.Error(err), logs.TagField(logs.TagExternalStorageTree))
}
lifecycleObj, treeErr := n.treeService.GetBucketLifecycleConfiguration(ctx, p.BktInfo)
@ -891,10 +891,14 @@ func (n *Layer) DeleteBucket(ctx context.Context, p *DeleteBucketParams) error {
return fmt.Errorf("delete container: %w", err)
}
if !corsObj.Container().Equals(p.BktInfo.CID) && !corsObj.Container().Equals(cid.ID{}) {
n.deleteCORSObject(ctx, p.BktInfo, corsObj)
for _, corsObj := range corsObjs {
if !corsObj.Container().Equals(p.BktInfo.CID) && !corsObj.Container().Equals(cid.ID{}) {
n.deleteCORSObject(ctx, p.BktInfo, corsObj)
}
}
n.deleteCORSVersions(ctx, p.BktInfo)
if treeErr == nil && !lifecycleObj.Container().Equals(p.BktInfo.CID) {
n.deleteLifecycleObject(ctx, p.BktInfo, lifecycleObj)
}

View file

@ -14,9 +14,11 @@ import (
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/tree"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/crdt"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object"
apiobject "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
)
@ -168,24 +170,42 @@ func (n *Layer) getCORS(ctx context.Context, bkt *data.BucketInfo, decoder func(
return cors, nil
}
addr, err := n.treeService.GetBucketCORS(ctx, bkt)
objNotFound := errors.Is(err, tree.ErrNodeNotFound)
if err != nil && !objNotFound {
corsVersions, err := n.getCORSVersions(ctx, bkt)
if err != nil {
return nil, err
}
if objNotFound {
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrNoSuchCORSConfiguration), err.Error())
}
var (
prmAuth frostfs.PrmAuth
objID oid.ID
corsBkt = bkt
lastCORS = corsVersions.GetLast()
)
var prmAuth frostfs.PrmAuth
corsBkt := bkt
if !addr.Container().Equals(bkt.CID) && !addr.Container().Equals(cid.ID{}) {
corsBkt = &data.BucketInfo{CID: addr.Container()}
if lastCORS != nil {
prmAuth.PrivateKey = &n.gateKey.PrivateKey
corsBkt = n.corsCnrInfo
objID = lastCORS.ObjID
} else {
addr, err := n.treeService.GetBucketCORS(ctx, bkt)
objNotFound := errors.Is(err, tree.ErrNodeNotFound)
if err != nil && !objNotFound {
return nil, err
}
if objNotFound {
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrNoSuchCORSConfiguration), err.Error())
}
if !addr.Container().Equals(bkt.CID) && !addr.Container().Equals(cid.ID{}) {
corsBkt = &data.BucketInfo{CID: addr.Container()}
prmAuth.PrivateKey = &n.gateKey.PrivateKey
}
objID = addr.Object()
}
obj, err := n.objectGetWithAuth(ctx, corsBkt, addr.Object(), prmAuth)
obj, err := n.objectGetWithAuth(ctx, corsBkt, objID, prmAuth)
if err != nil {
return nil, fmt.Errorf("get cors object: %w", err)
}
@ -200,6 +220,31 @@ func (n *Layer) getCORS(ctx context.Context, bkt *data.BucketInfo, decoder func(
return cors, nil
}
func (n *Layer) getCORSVersions(ctx context.Context, bkt *data.BucketInfo) (*crdt.ObjectVersions, error) {
corsVersions, err := n.frostFS.SearchObjects(ctx, frostfs.PrmObjectSearch{
Container: n.corsCnrInfo.CID,
ExactAttribute: [2]string{object.AttributeFilePath, bkt.CORSObjectFilePath()},
})
if err != nil {
return nil, fmt.Errorf("search cors objects: %w", err)
}
versions := crdt.NewObjectVersions(bkt.CORSObjectFilePath())
for _, id := range corsVersions {
objVersion, err := n.frostFS.HeadObject(ctx, frostfs.PrmObjectHead{
Container: n.corsCnrInfo.CID,
Object: id,
})
if err != nil {
return nil, fmt.Errorf("head cors object '%s': %w", id.EncodeToString(), err)
}
versions.AppendVersion(crdt.NewObjectVersion(objVersion))
}
return versions, nil
}
func lockObjectKey(objVersion *data.ObjectVersion) string {
// todo reconsider forming name since versionID can be "null" or ""
return ".lock." + objVersion.BktInfo.CID.EncodeToString() + "." + objVersion.ObjectName + "." + objVersion.VersionID
@ -261,7 +306,7 @@ func (n *Layer) attributesFromLock(ctx context.Context, lock *data.ObjectLock) (
if expEpoch != 0 {
result = append(result, [2]string{
object.SysAttributeExpEpoch, strconv.FormatUint(expEpoch, 10),
apiobject.SysAttributeExpEpoch, strconv.FormatUint(expEpoch, 10),
})
}

View file

@ -18,19 +18,19 @@ type Service interface {
// If tree node is not found returns ErrNodeNotFound error.
GetSettingsNode(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error)
// GetBucketCORS gets an object id that corresponds to object with bucket CORS.
// GetBucketCORS gets an object address that corresponds to object with bucket CORS.
//
// If object id is not found returns ErrNodeNotFound error.
// If object is not found returns ErrNodeNotFound error.
GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) (oid.Address, error)
// PutBucketCORS puts a node to a system tree and returns objectID of a previous cors config which must be deleted in FrostFS.
// GetAllBucketCORS gets all object addresses that corresponds to objects with bucket CORS.
//
// If object ids to remove is not found returns ErrNoNodeToRemove error.
PutBucketCORS(ctx context.Context, bktInfo *data.BucketInfo, addr oid.Address) ([]oid.Address, error)
// If objects are not found returns ErrNodeNotFound error.
GetAllBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) ([]oid.Address, error)
// DeleteBucketCORS removes a node from a system tree and returns objID which must be deleted in FrostFS.
// DeleteBucketCORS removes a node from a system tree and returns object addresses which must be deleted in FrostFS.
//
// If object ids to remove is not found returns ErrNoNodeToRemove error.
// If objects to remove are not found returns ErrNoNodeToRemove error.
DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) ([]oid.Address, error)
GetObjectTagging(ctx context.Context, bktInfo *data.BucketInfo, objVersion *data.NodeVersion) (map[string]string, error)

View file

@ -115,12 +115,12 @@ func (t *TreeServiceMock) GetSettingsNode(_ context.Context, bktInfo *data.Bucke
func (t *TreeServiceMock) GetBucketCORS(_ context.Context, bktInfo *data.BucketInfo) (oid.Address, error) {
systemMap, ok := t.system[bktInfo.CID.EncodeToString()]
if !ok {
return oid.Address{}, nil
return oid.Address{}, tree.ErrNodeNotFound
}
node, ok := systemMap["cors"]
if !ok {
return oid.Address{}, nil
return oid.Address{}, tree.ErrNodeNotFound
}
var addr oid.Address
@ -129,19 +129,13 @@ func (t *TreeServiceMock) GetBucketCORS(_ context.Context, bktInfo *data.BucketI
return addr, nil
}
func (t *TreeServiceMock) PutBucketCORS(_ context.Context, bktInfo *data.BucketInfo, addr oid.Address) ([]oid.Address, error) {
systemMap, ok := t.system[bktInfo.CID.EncodeToString()]
if !ok {
systemMap = make(map[string]*data.BaseNodeVersion)
func (t *TreeServiceMock) GetAllBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) ([]oid.Address, error) {
cors, err := t.GetBucketCORS(ctx, bktInfo)
if err != nil {
return nil, err
}
systemMap["cors"] = &data.BaseNodeVersion{
OID: addr.Object(),
}
t.system[bktInfo.CID.EncodeToString()] = systemMap
return nil, tree.ErrNoNodeToRemove
return []oid.Address{cors}, nil
}
func (t *TreeServiceMock) DeleteBucketCORS(context.Context, *data.BucketInfo) ([]oid.Address, error) {

View file

@ -279,12 +279,9 @@ func (a *App) initLayer(ctx context.Context) {
var gateOwner user.ID
user.IDFromKey(&gateOwner, a.key.PrivateKey.PublicKey)
var corsCnrInfo *data.BucketInfo
if a.config().IsSet(cfgContainersCORS) {
corsCnrInfo, err = a.fetchContainerInfo(ctx, cfgContainersCORS)
if err != nil {
a.log.Fatal(logs.CouldNotFetchCORSContainerInfo, zap.Error(err), logs.TagField(logs.TagApp))
}
corsCnrInfo, err := a.fetchContainerInfo(ctx, cfgContainersCORS)
if err != nil {
a.log.Fatal(logs.CouldNotFetchCORSContainerInfo, zap.Error(err), logs.TagField(logs.TagApp))
}
var lifecycleCnrInfo *data.BucketInfo

View file

@ -861,7 +861,7 @@ containers:
| Parameter | Type | SIGHUP reload | Default value | Description |
|-------------|----------|---------------|---------------|-------------------------------------------------------------------------------------------------------------------------|
| `cors` | `string` | no | | Container name for CORS configurations. If not set, container of the bucket is used. |
| `cors` | `string` | no | | Container name for CORS configurations. |
| `lifecycle` | `string` | no | | Container name for lifecycle configurations. If not set, container of the bucket is used. |
| `accessbox` | `string` | no | | Container name to lookup accessbox if custom aws credentials is used. If not set, custom credentials are not supported. |

View file

@ -86,6 +86,11 @@ func (v *ObjectVersions) GetLast() *ObjectVersion {
return v.objects[len(v.objects)-1]
}
func (v *ObjectVersions) GetSorted() []*ObjectVersion {
v.sort()
return v.objects
}
func splitVersions(header string) []string {
if len(header) == 0 {
return nil

View file

@ -180,6 +180,7 @@ const (
CouldNotFetchObjectMeta = "could not fetch object meta"
FailedToDeleteObject = "failed to delete object"
CouldntDeleteLifecycleObject = "couldn't delete lifecycle configuration object"
CouldntGetCORSObjectVersions = "couldn't get cors object versions"
)
// External blockchain.
@ -199,8 +200,8 @@ const (
ObjectTaggingNodeHasMultipleIDs = "object tagging node has multiple ids"
BucketTaggingNodeHasMultipleIDs = "bucket tagging node has multiple ids"
BucketSettingsNodeHasMultipleIDs = "bucket settings node has multiple ids"
BucketCORSNodeHasMultipleIDs = "bucket cors node has multiple ids"
GetBucketCorsFromTree = "get bucket cors from tree"
GetAllBucketCorsFromTree = "get all bucket cors from tree"
CouldntDeleteBucketCORS = "couldn't delete bucket cors"
)
// Authmate.

View file

@ -560,44 +560,22 @@ func (c *Tree) GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) (oid
return getTreeNodeAddress(node.Latest())
}
func (c *Tree) PutBucketCORS(ctx context.Context, bktInfo *data.BucketInfo, addr oid.Address) ([]oid.Address, error) {
multiNode, err := c.getSystemNode(ctx, bktInfo, corsFilename)
isErrNotFound := errors.Is(err, tree.ErrNodeNotFound)
if err != nil && !isErrNotFound {
return nil, fmt.Errorf("couldn't get node: %w", err)
func (c *Tree) GetAllBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) ([]oid.Address, error) {
node, err := c.getSystemNode(ctx, bktInfo, corsFilename)
if err != nil {
return nil, err
}
meta := make(map[string]string)
meta[FileNameKey] = corsFilename
meta[oidKV] = addr.Object().EncodeToString()
meta[cidKV] = addr.Container().EncodeToString()
if isErrNotFound {
if _, err = c.service.AddNode(ctx, bktInfo, systemTree, 0, meta); err != nil {
addrs := make([]oid.Address, 0, len(node.nodes))
for _, corsNode := range node.nodes {
addr, err := getTreeNodeAddress(corsNode)
if err != nil {
return nil, err
}
return nil, tree.ErrNoNodeToRemove
addrs = append(addrs, addr)
}
latest := multiNode.Latest()
ind := latest.GetLatestNodeIndex()
if latest.IsSplit() {
c.reqLogger(ctx).Error(logs.BucketCORSNodeHasMultipleIDs, logs.TagField(logs.TagExternalStorageTree))
}
if err = c.service.MoveNode(ctx, bktInfo, systemTree, latest.ID[ind], 0, meta); err != nil {
return nil, fmt.Errorf("move cors node: %w", err)
}
objToDelete := make([]oid.Address, 1, len(multiNode.nodes))
objToDelete[0], err = getTreeNodeAddress(latest)
if err != nil {
return nil, fmt.Errorf("parse object addr of latest cors node in tree: %w", err)
}
objToDelete = append(objToDelete, c.cleanOldNodes(ctx, multiNode.Old(), bktInfo)...)
return objToDelete, nil
return addrs, nil
}
func (c *Tree) DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) ([]oid.Address, error) {