[##] Add support virtual-hosted-style

Signed-off-by: Roman Loginov <r.loginov@yadro.com>
This commit is contained in:
Roman Loginov 2024-07-31 09:45:46 +03:00
parent c34680d157
commit 82eee8a246
13 changed files with 320 additions and 201 deletions

View file

@ -0,0 +1,125 @@
package middleware
import (
"net/http"
"strings"
)
const wildcardPlaceholder = "<wildcard>"
type VHSSettings interface {
Domains() []string
GlobalVHS() bool
VHSNamespacesEnabled() map[string]bool
}
func PrepareAddressStyle(settings VHSSettings) Func {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
reqInfo := GetReqInfo(ctx)
if isVHSAddress(settings.GlobalVHS(), settings.VHSNamespacesEnabled(), reqInfo.Namespace) {
prepareVHSAddress(reqInfo, r, settings)
} else {
preparePathStyleAddress(reqInfo, r)
}
h.ServeHTTP(w, r)
})
}
}
func isVHSAddress(enabledFlag bool, vhsNamespaces map[string]bool, namespace string) bool {
result := enabledFlag
if v, ok := vhsNamespaces[namespace]; ok {
result = v
}
return result
}
func prepareVHSAddress(reqInfo *ReqInfo, r *http.Request, settings VHSSettings) {
reqInfo.RequestVHSEnabled = true
bktName, match := checkDomain(r.Host, settings.Domains())
if match {
if bktName == "" {
reqInfo.RequestType = noneType
} else {
if objName := strings.TrimPrefix(r.URL.Path, "/"); objName != "" {
reqInfo.RequestType = objectType
reqInfo.ObjectName = objName
reqInfo.BucketName = bktName
} else {
reqInfo.RequestType = bucketType
reqInfo.BucketName = bktName
}
}
} else {
parts := strings.Split(r.Host, ".")
reqInfo.BucketName = parts[0]
if objName := strings.TrimPrefix(r.URL.Path, "/"); objName != "" {
reqInfo.RequestType = objectType
reqInfo.ObjectName = objName
} else {
reqInfo.RequestType = bucketType
}
}
}
func preparePathStyleAddress(reqInfo *ReqInfo, r *http.Request) {
bktObj := strings.TrimPrefix(r.URL.Path, "/")
if bktObj == "" {
reqInfo.RequestType = noneType
} else if ind := strings.IndexByte(bktObj, '/'); ind != -1 && bktObj[ind+1:] != "" {
reqInfo.RequestType = objectType
reqInfo.BucketName = bktObj[:ind]
reqInfo.ObjectName = bktObj[ind+1:]
} else {
reqInfo.RequestType = bucketType
reqInfo.BucketName = strings.TrimSuffix(bktObj, "/")
}
}
func checkDomain(requestURL string, domains []string) (bktName string, match bool) {
host := strings.Split(requestURL, ".")
for _, pattern := range domains {
p := strings.Split(pattern, ".")
bktName, match = compareMatch(host, p)
if match {
break
}
}
return
}
func compareMatch(host, pattern []string) (bktName string, match bool) {
if len(host) < len(pattern) {
return "", false
}
var patternIndex, hostIndex int
for i := 0; i < len(pattern); i++ {
patternIndex = len(pattern) - 1 - i
hostIndex = len(host) - 1 - i
if pattern[patternIndex] == wildcardPlaceholder {
continue
}
if host[hostIndex] != pattern[patternIndex] {
return "", false
}
}
switch hostIndex {
case 0:
return "", true
case 1:
return host[0], true
default:
return "", false
}
}

View file

