diff --git a/CHANGELOG.md b/CHANGELOG.md index 9774c52..2bc99b6 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 fa9e8c3..12b1cdd 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 43b03ea..4089bfe 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 4dd5d90..f21943d 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 08eee96..f77bcda 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 065901d..8ff90c9 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 22288b1..9c31f10 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 cc3a6d0..6d6de81 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 cfb7a71..3f52ca8 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. |