diff --git a/CHANGELOG.md b/CHANGELOG.md index 9774c521..2bc99b66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ This document outlines major changes between releases. ## [Unreleased] ### 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 diff --git a/api/middleware/address_style.go b/api/middleware/address_style.go index fa9e8c3f..12b1cdda 100644 --- a/api/middleware/address_style.go +++ b/api/middleware/address_style.go @@ -3,6 +3,7 @@ package middleware import ( "net/http" "net/url" + "strconv" "strings" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" @@ -14,6 +15,8 @@ const wildcardPlaceholder = "" type VHSSettings interface { Domains() []string GlobalVHS() bool + VHSHeader() string + ServernameHeader() string VHSNamespacesEnabled() map[string]bool } @@ -23,8 +26,9 @@ func PrepareAddressStyle(settings VHSSettings, log *zap.Logger) Func { ctx := r.Context() reqInfo := GetReqInfo(ctx) 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) } else { 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 { - result := enabledFlag +func isVHSAddress(headerVHSEnabled string, enabledFlag bool, vhsNamespaces map[string]bool, namespace string) bool { + if result, err := strconv.ParseBool(headerVHSEnabled); err == nil { + return result + } + result := enabledFlag if v, ok := vhsNamespaces[namespace]; ok { 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) { reqInfo.RequestVHSEnabled = true - bktName, match := checkDomain(r.Host, settings.Domains()) + bktName, match := checkDomain(r.Host, getDomains(r, settings)) if match { if bktName == "" { 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) { bktObj := strings.TrimPrefix(r.URL.Path, "/") if bktObj == "" { diff --git a/api/middleware/address_style_test.go b/api/middleware/address_style_test.go index 43b03eab..4089bfe3 100644 --- a/api/middleware/address_style_test.go +++ b/api/middleware/address_style_test.go @@ -10,6 +10,11 @@ import ( "go.uber.org/zap/zaptest" ) +const ( + FrostfsVHSHeader = "X-Frostfs-S3-VHS" + FrostfsServernameHeader = "X-Frostfs-Servername" +) + type VHSSettingsMock struct { domains []string } @@ -22,17 +27,26 @@ func (v *VHSSettingsMock) GlobalVHS() bool { return false } +func (v *VHSSettingsMock) VHSHeader() string { + return FrostfsVHSHeader +} + +func (v *VHSSettingsMock) ServernameHeader() string { + return FrostfsServernameHeader +} + func (v *VHSSettingsMock) VHSNamespacesEnabled() map[string]bool { return make(map[string]bool) } func TestIsVHSAddress(t *testing.T) { for _, tc := range []struct { - name string - vhsEnabledFlag bool - vhsNamespaced map[string]bool - namespace string - expected bool + name string + headerVHSEnabled string + vhsEnabledFlag bool + vhsNamespaced map[string]bool + namespace string + expected bool }{ { name: "vhs disabled", @@ -60,9 +74,29 @@ func TestIsVHSAddress(t *testing.T) { namespace: "kapusta", 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) { - actual := isVHSAddress(tc.vhsEnabledFlag, tc.vhsNamespaced, tc.namespace) + actual := isVHSAddress(tc.headerVHSEnabled, tc.vhsEnabledFlag, tc.vhsNamespaced, tc.namespace) 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..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) + }) +} diff --git a/api/router_mock_test.go b/api/router_mock_test.go index 4dd5d90f..f21943d9 100644 --- a/api/router_mock_test.go +++ b/api/router_mock_test.go @@ -23,7 +23,11 @@ import ( "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 { } @@ -102,6 +106,14 @@ func (r *middlewareSettingsMock) GlobalVHS() bool { return r.vhsEnabled } +func (r *middlewareSettingsMock) VHSHeader() string { + return FrostfsVHSHeader +} + +func (r *middlewareSettingsMock) ServernameHeader() string { + return FrostfsServernameHeader +} + func (r *middlewareSettingsMock) VHSNamespacesEnabled() map[string]bool { return r.vhsNamespacesEnabled } diff --git a/cmd/s3-gw/app.go b/cmd/s3-gw/app.go index 08eee964..f77bcda7 100644 --- a/cmd/s3-gw/app.go +++ b/cmd/s3-gw/app.go @@ -107,6 +107,8 @@ type ( retryMaxAttempts int domains []string vhsEnabled bool + vhsHeader string + servernameHeader string vhsNamespacesEnabled map[string]bool retryMaxBackoff time.Duration 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) { domains := fetchDomains(v, log) vhsEnabled := v.GetBool(cfgVHSEnabled) + vhsHeader := v.GetString(cfgVHSHeader) + servernameHeader := v.GetString(cfgServernameHeader) nsMap := fetchVHSNamespaces(v, log) vhsNamespaces := make(map[string]bool, len(nsMap)) for ns, flag := range nsMap { @@ -272,6 +276,8 @@ func (s *appSettings) setVHSSettings(v *viper.Viper, log *zap.Logger) { s.domains = domains s.vhsEnabled = vhsEnabled + s.vhsHeader = vhsHeader + s.servernameHeader = servernameHeader s.vhsNamespacesEnabled = vhsNamespaces } @@ -287,6 +293,18 @@ func (s *appSettings) GlobalVHS() bool { 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 { s.mu.RLock() defer s.mu.RUnlock() diff --git a/cmd/s3-gw/app_settings.go b/cmd/s3-gw/app_settings.go index 065901db..8ff90c93 100644 --- a/cmd/s3-gw/app_settings.go +++ b/cmd/s3-gw/app_settings.go @@ -54,7 +54,9 @@ const ( defaultAccessBoxCacheRemovingCheckInterval = 5 * time.Minute - defaultNamespaceHeader = "X-Frostfs-Namespace" + defaultNamespaceHeader = "X-Frostfs-Namespace" + defaultVHSHeader = "X-Frostfs-S3-VHS" + defaultServernameHeader = "X-Frostfs-Servername" defaultConstraintName = "default" @@ -146,8 +148,11 @@ const ( // Settings. cfgListenDomains = "listen_domains" - cfgVHSEnabled = "vhs.enabled" - cfgVHSNamespaces = "vhs.namespaces" + // VHS. + cfgVHSEnabled = "vhs.enabled" + cfgVHSHeader = "vhs.vhs_header" + cfgServernameHeader = "vhs.servername_header" + cfgVHSNamespaces = "vhs.namespaces" // Peers. cfgPeers = "peers" @@ -794,6 +799,10 @@ func newSettings() *viper.Viper { v.SetDefault(cfgRetryMaxAttempts, defaultRetryMaxAttempts) v.SetDefault(cfgRetryMaxBackoff, defaultRetryMaxBackoff) + // vhs + v.SetDefault(cfgVHSHeader, defaultVHSHeader) + v.SetDefault(cfgServernameHeader, defaultServernameHeader) + // Bind flags if err := bindFlags(v, flags); err != nil { panic(fmt.Errorf("bind flags: %w", err)) diff --git a/config/config.env b/config/config.env index 22288b1a..9c31f10a 100644 --- a/config/config.env +++ b/config/config.env @@ -41,6 +41,10 @@ S3_GW_LISTEN_DOMAINS="domain.com .domain.com" # VHS enabled flag 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 S3_GW_CONFIG=/path/to/config/yaml diff --git a/config/config.yaml b/config/config.yaml index cc3a6d04..6d6de81c 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -46,6 +46,8 @@ listen_domains: vhs: enabled: false + vhs_header: X-Frostfs-S3-VHS + servername_header: X-Frostfs-Servername namespaces: "ns1": false "ns2": true diff --git a/docs/configuration.md b/docs/configuration.md index cfb7a719..3f52ca86 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -733,12 +733,16 @@ Configuration of virtual hosted addressing style. ```yaml vhs: enabled: false + vhs_header: X-Frostfs-S3-VHS + servername_header: X-Frostfs-Servername 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. Overrides global 'enabled' setting even when it is disabled. | +| Parameter | Type | SIGHUP reload | Default value | Description | +| ------------------- | ----------------- | ------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `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. |