@ -73,7 +73,6 @@ type PolicyConfig struct {
Storage engine.ChainRouter Storage engine.ChainRouter
FrostfsID FrostFSIDInformer FrostfsID FrostFSIDInformer
Settings PolicySettings Settings PolicySettings
Domains []string
Log *zap.Logger Log *zap.Logger
BucketResolver BucketResolveFunc BucketResolver BucketResolveFunc
Decoder XMLDecoder Decoder XMLDecoder
@ -99,21 +98,21 @@ func PolicyCheck(cfg PolicyConfig) Func {
} }
func policyCheck(r *http.Request, cfg PolicyConfig) error { func policyCheck(r *http.Request, cfg PolicyConfig) error {
reqType, bktName, objName := getBucketObject(r, cfg.Domains) reqInfo := GetReqInfo(r.Context())
req, userKey, userGroups, err := getPolicyRequest(r, cfg, reqType, bktName, objName)
req, userKey, userGroups, err := getPolicyRequest(r, cfg, reqInfo.RequestType, reqInfo.BucketName, reqInfo.ObjectName)
if err != nil { if err != nil {
return err return err
} }
var bktInfo *data.BucketInfo var bktInfo *data.BucketInfo
if reqType != noneType && !strings.HasSuffix(req.Operation(), CreateBucketOperation) { if reqInfo.RequestType != noneType && !strings.HasSuffix(req.Operation(), CreateBucketOperation) {
bktInfo, err = cfg.BucketResolver(r.Context(), bktName) bktInfo, err = cfg.BucketResolver(r.Context(), reqInfo.BucketName)
if err != nil { if err != nil {
return err return err
} }
} }
reqInfo := GetReqInfo(r.Context())
target := engine.NewRequestTargetWithNamespace(reqInfo.Namespace) target := engine.NewRequestTargetWithNamespace(reqInfo.Namespace)
if bktInfo != nil { if bktInfo != nil {
cnrTarget := engine.ContainerTarget(bktInfo.CID.EncodeToString()) cnrTarget := engine.ContainerTarget(bktInfo.CID.EncodeToString())
@ -208,33 +207,6 @@ const (
objectType objectType
) )
func getBucketObject(r *http.Request, domains []string) (reqType ReqType, bktName string, objName string) {
for _, domain := range domains {
ind := strings.Index(r.Host, "."+domain)
if ind == -1 {
continue
}
bkt := r.Host[:ind]
if obj := strings.TrimPrefix(r.URL.Path, "/"); obj != "" {
return objectType, bkt, obj
}
return bucketType, bkt, ""
}
bktObj := strings.TrimPrefix(r.URL.Path, "/")
if bktObj == "" {
return noneType, "", ""
}
if ind := strings.IndexByte(bktObj, '/'); ind != -1 && bktObj[ind+1:] != "" {
return objectType, bktObj[:ind], bktObj[ind+1:]
}
return bucketType, strings.TrimSuffix(bktObj, "/"), ""
}
func determineOperation(r *http.Request, reqType ReqType) (operation string) { func determineOperation(r *http.Request, reqType ReqType) (operation string) {
switch reqType { switch reqType {
case objectType: case objectType:

View file

@ -8,79 +8,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestReqTypeDetermination(t *testing.T) {
bkt, obj, domain := "test-bucket", "test-object", "domain"
for _, tc := range []struct {
name string
target string
host string
domains []string
expectedType ReqType
expectedBktName string
expectedObjName string
}{
{
name: "bucket request, path-style",
target: "/" + bkt,
expectedType: bucketType,
expectedBktName: bkt,
},
{
name: "bucket request with slash, path-style",
target: "/" + bkt + "/",
expectedType: bucketType,
expectedBktName: bkt,
},
{
name: "object request, path-style",
target: "/" + bkt + "/" + obj,
expectedType: objectType,
expectedBktName: bkt,
expectedObjName: obj,
},
{
name: "object request with slash, path-style",
target: "/" + bkt + "/" + obj + "/",
expectedType: objectType,
expectedBktName: bkt,
expectedObjName: obj + "/",
},
{
name: "none type request",
target: "/",
expectedType: noneType,
},
{
name: "bucket request, virtual-hosted style",
target: "/",
host: bkt + "." + domain,
domains: []string{"some-domain", domain},
expectedType: bucketType,
expectedBktName: bkt,
},
{
name: "object request, virtual-hosted style",
target: "/" + obj,
host: bkt + "." + domain,
domains: []string{"some-domain", domain},
expectedType: objectType,
expectedBktName: bkt,
expectedObjName: obj,
},
} {
t.Run(tc.name, func(t *testing.T) {
r := httptest.NewRequest(http.MethodPut, tc.target, nil)
r.Host = tc.host
reqType, bktName, objName := getBucketObject(r, tc.domains)
require.Equal(t, tc.expectedType, reqType)
require.Equal(t, tc.expectedBktName, bktName)
require.Equal(t, tc.expectedObjName, objName)
})
}
}
func TestDetermineBucketOperation(t *testing.T) { func TestDetermineBucketOperation(t *testing.T) {
const defaultValue = "value" const defaultValue = "value"

View file

@ -12,7 +12,6 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
treepool "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool/tree" treepool "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool/tree"
"github.com/go-chi/chi/v5"
"github.com/google/uuid" "github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
@ -41,6 +40,8 @@ type (
Namespace string Namespace string
User string // User owner id User string // User owner id
Tagging *data.Tagging Tagging *data.Tagging
RequestVHSEnabled bool
RequestType ReqType
} }
// ObjectRequest represents object request data. // ObjectRequest represents object request data.
@ -197,57 +198,6 @@ func Request(log *zap.Logger, settings RequestSettings) Func {
} }
} }
// AddBucketName adds bucket name to ReqInfo from context.
func AddBucketName(l *zap.Logger) Func {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
reqInfo := GetReqInfo(ctx)
reqInfo.BucketName = chi.URLParam(r, BucketURLPrm)
if reqInfo.BucketName != "" {
reqLogger := reqLogOrDefault(ctx, l)
r = r.WithContext(SetReqLogger(ctx, reqLogger.With(zap.String("bucket", reqInfo.BucketName))))
}
h.ServeHTTP(w, r)
})
}
}
// AddObjectName adds objects name to ReqInfo from context.
func AddObjectName(l *zap.Logger) Func {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
reqInfo := GetReqInfo(ctx)
reqLogger := reqLogOrDefault(ctx, l)
rctx := chi.RouteContext(ctx)
// trim leading slash (always present)
reqInfo.ObjectName = rctx.RoutePath[1:]
if r.URL.RawPath != "" {
// we have to do this because of
// https://github.com/go-chi/chi/issues/641
// https://github.com/go-chi/chi/issues/642
if obj, err := url.PathUnescape(reqInfo.ObjectName); err != nil {
reqLogger.Warn(logs.FailedToUnescapeObjectName, zap.Error(err))
} else {
reqInfo.ObjectName = obj
}
}
if reqInfo.ObjectName != "" {
r = r.WithContext(SetReqLogger(ctx, reqLogger.With(zap.String("object", reqInfo.ObjectName))))
}
h.ServeHTTP(w, r)
})
}
}
// getSourceIP retrieves the IP from the X-Forwarded-For, X-Real-IP and RFC7239 // getSourceIP retrieves the IP from the X-Forwarded-For, X-Real-IP and RFC7239
// Forwarded headers (in that order), falls back to r.RemoteAddr when everything // Forwarded headers (in that order), falls back to r.RemoteAddr when everything
// else fails. // else fails.

