diff --git a/api/data/info.go b/api/data/info.go
index 9dd17cbd..c9f9df69 100644
--- a/api/data/info.go
+++ b/api/data/info.go
@@ -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
}
diff --git a/api/handler/bucket_list_test.go b/api/handler/bucket_list_test.go
index 8ac2a123..3c52bb3d 100644
--- a/api/handler/bucket_list_test.go
+++ b/api/handler/bucket_list_test.go
@@ -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{
diff --git a/api/handler/cors_test.go b/api/handler/cors_test.go
index 595bff7e..ac0f7ff3 100644
--- a/api/handler/cors_test.go
+++ b/api/handler/cors_test.go
@@ -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 := `
+
+
+ GET
+ PUT
+ *
+ *
+
+
+`
+ 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 := `
+
+
+ GET
+ PUT
+ *
+ *
+
+
+`
+ body := `
+
+
+ DELETE
+ *
+ *
+
+
+ `
+ 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 := `
+
+
+ GET
+ PUT
+ *
+ *
+
+
+`
+ 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 := `
+
+
+ GET
+ PUT
+ *
+ *
+
+
+`
+
+ 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
+}
diff --git a/api/handler/handlers_test.go b/api/handler/handlers_test.go
index 2c903b38..0e66df5d 100644
--- a/api/handler/handlers_test.go
+++ b/api/handler/handlers_test.go
@@ -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"
@@ -45,12 +47,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
@@ -162,9 +165,27 @@ 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 {
log := zaptest.NewLogger(t)
- hc, err := prepareHandlerContextBase(layer.DefaultCachesConfigs(log), log)
+ hc, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: layer.DefaultCachesConfigs(log)}, log)
+ require.NoError(t, err)
+ return &handlerContext{
+ handlerContextBase: hc,
+ t: t,
+ }
+}
+
+func prepareWithoutCORSHandlerContext(t *testing.T) *handlerContext {
+ log := zaptest.NewLogger(t)
+ hc, err := prepareHandlerContextBase(&handlerConfig{
+ cacheCfg: layer.DefaultCachesConfigs(log),
+ withoutCORS: true,
+ }, log)
require.NoError(t, err)
return &handlerContext{
handlerContextBase: hc,
@@ -174,7 +195,7 @@ func prepareHandlerContext(t *testing.T) *handlerContext {
func prepareHandlerContextWithMinCache(t *testing.T) *handlerContext {
log := zaptest.NewLogger(t)
- hc, err := prepareHandlerContextBase(getMinCacheConfig(log), log)
+ hc, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: getMinCacheConfig(log)}, log)
require.NoError(t, err)
return &handlerContext{
handlerContextBase: hc,
@@ -182,7 +203,7 @@ func prepareHandlerContextWithMinCache(t *testing.T) *handlerContext {
}
}
-func prepareHandlerContextBase(cacheCfg *layer.CachesConfig, log *zap.Logger) (*handlerContextBase, error) {
+func prepareHandlerContextBase(config *handlerConfig, log *zap.Logger) (*handlerContextBase, error) {
key, err := keys.NewPrivateKey()
if err != nil {
return nil, err
@@ -213,15 +234,23 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig, log *zap.Logger) (*
}
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 {
@@ -245,7 +274,7 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig, log *zap.Logger) (*
return nil, err
}
- return &handlerContextBase{
+ hc := &handlerContextBase{
owner: owner,
h: h,
tp: tp,
@@ -256,6 +285,44 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig, log *zap.Logger) (*
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
}
diff --git a/api/handler/object_list_test.go b/api/handler/object_list_test.go
index 3c6345b1..0dbb5b8c 100644
--- a/api/handler/object_list_test.go
+++ b/api/handler/object_list_test.go
@@ -105,7 +105,7 @@ func TestListObjectsVersionsSkipLogTaggingNodesError(t *testing.T) {
loggerCore, observedLog := observer.New(zap.DebugLevel)
log := zap.New(loggerCore)
- hcBase, err := prepareHandlerContextBase(layer.DefaultCachesConfigs(log), log)
+ hcBase, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: layer.DefaultCachesConfigs(log)}, log)
require.NoError(t, err)
hc := &handlerContext{
handlerContextBase: hcBase,
@@ -178,7 +178,7 @@ func TestListObjectsContextCanceled(t *testing.T) {
layerCfg.SessionList.Lifetime = time.Hour
layerCfg.SessionList.Size = 1
- hcBase, err := prepareHandlerContextBase(layerCfg, log)
+ hcBase, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: layerCfg}, log)
require.NoError(t, err)
hc := &handlerContext{
handlerContextBase: hcBase,
diff --git a/api/layer/cors.go b/api/layer/cors.go
index 2936c54a..c6b1b7d8 100644
--- a/api/layer/cors.go
+++ b/api/layer/cors.go
@@ -46,41 +46,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
}
@@ -117,10 +112,22 @@ func (n *Layer) DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo)
ctx, span := tracing.StartSpanFromContext(ctx, "layer.DeleteBucketCORS")
defer span.End()
+ 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 {
@@ -134,6 +141,22 @@ 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))
+ return
+ }
+
+ 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 {
diff --git a/api/layer/frostfs_mock.go b/api/layer/frostfs_mock.go
index 193c15c3..4726d21d 100644
--- a/api/layer/frostfs_mock.go
+++ b/api/layer/frostfs_mock.go
@@ -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"
@@ -175,6 +176,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()
diff --git a/api/layer/layer.go b/api/layer/layer.go
index e7f9cfa2..b658d51e 100644
--- a/api/layer/layer.go
+++ b/api/layer/layer.go
@@ -907,9 +907,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)
@@ -922,10 +922,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)
}
diff --git a/api/layer/system_object.go b/api/layer/system_object.go
index 26fdbf73..c19127a9 100644
--- a/api/layer/system_object.go
+++ b/api/layer/system_object.go
@@ -15,9 +15,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"
)
@@ -175,24 +177,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)
}
@@ -207,6 +227,56 @@ 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())
+ versions.SetLessFunc(func(ov1, ov2 *crdt.ObjectVersion) bool {
+ versionID1, versionID2 := ov1.VersionID(), ov2.VersionID()
+ timestamp1, timestamp2 := ov1.Headers[object.AttributeTimestamp], ov2.Headers[object.AttributeTimestamp]
+
+ if ov1.CreationEpoch != ov2.CreationEpoch {
+ return ov1.CreationEpoch < ov2.CreationEpoch
+ }
+
+ if len(timestamp1) > 0 && len(timestamp2) > 0 && timestamp1 != timestamp2 {
+ unixTime1, err := strconv.ParseInt(timestamp1, 10, 64)
+ if err != nil {
+ return versionID1 < versionID2
+ }
+
+ unixTime2, err := strconv.ParseInt(timestamp2, 10, 64)
+ if err != nil {
+ return versionID1 < versionID2
+ }
+
+ return unixTime1 < unixTime2
+ }
+
+ return versionID1 < versionID2
+ })
+
+ 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
@@ -274,7 +344,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),
})
}
diff --git a/api/layer/tree/tree_service.go b/api/layer/tree/tree_service.go
index db079b10..f3b6f6e3 100644
--- a/api/layer/tree/tree_service.go
+++ b/api/layer/tree/tree_service.go
@@ -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)
diff --git a/api/layer/tree_mock.go b/api/layer/tree_mock.go
index 577f4068..4d27da44 100644
--- a/api/layer/tree_mock.go
+++ b/api/layer/tree_mock.go
@@ -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) {
diff --git a/cmd/s3-gw/app.go b/cmd/s3-gw/app.go
index 807c453b..d81babf2 100644
--- a/cmd/s3-gw/app.go
+++ b/cmd/s3-gw/app.go
@@ -281,12 +281,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
diff --git a/docs/configuration.md b/docs/configuration.md
index 5c923b0c..d3662fba 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -863,7 +863,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. |
diff --git a/internal/frostfs/crdt/gset.go b/internal/frostfs/crdt/gset.go
index 888e11f3..adc786da 100644
--- a/internal/frostfs/crdt/gset.go
+++ b/internal/frostfs/crdt/gset.go
@@ -17,6 +17,7 @@ type ObjectVersions struct {
objects []*ObjectVersion
addList []string
isSorted bool
+ less func(*ObjectVersion, *ObjectVersion) bool
}
type ObjectVersion struct {
@@ -30,7 +31,7 @@ func (o *ObjectVersion) VersionID() string {
}
func NewObjectVersions(name string) *ObjectVersions {
- return &ObjectVersions{name: name}
+ return &ObjectVersions{name: name, less: less}
}
func NewObjectVersion(obj *object.Object) *ObjectVersion {
@@ -86,6 +87,15 @@ func (v *ObjectVersions) GetLast() *ObjectVersion {
return v.objects[len(v.objects)-1]
}
+func (v *ObjectVersions) GetSorted() []*ObjectVersion {
+ v.sort()
+ return v.objects
+}
+
+func (v *ObjectVersions) SetLessFunc(less func(*ObjectVersion, *ObjectVersion) bool) {
+ v.less = less
+}
+
func splitVersions(header string) []string {
if len(header) == 0 {
return nil
@@ -97,7 +107,7 @@ func splitVersions(header string) []string {
func (v *ObjectVersions) sort() {
if !v.isSorted {
sort.Slice(v.objects, func(i, j int) bool {
- return less(v.objects[i], v.objects[j])
+ return v.less(v.objects[i], v.objects[j])
})
v.isSorted = true
}
diff --git a/internal/logs/logs.go b/internal/logs/logs.go
index 539d28b3..c0db6fe3 100644
--- a/internal/logs/logs.go
+++ b/internal/logs/logs.go
@@ -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.
diff --git a/pkg/service/tree/tree.go b/pkg/service/tree/tree.go
index 4f0d3317..2b09d9e1 100644
--- a/pkg/service/tree/tree.go
+++ b/pkg/service/tree/tree.go
@@ -570,47 +570,25 @@ 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) {
- ctx, span := tracing.StartSpanFromContext(ctx, "tree.PutBucketCORS")
+func (c *Tree) GetAllBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) ([]oid.Address, error) {
+ ctx, span := tracing.StartSpanFromContext(ctx, "tree.GetAllBucketCORS")
defer span.End()
- 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)
+ 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) {