[#449] Add support headers for vhs and servername

Signed-off-by: Roman Loginov <r.loginov@yadro.com>
This commit is contained in:
Roman Loginov 2024-08-01 16:24:47 +03:00 committed by Alexey Vanin
parent ff690ce996
commit bf00fa6aa9
9 changed files with 141 additions and 19 deletions

View file

@ -5,7 +5,7 @@ This document outlines major changes between releases.
## [Unreleased] ## [Unreleased]
### Added ### Added
- Add support for virtual hosted style addressing (#446) - Add support for virtual hosted style addressing (#446, #449)
## [0.30.0] - Kangshung -2024-07-19 ## [0.30.0] - Kangshung -2024-07-19

View file

@ -3,6 +3,7 @@ package middleware
import ( import (
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
@ -14,6 +15,8 @@ const wildcardPlaceholder = "<wildcard>"
type VHSSettings interface { type VHSSettings interface {
Domains() []string Domains() []string
GlobalVHS() bool GlobalVHS() bool
VHSHeader() string
ServernameHeader() string
VHSNamespacesEnabled() map[string]bool VHSNamespacesEnabled() map[string]bool
} }
@ -23,8 +26,9 @@ func PrepareAddressStyle(settings VHSSettings, log *zap.Logger) Func {
ctx := r.Context() ctx := r.Context()
reqInfo := GetReqInfo(ctx) reqInfo := GetReqInfo(ctx)
reqLogger := reqLogOrDefault(ctx, log) reqLogger := reqLogOrDefault(ctx, log)
headerVHSEnabled := r.Header.Get(settings.VHSHeader())
if isVHSAddress(settings.GlobalVHS(), settings.VHSNamespacesEnabled(), reqInfo.Namespace) { if isVHSAddress(headerVHSEnabled, settings.GlobalVHS(), settings.VHSNamespacesEnabled(), reqInfo.Namespace) {
prepareVHSAddress(reqInfo, r, settings) prepareVHSAddress(reqInfo, r, settings)
} else { } else {
preparePathStyleAddress(reqInfo, r, reqLogger) preparePathStyleAddress(reqInfo, r, reqLogger)
@ -35,9 +39,12 @@ func PrepareAddressStyle(settings VHSSettings, log *zap.Logger) Func {
} }
} }
func isVHSAddress(enabledFlag bool, vhsNamespaces map[string]bool, namespace string) bool { func isVHSAddress(headerVHSEnabled string, enabledFlag bool, vhsNamespaces map[string]bool, namespace string) bool {
result := enabledFlag if result, err := strconv.ParseBool(headerVHSEnabled); err == nil {
return result
}
result := enabledFlag
if v, ok := vhsNamespaces[namespace]; ok { if v, ok := vhsNamespaces[namespace]; ok {
result = v result = v
} }
@ -47,7 +54,7 @@ func isVHSAddress(enabledFlag bool, vhsNamespaces map[string]bool, namespace str
func prepareVHSAddress(reqInfo *ReqInfo, r *http.Request, settings VHSSettings) { func prepareVHSAddress(reqInfo *ReqInfo, r *http.Request, settings VHSSettings) {
reqInfo.RequestVHSEnabled = true reqInfo.RequestVHSEnabled = true
bktName, match := checkDomain(r.Host, settings.Domains()) bktName, match := checkDomain(r.Host, getDomains(r, settings))
if match { if match {
if bktName == "" { if bktName == "" {
reqInfo.RequestType = noneType reqInfo.RequestType = noneType
@ -74,6 +81,14 @@ func prepareVHSAddress(reqInfo *ReqInfo, r *http.Request, settings VHSSettings)
} }
} }
func getDomains(r *http.Request, settings VHSSettings) []string {
if headerServername := r.Header.Get(settings.ServernameHeader()); headerServername != "" {
return []string{headerServername}
}
return settings.Domains()
}
func preparePathStyleAddress(reqInfo *ReqInfo, r *http.Request, reqLogger *zap.Logger) { func preparePathStyleAddress(reqInfo *ReqInfo, r *http.Request, reqLogger *zap.Logger) {
bktObj := strings.TrimPrefix(r.URL.Path, "/") bktObj := strings.TrimPrefix(r.URL.Path, "/")
if bktObj == "" { if bktObj == "" {

View file

@ -10,6 +10,11 @@ import (
"go.uber.org/zap/zaptest" "go.uber.org/zap/zaptest"
) )
const (
FrostfsVHSHeader = "X-Frostfs-S3-VHS"
FrostfsServernameHeader = "X-Frostfs-Servername"
)
type VHSSettingsMock struct { type VHSSettingsMock struct {
domains []string domains []string
} }
@ -22,6 +27,14 @@ func (v *VHSSettingsMock) GlobalVHS() bool {
return false return false
} }
func (v *VHSSettingsMock) VHSHeader() string {
return FrostfsVHSHeader
}
func (v *VHSSettingsMock) ServernameHeader() string {
return FrostfsServernameHeader
}
func (v *VHSSettingsMock) VHSNamespacesEnabled() map[string]bool { func (v *VHSSettingsMock) VHSNamespacesEnabled() map[string]bool {
return make(map[string]bool) return make(map[string]bool)
} }
@ -29,6 +42,7 @@ func (v *VHSSettingsMock) VHSNamespacesEnabled() map[string]bool {
func TestIsVHSAddress(t *testing.T) { func TestIsVHSAddress(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
name string name string
headerVHSEnabled string
vhsEnabledFlag bool vhsEnabledFlag bool
vhsNamespaced map[string]bool vhsNamespaced map[string]bool
namespace string namespace string
@ -60,9 +74,29 @@ func TestIsVHSAddress(t *testing.T) {
namespace: "kapusta", namespace: "kapusta",
expected: true, expected: true,
}, },
{
name: "vhs enabled (header)",
headerVHSEnabled: "true",
vhsEnabledFlag: false,
vhsNamespaced: map[string]bool{
"kapusta": false,
},
namespace: "kapusta",
expected: true,
},
{
name: "vhs disabled (header)",
headerVHSEnabled: "false",
vhsEnabledFlag: true,
vhsNamespaced: map[string]bool{
"kapusta": true,
},
namespace: "kapusta",
expected: false,
},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
actual := isVHSAddress(tc.vhsEnabledFlag, tc.vhsNamespaced, tc.namespace) actual := isVHSAddress(tc.headerVHSEnabled, tc.vhsEnabledFlag, tc.vhsNamespaced, tc.namespace)
require.Equal(t, tc.expected, actual) require.Equal(t, tc.expected, actual)
}) })
} }
@ -383,3 +417,27 @@ func TestCheckDomains(t *testing.T) {
}) })
} }
} }
func TestGetDomains(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
settings := &VHSSettingsMock{
domains: []string{
"s3.domain.com",
"s3.<wildcard>.domain.com",
"domain.com",
},
}
t.Run("the request does not contain the X-Frostfs-Servername header", func(t *testing.T) {
actualDomains := getDomains(req, settings)
require.Equal(t, settings.domains, actualDomains)
})
serverName := "domain.com"
req.Header.Set(settings.ServernameHeader(), serverName)
t.Run("the request contains the X-Frostfs-Servername header", func(t *testing.T) {
actualDomains := getDomains(req, settings)
require.Equal(t, []string{serverName}, actualDomains)
})
}