View file

@ -97,6 +97,7 @@ type Settings interface {
s3middleware.RequestSettings s3middleware.RequestSettings
s3middleware.PolicySettings s3middleware.PolicySettings
s3middleware.MetricsSettings s3middleware.MetricsSettings
s3middleware.VHSSettings
} }
type FrostFSID interface { type FrostFSID interface {
@ -113,9 +114,6 @@ type Config struct {
MiddlewareSettings Settings MiddlewareSettings Settings
// Domains optional. If empty no virtual hosted domains will be attached.
Domains []string
FrostfsID FrostFSID FrostfsID FrostFSID
FrostFSIDValidation bool FrostFSIDValidation bool
@ -142,11 +140,11 @@ func NewRouter(cfg Config) *chi.Mux {
api.Use(s3middleware.FrostfsIDValidation(cfg.FrostfsID, cfg.Log)) api.Use(s3middleware.FrostfsIDValidation(cfg.FrostfsID, cfg.Log))
} }
api.Use(s3middleware.PrepareAddressStyle(cfg.MiddlewareSettings))
api.Use(s3middleware.PolicyCheck(s3middleware.PolicyConfig{ api.Use(s3middleware.PolicyCheck(s3middleware.PolicyConfig{
Storage: cfg.PolicyChecker, Storage: cfg.PolicyChecker,
FrostfsID: cfg.FrostfsID, FrostfsID: cfg.FrostfsID,
Settings: cfg.MiddlewareSettings, Settings: cfg.MiddlewareSettings,
Domains: cfg.Domains,
Log: cfg.Log, Log: cfg.Log,
BucketResolver: cfg.Handler.ResolveBucket, BucketResolver: cfg.Handler.ResolveBucket,
Decoder: cfg.XMLDecoder, Decoder: cfg.XMLDecoder,
@ -154,22 +152,42 @@ func NewRouter(cfg Config) *chi.Mux {
})) }))
defaultRouter := chi.NewRouter() defaultRouter := chi.NewRouter()
defaultRouter.Mount(fmt.Sprintf("/{%s}", s3middleware.BucketURLPrm), bucketRouter(cfg.Handler, cfg.Log)) defaultRouter.Mount(fmt.Sprintf("/{%s}", s3middleware.BucketURLPrm), bucketRouter(cfg.Handler))
defaultRouter.Get("/", named("ListBuckets", cfg.Handler.ListBucketsHandler)) defaultRouter.Get("/", named(s3middleware.ListBucketsOperation, cfg.Handler.ListBucketsHandler))
attachErrorHandler(defaultRouter) attachErrorHandler(defaultRouter)
hr := NewHostBucketRouter("bucket") vhsRouter := bucketRouter(cfg.Handler)
hr.Default(defaultRouter) vhsRouter.Get("/", listWrapper(cfg))
for _, domain := range cfg.Domains { router := newGlobalRouter(defaultRouter, vhsRouter)
hr.Map(domain, bucketRouter(cfg.Handler, cfg.Log))
} api.Mount("/", router)
api.Mount("/", hr)
attachErrorHandler(api) attachErrorHandler(api)
return api return api
} }
type globalRouter struct {
pathStyleRouter chi.Router
vhsRouter chi.Router
}
func newGlobalRouter(pathStyleRouter, vhsRouter chi.Router) *globalRouter {
return &globalRouter{
pathStyleRouter: pathStyleRouter,
vhsRouter: vhsRouter,
}
}
func (g *globalRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
router := g.pathStyleRouter
if reqInfo := s3middleware.GetReqInfo(r.Context()); reqInfo.RequestVHSEnabled {
router = g.vhsRouter
}
router.ServeHTTP(w, r)
}
func named(name string, handlerFunc http.HandlerFunc) http.HandlerFunc { func named(name string, handlerFunc http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
reqInfo := s3middleware.GetReqInfo(r.Context()) reqInfo := s3middleware.GetReqInfo(r.Context())
@ -178,6 +196,18 @@ func named(name string, handlerFunc http.HandlerFunc) http.HandlerFunc {
} }
} }
func listWrapper(cfg Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if reqInfo := s3middleware.GetReqInfo(r.Context()); reqInfo.BucketName == "" {
reqInfo.API = s3middleware.ListBucketsOperation
cfg.Handler.ListBucketsHandler(w, r)
} else {
reqInfo.API = s3middleware.ListObjectsV1Operation
cfg.Handler.ListObjectsV1Handler(w, r)
}
}
}
// If none of the http routes match respond with appropriate errors. // If none of the http routes match respond with appropriate errors.
func errorResponseHandler(w http.ResponseWriter, r *http.Request) { func errorResponseHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@ -214,14 +244,13 @@ func attachErrorHandler(api *chi.Mux) {
api.MethodNotAllowed(named("MethodNotAllowed", errorHandler)) api.MethodNotAllowed(named("MethodNotAllowed", errorHandler))
} }
func bucketRouter(h Handler, log *zap.Logger) chi.Router { func bucketRouter(h Handler) chi.Router {
bktRouter := chi.NewRouter() bktRouter := chi.NewRouter()
bktRouter.Use( bktRouter.Use(
s3middleware.AddBucketName(log),
s3middleware.WrapHandler(h.AppendCORSHeaders), s3middleware.WrapHandler(h.AppendCORSHeaders),
) )
bktRouter.Mount("/", objectRouter(h, log)) bktRouter.Mount("/", objectRouter(h))
bktRouter.Options("/", named(s3middleware.OptionsBucketOperation, h.Preflight)) bktRouter.Options("/", named(s3middleware.OptionsBucketOperation, h.Preflight))
@ -368,9 +397,8 @@ func bucketRouter(h Handler, log *zap.Logger) chi.Router {
return bktRouter return bktRouter
} }
func objectRouter(h Handler, l *zap.Logger) chi.Router { func objectRouter(h Handler) chi.Router {
objRouter := chi.NewRouter() objRouter := chi.NewRouter()
objRouter.Use(s3middleware.AddObjectName(l))
objRouter.Options("/*", named(s3middleware.OptionsObjectOperation, h.Preflight)) objRouter.Options("/*", named(s3middleware.OptionsObjectOperation, h.Preflight))

View file

@ -73,6 +73,9 @@ func (c *centerMock) Authenticate(*http.Request) (*middleware.Box, error) {
type middlewareSettingsMock struct { type middlewareSettingsMock struct {
denyByDefault bool denyByDefault bool
sourceIPHeader string sourceIPHeader string
domains []string
vhsEnabled bool
vhsNamespacesEnabled map[string]bool
} }
func (r *middlewareSettingsMock) SourceIPHeader() string { func (r *middlewareSettingsMock) SourceIPHeader() string {
@ -91,6 +94,18 @@ func (r *middlewareSettingsMock) PolicyDenyByDefault() bool {
return r.denyByDefault return r.denyByDefault
} }
func (r *middlewareSettingsMock) Domains() []string {
return r.domains
}
func (r *middlewareSettingsMock) GlobalVHS() bool {
return r.vhsEnabled
}
func (r *middlewareSettingsMock) VHSNamespacesEnabled() map[string]bool {
return r.vhsNamespacesEnabled
}
type frostFSIDMock struct { type frostFSIDMock struct {
tags map[string]string tags map[string]string
validateError bool validateError bool

View file

@ -78,7 +78,6 @@ func prepareRouter(t *testing.T, opts ...option) *routerMock {
Metrics: metrics.NewAppMetrics(metricsConfig), Metrics: metrics.NewAppMetrics(metricsConfig),
MiddlewareSettings: middlewareSettings, MiddlewareSettings: middlewareSettings,
PolicyChecker: policyChecker, PolicyChecker: policyChecker,
Domains: []string{"domain1", "domain2"},
FrostfsID: &frostFSIDMock{}, FrostfsID: &frostFSIDMock{},
XMLDecoder: &xmlMock{}, XMLDecoder: &xmlMock{},
Tagging: &resourceTaggingMock{}, Tagging: &resourceTaggingMock{},

View file

@ -105,6 +105,9 @@ type (
policyDenyByDefault bool policyDenyByDefault bool
sourceIPHeader string sourceIPHeader string
retryMaxAttempts int retryMaxAttempts int
domains []string
vhsEnabled bool
vhsNamespacesEnabled map[string]bool
retryMaxBackoff time.Duration retryMaxBackoff time.Duration
retryStrategy handler.RetryStrategy retryStrategy handler.RetryStrategy
} }
@ -231,6 +234,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) {
s.setRetryMaxAttempts(fetchRetryMaxAttempts(v)) s.setRetryMaxAttempts(fetchRetryMaxAttempts(v))
s.setRetryMaxBackoff(fetchRetryMaxBackoff(v)) s.setRetryMaxBackoff(fetchRetryMaxBackoff(v))
s.setRetryStrategy(fetchRetryStrategy(v)) s.setRetryStrategy(fetchRetryStrategy(v))
s.setVHSSettings(v, log)
} }
func (s *appSettings) updateNamespacesSettings(v *viper.Viper, log *zap.Logger) { func (s *appSettings) updateNamespacesSettings(v *viper.Viper, log *zap.Logger) {
@ -245,6 +249,39 @@ func (s *appSettings) updateNamespacesSettings(v *viper.Viper, log *zap.Logger)
s.namespaces = nsConfig.Namespaces s.namespaces = nsConfig.Namespaces
} }
func (s *appSettings) setVHSSettings(v *viper.Viper, log *zap.Logger) {
nsMap := fetchVHSNamespaces(v, log)
vhsNamespaces := make(map[string]bool, len(nsMap))
for ns, flag := range nsMap {
vhsNamespaces[s.ResolveNamespaceAlias(ns)] = flag
}
s.mu.Lock()
defer s.mu.Unlock()
s.domains = fetchDomains(v)
s.vhsEnabled = v.GetBool(cfgVHSEnabled)
s.vhsNamespacesEnabled = vhsNamespaces
}
func (s *appSettings) Domains() []string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.domains
}
func (s *appSettings) GlobalVHS() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.vhsEnabled
}
func (s *appSettings) VHSNamespacesEnabled() map[string]bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.vhsNamespacesEnabled
}
func (s *appSettings) BypassContentEncodingInChunks() bool { func (s *appSettings) BypassContentEncodingInChunks() bool {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
@ -683,10 +720,6 @@ func (a *App) setHealthStatus() {
// Serve runs HTTP server to handle S3 API requests. // Serve runs HTTP server to handle S3 API requests.
func (a *App) Serve(ctx context.Context) { func (a *App) Serve(ctx context.Context) {
// Attach S3 API:
domains := a.cfg.GetStringSlice(cfgListenDomains)
a.log.Info(logs.FetchDomainsPrepareToUseAPI, zap.Strings("domains", domains))
cfg := api.Config{ cfg := api.Config{
Throttle: middleware.ThrottleOpts{ Throttle: middleware.ThrottleOpts{
Limit: a.settings.maxClient.count, Limit: a.settings.maxClient.count,
@ -696,7 +729,6 @@ func (a *App) Serve(ctx context.Context) {
Center: a.ctr, Center: a.ctr,
Log: a.log, Log: a.log,
Metrics: a.metrics, Metrics: a.metrics,
Domains: domains,
MiddlewareSettings: a.settings, MiddlewareSettings: a.settings,
PolicyChecker: a.policyStorage, PolicyChecker: a.policyStorage,

View file

@ -144,6 +144,9 @@ const ( // Settings.
cfgListenDomains = "listen_domains" cfgListenDomains = "listen_domains"
cfgVHSEnabled = "vhs.enabled"
cfgVHSNamespaces = "vhs.namespaces"
// Peers. // Peers.
cfgPeers = "peers" cfgPeers = "peers"
@ -667,6 +670,45 @@ func fetchServers(v *viper.Viper, log *zap.Logger) []ServerInfo {
return servers return servers
} }
func fetchDomains(v *viper.Viper) []string {
domains := v.GetStringSlice(cfgListenDomains)
countParts := func(domain string) int {
return strings.Count(domain, ".")
}
sort.Slice(domains, func(i, j int) bool {
return countParts(domains[i]) > countParts(domains[j])
})
return domains
}
func fetchVHSNamespaces(v *viper.Viper, log *zap.Logger) map[string]bool {
vhsNamespacesEnabled := make(map[string]bool)
nsMap := v.GetStringMap(cfgVHSNamespaces)
for ns, val := range nsMap {
if _, ok := vhsNamespacesEnabled[ns]; ok {
log.Warn(logs.WarnDuplicateNamespaceVHS, zap.String("namespace", ns))
continue
}
enabledFlag, ok := val.(bool)
if !ok {
log.Warn(logs.WarnValueVHSEnabledFlagWrongType, zap.String("namespace", ns))
continue
}
vhsNamespacesEnabled[ns] = enabledFlag
}
if len(vhsNamespacesEnabled) > 0 {
return vhsNamespacesEnabled
}
return nil
}
func newSettings() *viper.Viper { func newSettings() *viper.Viper {
v := viper.New() v := viper.New()

View file

@ -36,8 +36,11 @@ S3_GW_SERVER_1_TLS_KEY_FILE=/path/to/tls/key
# How often to reconnect to the servers # How often to reconnect to the servers
S3_GW_RECONNECT_INTERVAL: 1m S3_GW_RECONNECT_INTERVAL: 1m
# Domains to be able to use virtual-hosted-style access to bucket. # Domains to be able to use virtual-hosted-style access to bucket
S3_GW_LISTEN_DOMAINS=s3dev.frostfs.devenv S3_GW_LISTEN_DOMAINS="domain.com <wildcard>.domain.com"
# VHS enabled flag
S3_GW_VHS_ENABLED=false
# Config file # Config file
S3_GW_CONFIG=/path/to/config/yaml S3_GW_CONFIG=/path/to/config/yaml

View file

@ -42,6 +42,13 @@ server:
# Domains to be able to use virtual-hosted-style access to bucket. # Domains to be able to use virtual-hosted-style access to bucket.
listen_domains: listen_domains:
- s3dev.frostfs.devenv - s3dev.frostfs.devenv
- s3dev.<wildcard>.frostfs.devenv
vhs:
enabled: false
namespaces:
"ns1": false
"ns2": true
logger: logger:
level: debug level: debug

View file

@ -193,12 +193,14 @@ There are some custom types used for brevity:
| `namespaces` | [Namespaces configuration](#namespaces-section) | | `namespaces` | [Namespaces configuration](#namespaces-section) |
| `retry` | [Retry configuration](#retry-section) | | `retry` | [Retry configuration](#retry-section) |
| `containers` | [Containers configuration](#containers-section) | | `containers` | [Containers configuration](#containers-section) |
| `vhs` | [VHS configuration](#vhs-section) |
### General section ### General section
```yaml ```yaml
listen_domains: listen_domains:
- s3dev.frostfs.devenv - s3dev.frostfs.devenv
- s3dev.<wildcard>.frostfs.devenv
- s3dev2.frostfs.devenv - s3dev2.frostfs.devenv
rpc_endpoint: http://morph-chain.frostfs.devenv:30333 rpc_endpoint: http://morph-chain.frostfs.devenv:30333
@ -226,7 +228,7 @@ source_ip_header: "Source-Ip"
| Parameter | Type | SIGHUP reload | Default value | Description | | Parameter | Type | SIGHUP reload | Default value | Description |
|----------------------------------|------------|---------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |----------------------------------|------------|---------------|---------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `listen_domains` | `[]string` | no | | Domains to be able to use virtual-hosted-style access to bucket. | | `listen_domains` | `[]string` | yes | | Domains to be able to use virtual-hosted-style access to bucket. The presence of placeholders of the <wildcard> type is supported. |
| `rpc_endpoint` | `string` | no | | The address of the RPC host to which the gateway connects to resolve bucket names and interact with frostfs contracts (required to use the `nns` resolver and `frostfsid` contract). | | `rpc_endpoint` | `string` | no | | The address of the RPC host to which the gateway connects to resolve bucket names and interact with frostfs contracts (required to use the `nns` resolver and `frostfsid` contract). |
| `resolve_order` | `[]string` | yes | `[dns]` | Order of bucket name resolvers to use. Available resolvers: `dns`, `nns`. | | `resolve_order` | `[]string` | yes | `[dns]` | Order of bucket name resolvers to use. Available resolvers: `dns`, `nns`. |
| `connect_timeout` | `duration` | no | `10s` | Timeout to connect to a node. | | `connect_timeout` | `duration` | no | `10s` | Timeout to connect to a node. |
@ -721,3 +723,20 @@ containers:
| Parameter | Type | SIGHUP reload | Default value | Description | | 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. If not set, container of the bucket is used. |
# `vhs` section
Configuration of virtual hosted addressing style.
```yaml
vhs:
enabled: false
namespaces:
"ns1": false
"ns2": true
```
| Parameter | Type | SIGHUP reload | Default value | Description |
|--------------|-------------------|---------------|---------------|----------------------------------------------------------------------------------------------------------------------------------------------|
| `enabled` | `bool` | yes | `false` | Enables the use of virtual host addressing for banquets at the application level. |
| `namespaces` | `map[string]bool` | yes | | A map in which the keys are the name of the namespace, and the values are the flag responsible for enabling VHS for the specified namespace. |

View file

@ -20,7 +20,6 @@ const (
UsingCredentials = "using credentials" // Info in ../../cmd/s3-gw/app.go UsingCredentials = "using credentials" // Info in ../../cmd/s3-gw/app.go
ApplicationStarted = "application started" // Info in ../../cmd/s3-gw/app.go ApplicationStarted = "application started" // Info in ../../cmd/s3-gw/app.go
ApplicationFinished = "application finished" // Info in ../../cmd/s3-gw/app.go ApplicationFinished = "application finished" // Info in ../../cmd/s3-gw/app.go
FetchDomainsPrepareToUseAPI = "fetch domains, prepare to use API" // Info in ../../cmd/s3-gw/app.go
StartingServer = "starting server" // Info in ../../cmd/s3-gw/app.go StartingServer = "starting server" // Info in ../../cmd/s3-gw/app.go
StoppingServer = "stopping server" // Info in ../../cmd/s3-gw/app.go StoppingServer = "stopping server" // Info in ../../cmd/s3-gw/app.go
SIGHUPConfigReloadStarted = "SIGHUP config reload started" // Info in ../../cmd/s3-gw/app.go SIGHUPConfigReloadStarted = "SIGHUP config reload started" // Info in ../../cmd/s3-gw/app.go
@ -100,7 +99,6 @@ const (
FailedToPassAuthentication = "failed to pass authentication" // Error in ../../api/middleware/auth.go FailedToPassAuthentication = "failed to pass authentication" // Error in ../../api/middleware/auth.go
FailedToResolveCID = "failed to resolve CID" // Debug in ../../api/middleware/metrics.go FailedToResolveCID = "failed to resolve CID" // Debug in ../../api/middleware/metrics.go
RequestStart = "request start" // Info in ../../api/middleware/reqinfo.go RequestStart = "request start" // Info in ../../api/middleware/reqinfo.go
FailedToUnescapeObjectName = "failed to unescape object name" // Warn in ../../api/middleware/reqinfo.go
InvalidDefaultMaxAge = "invalid defaultMaxAge" // Fatal in ../../cmd/s3-gw/app_settings.go InvalidDefaultMaxAge = "invalid defaultMaxAge" // Fatal in ../../cmd/s3-gw/app_settings.go
CantShutDownService = "can't shut down service" // Panic in ../../cmd/s3-gw/service.go CantShutDownService = "can't shut down service" // Panic in ../../cmd/s3-gw/service.go
CouldntGenerateRandomKey = "couldn't generate random key" // Fatal in ../../cmd/s3-gw/app.go CouldntGenerateRandomKey = "couldn't generate random key" // Fatal in ../../cmd/s3-gw/app.go
@ -154,4 +152,6 @@ const (
FailedToParsePartInfo = "failed to parse part info" FailedToParsePartInfo = "failed to parse part info"
CouldNotFetchCORSContainerInfo = "couldn't fetch CORS container info" CouldNotFetchCORSContainerInfo = "couldn't fetch CORS container info"
CloseCredsObjectPayload = "close creds object payload" CloseCredsObjectPayload = "close creds object payload"
WarnDuplicateNamespaceVHS = "duplicate namespace with enabled VHS, config value skipped"
WarnValueVHSEnabledFlagWrongType = "the value of the VHS enable flag for the namespace is of the wrong type, config value skipped"
) )