diff --git a/api/handler/api.go b/api/handler/api.go index b32f46c3f..891897c83 100644 --- a/api/handler/api.go +++ b/api/handler/api.go @@ -24,12 +24,17 @@ type ( // Config contains data which handler needs to keep. Config struct { - DefaultPolicy netmap.PlacementPolicy + Policy PlacementPolicy DefaultMaxAge int NotificatorEnabled bool TLSEnabled bool CopiesNumber uint32 } + + PlacementPolicy struct { + Default netmap.PlacementPolicy + RegionMap map[string]netmap.PlacementPolicy + } ) const ( diff --git a/api/handler/head.go b/api/handler/head.go index e57b4c34d..177c64c86 100644 --- a/api/handler/head.go +++ b/api/handler/head.go @@ -123,6 +123,7 @@ func (h *handler) HeadBucketHandler(w http.ResponseWriter, r *http.Request) { } w.Header().Set(api.ContainerID, bktInfo.CID.EncodeToString()) + w.Header().Set(api.AmzBucketRegion, bktInfo.LocationConstraint) api.WriteResponse(w, http.StatusOK, nil, api.MimeNone) } diff --git a/api/handler/put.go b/api/handler/put.go index b7ce2afc6..d0a42801a 100644 --- a/api/handler/put.go +++ b/api/handler/put.go @@ -667,12 +667,10 @@ func parseMetadata(r *http.Request) map[string]string { } func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) { - var ( - reqInfo = api.GetReqInfo(r.Context()) - p = layer.CreateBucketParams{ - Name: reqInfo.BucketName, - } - ) + reqInfo := api.GetReqInfo(r.Context()) + p := &layer.CreateBucketParams{ + Name: reqInfo.BucketName, + } if err := checkBucketName(reqInfo.BucketName); err != nil { h.logAndSendError(w, "invalid bucket name", reqInfo, err) @@ -722,24 +720,11 @@ func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) { return } - useDefaultPolicy := true - if createParams.LocationConstraint != "" { - for _, placementPolicy := range policies { - if placementPolicy.LocationConstraint == createParams.LocationConstraint { - p.Policy = placementPolicy.Policy - p.LocationConstraint = createParams.LocationConstraint - useDefaultPolicy = false - break - } - } - } - if useDefaultPolicy { - p.Policy = h.cfg.DefaultPolicy - } + h.setPolicy(p, createParams.LocationConstraint, policies) p.ObjectLockEnabled = isLockEnabled(r.Header) - bktInfo, err := h.obj.CreateBucket(r.Context(), &p) + bktInfo, err := h.obj.CreateBucket(r.Context(), p) if err != nil { h.logAndSendError(w, "could not create bucket", reqInfo, err) return @@ -762,6 +747,27 @@ func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) { api.WriteSuccessResponseHeadersOnly(w) } +func (h handler) setPolicy(prm *layer.CreateBucketParams, locationConstraint string, userPolicies []*accessbox.ContainerPolicy) { + prm.Policy = h.cfg.Policy.Default + + if locationConstraint == "" { + return + } + + if policy, ok := h.cfg.Policy.RegionMap[locationConstraint]; ok { + prm.Policy = policy + prm.LocationConstraint = locationConstraint + } + + for _, placementPolicy := range userPolicies { + if placementPolicy.LocationConstraint == locationConstraint { + prm.Policy = placementPolicy.Policy + prm.LocationConstraint = locationConstraint + return + } + } +} + func isLockEnabled(header http.Header) bool { lockEnabledStr := header.Get(api.AmzBucketObjectLockEnabled) lockEnabled, _ := strconv.ParseBool(lockEnabledStr) diff --git a/cmd/s3-gw/app.go b/cmd/s3-gw/app.go index 50f98aa2c..f042c3e34 100644 --- a/cmd/s3-gw/app.go +++ b/cmd/s3-gw/app.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "encoding/hex" + "encoding/json" "errors" "fmt" "net" @@ -27,6 +28,7 @@ import ( "github.com/nspcc-dev/neofs-s3-gw/internal/neofs" "github.com/nspcc-dev/neofs-s3-gw/internal/version" "github.com/nspcc-dev/neofs-s3-gw/internal/wallet" + "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/pool" "github.com/spf13/viper" "go.uber.org/zap" @@ -596,41 +598,83 @@ func getAccessBoxCacheConfig(v *viper.Viper, l *zap.Logger) *cache.Config { } func getHandlerOptions(v *viper.Viper, l *zap.Logger) *handler.Config { - var ( - cfg handler.Config - err error - policyStr = handler.DefaultPolicy - defaultMaxAge = handler.DefaultMaxAge - setCopiesNumber = handler.DefaultCopiesNumber - ) - - if v.IsSet(cfgDefaultPolicy) { - policyStr = v.GetString(cfgDefaultPolicy) + cfg := &handler.Config{ + Policy: handler.PlacementPolicy{ + RegionMap: make(map[string]netmap.PlacementPolicy), + }, + DefaultMaxAge: handler.DefaultMaxAge, + NotificatorEnabled: v.GetBool(cfgEnableNATS), + TLSEnabled: v.IsSet(cfgTLSKeyFile) && v.IsSet(cfgTLSCertFile), + CopiesNumber: handler.DefaultCopiesNumber, } - if err = cfg.DefaultPolicy.DecodeString(policyStr); err != nil { - l.Fatal("couldn't parse container default policy", - zap.Error(err)) + defaultPolicyStr := handler.DefaultPolicy + if v.IsSet(cfgPolicyDefault) { + defaultPolicyStr = v.GetString(cfgPolicyDefault) + } + + if err := cfg.Policy.Default.DecodeString(defaultPolicyStr); err != nil { + l.Fatal("couldn't parse container default policy", zap.Error(err)) + } + + regionPolicyMap, err := parseRegionMap(v) + if err != nil { + l.Fatal("couldn't parse region mapping policy file", zap.Error(err)) + } + + for region, policy := range regionPolicyMap { + var pp netmap.PlacementPolicy + if err = pp.DecodeString(policy); err == nil { + cfg.Policy.RegionMap[region] = pp + continue + } + + if err = pp.UnmarshalJSON([]byte(policy)); err == nil { + cfg.Policy.RegionMap[region] = pp + continue + } + + l.Fatal("couldn't parse region mapping policy", zap.String("name", region), zap.Error(err)) } if v.IsSet(cfgDefaultMaxAge) { - defaultMaxAge = v.GetInt(cfgDefaultMaxAge) + defaultMaxAge := v.GetInt(cfgDefaultMaxAge) if defaultMaxAge <= 0 && defaultMaxAge != -1 { l.Fatal("invalid defaultMaxAge", zap.String("parameter", cfgDefaultMaxAge), zap.String("value in config", strconv.Itoa(defaultMaxAge))) } + cfg.DefaultMaxAge = defaultMaxAge } if val := v.GetUint32(cfgSetCopiesNumber); val > 0 { - setCopiesNumber = val + cfg.CopiesNumber = val } - cfg.DefaultMaxAge = defaultMaxAge - cfg.NotificatorEnabled = v.GetBool(cfgEnableNATS) - cfg.TLSEnabled = v.IsSet(cfgTLSKeyFile) && v.IsSet(cfgTLSCertFile) - cfg.CopiesNumber = setCopiesNumber - - return &cfg + return cfg +} + +func parseRegionMap(v *viper.Viper) (map[string]string, error) { + regionMap := make(map[string]string) + + filePath := v.GetString(cfgPolicyRegionMapFile) + if filePath == "" { + return regionMap, nil + } + + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("coudln't read file '%s'", filePath) + } + + if err = json.Unmarshal(data, ®ionMap); err != nil { + return nil, fmt.Errorf("unmarshal policies: %w", err) + } + + if _, ok := regionMap[api.DefaultLocationConstraint]; ok { + return nil, fmt.Errorf("config overrides %s location constraint", api.DefaultLocationConstraint) + } + + return regionMap, nil } diff --git a/cmd/s3-gw/app_settings.go b/cmd/s3-gw/app_settings.go index 917ff77eb..5a60aa1db 100644 --- a/cmd/s3-gw/app_settings.go +++ b/cmd/s3-gw/app_settings.go @@ -76,7 +76,8 @@ const ( // Settings. cfgNATSRootCAFiles = "nats.root_ca" // Policy. - cfgDefaultPolicy = "default_policy" + cfgPolicyDefault = "placement_policy.default" + cfgPolicyRegionMapFile = "placement_policy.region_mapping" // CORS. cfgDefaultMaxAge = "cors.default_max_age"