View file

@ -23,7 +23,11 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const FrostfsNamespaceHeader = "X-Frostfs-Namespace" const (
FrostfsNamespaceHeader = "X-Frostfs-Namespace"
FrostfsVHSHeader = "X-Frostfs-S3-VHS"
FrostfsServernameHeader = "X-Frostfs-Servername"
)
type poolStatisticMock struct { type poolStatisticMock struct {
} }
@ -102,6 +106,14 @@ func (r *middlewareSettingsMock) GlobalVHS() bool {
return r.vhsEnabled return r.vhsEnabled
} }
func (r *middlewareSettingsMock) VHSHeader() string {
return FrostfsVHSHeader
}
func (r *middlewareSettingsMock) ServernameHeader() string {
return FrostfsServernameHeader
}
func (r *middlewareSettingsMock) VHSNamespacesEnabled() map[string]bool { func (r *middlewareSettingsMock) VHSNamespacesEnabled() map[string]bool {
return r.vhsNamespacesEnabled return r.vhsNamespacesEnabled
} }

View file

@ -107,6 +107,8 @@ type (
retryMaxAttempts int retryMaxAttempts int
domains []string domains []string
vhsEnabled bool vhsEnabled bool
vhsHeader string
servernameHeader string
vhsNamespacesEnabled map[string]bool vhsNamespacesEnabled map[string]bool
retryMaxBackoff time.Duration retryMaxBackoff time.Duration
retryStrategy handler.RetryStrategy retryStrategy handler.RetryStrategy
@ -261,6 +263,8 @@ func (s *appSettings) updateNamespacesSettings(v *viper.Viper, log *zap.Logger)
func (s *appSettings) setVHSSettings(v *viper.Viper, log *zap.Logger) { func (s *appSettings) setVHSSettings(v *viper.Viper, log *zap.Logger) {
domains := fetchDomains(v, log) domains := fetchDomains(v, log)
vhsEnabled := v.GetBool(cfgVHSEnabled) vhsEnabled := v.GetBool(cfgVHSEnabled)
vhsHeader := v.GetString(cfgVHSHeader)
servernameHeader := v.GetString(cfgServernameHeader)
nsMap := fetchVHSNamespaces(v, log) nsMap := fetchVHSNamespaces(v, log)
vhsNamespaces := make(map[string]bool, len(nsMap)) vhsNamespaces := make(map[string]bool, len(nsMap))
for ns, flag := range nsMap { for ns, flag := range nsMap {
@ -272,6 +276,8 @@ func (s *appSettings) setVHSSettings(v *viper.Viper, log *zap.Logger) {
s.domains = domains s.domains = domains
s.vhsEnabled = vhsEnabled s.vhsEnabled = vhsEnabled
s.vhsHeader = vhsHeader
s.servernameHeader = servernameHeader
s.vhsNamespacesEnabled = vhsNamespaces s.vhsNamespacesEnabled = vhsNamespaces
} }
@ -287,6 +293,18 @@ func (s *appSettings) GlobalVHS() bool {
return s.vhsEnabled return s.vhsEnabled
} }
func (s *appSettings) VHSHeader() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.vhsHeader
}
func (s *appSettings) ServernameHeader() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.servernameHeader
}
func (s *appSettings) VHSNamespacesEnabled() map[string]bool { func (s *appSettings) VHSNamespacesEnabled() map[string]bool {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()

View file

@ -55,6 +55,8 @@ const (
defaultAccessBoxCacheRemovingCheckInterval = 5 * time.Minute defaultAccessBoxCacheRemovingCheckInterval = 5 * time.Minute
defaultNamespaceHeader = "X-Frostfs-Namespace" defaultNamespaceHeader = "X-Frostfs-Namespace"
defaultVHSHeader = "X-Frostfs-S3-VHS"
defaultServernameHeader = "X-Frostfs-Servername"
defaultConstraintName = "default" defaultConstraintName = "default"
@ -146,7 +148,10 @@ const ( // Settings.
cfgListenDomains = "listen_domains" cfgListenDomains = "listen_domains"
// VHS.
cfgVHSEnabled = "vhs.enabled" cfgVHSEnabled = "vhs.enabled"
cfgVHSHeader = "vhs.vhs_header"
cfgServernameHeader = "vhs.servername_header"
cfgVHSNamespaces = "vhs.namespaces" cfgVHSNamespaces = "vhs.namespaces"
// Peers. // Peers.
@ -794,6 +799,10 @@ func newSettings() *viper.Viper {
v.SetDefault(cfgRetryMaxAttempts, defaultRetryMaxAttempts) v.SetDefault(cfgRetryMaxAttempts, defaultRetryMaxAttempts)
v.SetDefault(cfgRetryMaxBackoff, defaultRetryMaxBackoff) v.SetDefault(cfgRetryMaxBackoff, defaultRetryMaxBackoff)
// vhs
v.SetDefault(cfgVHSHeader, defaultVHSHeader)
v.SetDefault(cfgServernameHeader, defaultServernameHeader)
// Bind flags // Bind flags
if err := bindFlags(v, flags); err != nil { if err := bindFlags(v, flags); err != nil {
panic(fmt.Errorf("bind flags: %w", err)) panic(fmt.Errorf("bind flags: %w", err))

View file

@ -41,6 +41,10 @@ S3_GW_LISTEN_DOMAINS="domain.com <wildcard>.domain.com"
# VHS enabled flag # VHS enabled flag
S3_GW_VHS_ENABLED=false S3_GW_VHS_ENABLED=false
# Header for determining whether VHS is enabled for the request
S3_GW_VHS_VHS_HEADER=X-Frostfs-S3-VHS
# Header for determining servername
S3_GW_VHS_SERVERNAME_HEADER=X-Frostfs-Servername
# Config file # Config file
S3_GW_CONFIG=/path/to/config/yaml S3_GW_CONFIG=/path/to/config/yaml

View file

@ -46,6 +46,8 @@ listen_domains:
vhs: vhs:
enabled: false enabled: false
vhs_header: X-Frostfs-S3-VHS
servername_header: X-Frostfs-Servername
namespaces: namespaces:
"ns1": false "ns1": false
"ns2": true "ns2": true

View file

@ -733,12 +733,16 @@ Configuration of virtual hosted addressing style.
```yaml ```yaml
vhs: vhs:
enabled: false enabled: false
vhs_header: X-Frostfs-S3-VHS
servername_header: X-Frostfs-Servername
namespaces: namespaces:
"ns1": false "ns1": false
"ns2": true "ns2": true
``` ```
| Parameter | Type | SIGHUP reload | Default value | Description | | Parameter | Type | SIGHUP reload | Default value | Description |
| ------------ | ----------------- | ------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------- | ----------------- | ------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | `bool` | yes | `false` | Enables the use of virtual host addressing for banquets at the application level. | | `enabled` | `bool` | yes | `false` | Enables the use of virtual host addressing for buckets at the application level. |
| `vhs_header` | `string` | yes | `X-Frostfs-S3-VHS` | Header for determining whether VHS is enabled for the request. |
| `servername_header` | `string` | yes | `X-Frostfs-Servername` | Header for determining servername. |
| `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. Overrides global 'enabled' setting even when it is disabled. | | `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. Overrides global 'enabled' setting even when it is disabled. |