Skip to content

Commit d3f9532

Browse files
authored
fix: Fix regression in S3 client configuration (#20110)
### Summary This PR fixes a regression in how Loki's S3 configuration options are handled to create the S3 client which was introduced with the upgrade of the AWS SDK from v1 to v2 (#19205). With the v1 SDK the `s3` field of the `aws` configuration block was parsed to extract credentials, region, host, and schema. ``` storage_config: aws: s3: s3://<accesskey>:<secret>@<endpoint_or_region>/<bucketnames> ``` After the upgrade, only credentials were extraced, leaving all other parts of the URL as default, which would require explicit overrides. This was a breaking change and not documented in the upgrade notes. We want to keep backwards compatibility, even though the Thanos object client is going to supersed the custom S3/GCS/... clients. ref: #19908 Signed-off-by: Christian Haudum <christian.haudum@gmail.com>
1 parent a35ccab commit d3f9532

4 files changed

Lines changed: 249 additions & 116 deletions

File tree

‎pkg/storage/chunk/client/aws/config.go‎

Lines changed: 2 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,12 @@
1-
// Provenance-includes-location: https://github.com/weaveworks/common/blob/main/aws/config.go
2-
// Provenance-includes-license: Apache-2.0
3-
// Provenance-includes-copyright: Weaveworks Ltd.
4-
51
package aws
62

73
import (
8-
"fmt"
9-
"net"
10-
"net/http"
114
"net/url"
12-
"os"
13-
"strings"
14-
"time"
15-
16-
"github.com/aws/aws-sdk-go-v2/aws"
17-
"github.com/aws/aws-sdk-go-v2/credentials"
18-
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
195
)
206

21-
// DynamoConfigFromURL returns AWS config from given URL. It expects escaped
22-
// AWS Access key ID & Secret Access Key to be encoded in the URL. It
23-
// also expects region specified as a host (letting AWS generate full
24-
// endpoint) or fully valid endpoint with dummy region assumed (e.g
25-
// for URLs to emulated services).
26-
func DynamoConfigFromURL(awsURL *url.URL) (*dynamodb.Options, error) {
27-
httpClient := &http.Client{
28-
Transport: &http.Transport{
29-
Proxy: http.ProxyFromEnvironment,
30-
DialContext: (&net.Dialer{
31-
Timeout: 30 * time.Second,
32-
KeepAlive: 30 * time.Second,
33-
DualStack: true,
34-
}).DialContext,
35-
MaxIdleConns: 100,
36-
IdleConnTimeout: 90 * time.Second,
37-
MaxIdleConnsPerHost: 100,
38-
TLSHandshakeTimeout: 3 * time.Second,
39-
ExpectContinueTimeout: 1 * time.Second,
40-
},
41-
}
42-
config := dynamodb.Options{HTTPClient: httpClient}
43-
44-
// Use a custom http.Client with the golang defaults but also specifying
45-
// MaxIdleConnsPerHost because of a bug in golang https://github.com/golang/go/issues/13801
46-
// where MaxIdleConnsPerHost does not work as expected.
47-
48-
if awsURL.User != nil {
49-
username := awsURL.User.Username()
50-
password, _ := awsURL.User.Password()
51-
52-
// We request at least the username or password being set to enable the static credentials.
53-
if username != "" || password != "" {
54-
config.Credentials = credentials.NewStaticCredentialsProvider(username, password, "")
55-
}
56-
}
57-
58-
if strings.Contains(awsURL.Host, ".") {
59-
region := os.Getenv("AWS_REGION")
60-
if region == "" {
61-
region = "dummy"
62-
}
63-
config.Region = region
64-
if awsURL.Scheme == "https" {
65-
config.BaseEndpoint = aws.String(fmt.Sprintf("https://%s", awsURL.Host))
66-
}
67-
config.BaseEndpoint = aws.String(fmt.Sprintf("http://%s", awsURL.Host))
68-
} else {
69-
config.Region = awsURL.Host
70-
}
71-
// Let AWS generate default endpoint based on region passed as a host in URL.
72-
return &config, nil
73-
}
7+
const InvalidAWSRegion = "dummy"
748

75-
func CredentialsFromURL(awsURL *url.URL) (key, secret string) {
9+
func credentialsFromURL(awsURL *url.URL) (key, secret string) {
7610
if awsURL.User != nil {
7711
username := awsURL.User.Username()
7812
password, _ := awsURL.User.Password()

‎pkg/storage/chunk/client/aws/config_test.go‎

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import (
44
"net/url"
55
"testing"
66

7+
"github.com/aws/aws-sdk-go-v2/service/s3"
8+
"github.com/grafana/dskit/flagext"
79
"github.com/stretchr/testify/require"
10+
11+
"github.com/grafana/loki/v3/pkg/storage/chunk/client/hedging"
812
)
913

1014
func TestCredentialsFromURL(t *testing.T) {
@@ -44,16 +48,145 @@ func TestCredentialsFromURL(t *testing.T) {
4448
expectedKey: "key",
4549
expectedSecret: "secret",
4650
},
51+
{
52+
name: "URL with credentials and bucket",
53+
urlStr: "s3://key:secret@us-east-1/bucket",
54+
expectedKey: "key",
55+
expectedSecret: "secret",
56+
},
4757
}
4858

4959
for _, tt := range tests {
5060
t.Run(tt.name, func(t *testing.T) {
5161
u, err := url.Parse(tt.urlStr)
5262
require.NoError(t, err)
5363

54-
key, secret := CredentialsFromURL(u)
64+
key, secret := credentialsFromURL(u)
5565
require.Equal(t, tt.expectedKey, key)
5666
require.Equal(t, tt.expectedSecret, secret)
5767
})
5868
}
5969
}
70+
71+
func urlValue(s string) flagext.URLValue {
72+
url := &flagext.URLValue{}
73+
_ = url.Set(s)
74+
return *url
75+
}
76+
77+
func TestS3ClientOptions(t *testing.T) {
78+
79+
t.Run("s3 schema with region as hostname", func(t *testing.T) {
80+
cfg := S3Config{
81+
S3: urlValue("s3://us-east-0/bucket"),
82+
}
83+
fn, _ := s3ClientConfigFunc(cfg, hedging.Config{}, false)
84+
opts := s3.Options{}
85+
fn(&opts)
86+
87+
require.Equal(t, "us-east-0", opts.Region)
88+
require.Nil(t, opts.BaseEndpoint)
89+
})
90+
91+
t.Run("https schema with region as hostname", func(t *testing.T) {
92+
cfg := S3Config{
93+
S3: urlValue("https://us-east-0/bucket"),
94+
}
95+
fn, _ := s3ClientConfigFunc(cfg, hedging.Config{}, false)
96+
opts := s3.Options{}
97+
fn(&opts)
98+
99+
require.Equal(t, "us-east-0", opts.Region)
100+
require.Nil(t, opts.BaseEndpoint)
101+
})
102+
103+
t.Run("s3 schema with endpoint hostname", func(t *testing.T) {
104+
cfg := S3Config{
105+
S3: urlValue("s3://s3.us-east-0.amazonaws.com/bucket"),
106+
}
107+
fn, _ := s3ClientConfigFunc(cfg, hedging.Config{}, false)
108+
opts := s3.Options{}
109+
fn(&opts)
110+
111+
require.Equal(t, "", opts.Region)
112+
require.Equal(t, "http://s3.us-east-0.amazonaws.com", *opts.BaseEndpoint)
113+
})
114+
115+
t.Run("https schema with endpoint hostname", func(t *testing.T) {
116+
cfg := S3Config{
117+
S3: urlValue("https://s3.us-east-0.amazonaws.com/bucket"),
118+
}
119+
fn, _ := s3ClientConfigFunc(cfg, hedging.Config{}, false)
120+
opts := s3.Options{}
121+
fn(&opts)
122+
123+
require.Equal(t, "", opts.Region)
124+
require.Equal(t, "https://s3.us-east-0.amazonaws.com", *opts.BaseEndpoint)
125+
})
126+
127+
t.Run("access key and secret in url", func(t *testing.T) {
128+
cfg := S3Config{
129+
S3: urlValue("s3://accesskey:secret@us-east-0/bucket"),
130+
}
131+
fn, _ := s3ClientConfigFunc(cfg, hedging.Config{}, false)
132+
opts := s3.Options{}
133+
fn(&opts)
134+
135+
cred, err := opts.Credentials.Retrieve(t.Context())
136+
require.NoError(t, err)
137+
require.Equal(t, "accesskey", cred.AccessKeyID)
138+
require.Equal(t, "secret", cred.SecretAccessKey)
139+
})
140+
141+
t.Run("fields override url config", func(t *testing.T) {
142+
cfg := S3Config{
143+
S3: urlValue("s3://accesskey:secret@us-east-0/bucket"),
144+
AccessKeyID: "loki",
145+
SecretAccessKey: flagext.SecretWithValue("s3cr3t"),
146+
Endpoint: "s3.eu-south-0.amazonaws.com",
147+
}
148+
fn, _ := s3ClientConfigFunc(cfg, hedging.Config{}, false)
149+
opts := s3.Options{}
150+
fn(&opts)
151+
152+
cred, err := opts.Credentials.Retrieve(t.Context())
153+
require.NoError(t, err)
154+
require.Equal(t, "loki", cred.AccessKeyID)
155+
require.Equal(t, "s3cr3t", cred.SecretAccessKey)
156+
require.Equal(t, "https://s3.eu-south-0.amazonaws.com", *opts.BaseEndpoint)
157+
})
158+
159+
t.Run("insecure=false flag uses https for endpoint", func(t *testing.T) {
160+
cfg := S3Config{
161+
Endpoint: "minio:9100",
162+
Insecure: false,
163+
}
164+
fn, _ := s3ClientConfigFunc(cfg, hedging.Config{}, false)
165+
opts := s3.Options{}
166+
fn(&opts)
167+
168+
require.Equal(t, "https://minio:9100", *opts.BaseEndpoint)
169+
})
170+
171+
t.Run("insecure=true flag uses http for endpoint", func(t *testing.T) {
172+
cfg := S3Config{
173+
Endpoint: "minio:9100",
174+
Insecure: true,
175+
}
176+
fn, _ := s3ClientConfigFunc(cfg, hedging.Config{}, false)
177+
opts := s3.Options{}
178+
fn(&opts)
179+
180+
require.Equal(t, "http://minio:9100", *opts.BaseEndpoint)
181+
})
182+
183+
t.Run("region is set to invalid when url is not present", func(t *testing.T) {
184+
cfg := S3Config{}
185+
fn, _ := s3ClientConfigFunc(cfg, hedging.Config{}, false)
186+
opts := s3.Options{}
187+
fn(&opts)
188+
189+
require.Equal(t, InvalidAWSRegion, opts.Region)
190+
})
191+
192+
}

‎pkg/storage/chunk/client/aws/dynamodb_storage_client.go‎

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import (
88
"net"
99
"net/http"
1010
"net/url"
11+
"os"
1112
"reflect"
1213
"strings"
1314
"time"
1415

1516
"github.com/IBM/ibm-cos-sdk-go/aws"
1617
"github.com/IBM/ibm-cos-sdk-go/aws/request"
18+
"github.com/aws/aws-sdk-go-v2/credentials"
1719
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
1820
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
1921
"github.com/aws/smithy-go"
@@ -922,28 +924,63 @@ func dynamoOptionsFromURL(awsURL *url.URL) (dynamodb.Options, error) {
922924
if len(path) > 0 {
923925
level.Warn(log.Logger).Log("msg", "ignoring DynamoDB URL path", "path", path)
924926
}
925-
config, err := DynamoConfigFromURL(awsURL)
927+
config, err := dynamodbOptionsFromURL(awsURL)
926928
if err != nil {
927929
return dynamodb.Options{}, err
928930
}
929931
config.RetryMaxAttempts = 0 // We do our own retries, so we can monitor them
930-
config.HTTPClient = &http.Client{Transport: defaultTransport}
931932
return *config, nil
932933
}
933934

934-
// Copy-pasted http.DefaultTransport
935-
var defaultTransport http.RoundTripper = &http.Transport{
936-
Proxy: http.ProxyFromEnvironment,
937-
DialContext: (&net.Dialer{
938-
Timeout: 30 * time.Second,
939-
KeepAlive: 30 * time.Second,
940-
}).DialContext,
941-
ForceAttemptHTTP2: true,
942-
MaxIdleConns: 100,
943-
// We will connect many times in parallel to the same DynamoDB server,
944-
// see https://github.com/golang/go/issues/13801
945-
MaxIdleConnsPerHost: 100,
946-
IdleConnTimeout: 90 * time.Second,
947-
TLSHandshakeTimeout: 10 * time.Second,
948-
ExpectContinueTimeout: 1 * time.Second,
935+
// Deprecated: Has been copied from ConfigFromURL which was used for generating the configuration for both S3 and DynamoDB
936+
// when the Amazon AWS SKD v1 was still used.
937+
func dynamodbOptionsFromURL(awsURL *url.URL) (*dynamodb.Options, error) {
938+
httpClient := &http.Client{
939+
Transport: &http.Transport{
940+
Proxy: http.ProxyFromEnvironment,
941+
DialContext: (&net.Dialer{
942+
Timeout: 30 * time.Second,
943+
KeepAlive: 30 * time.Second,
944+
}).DialContext,
945+
ForceAttemptHTTP2: true,
946+
MaxIdleConns: 100,
947+
// We will connect many times in parallel to the same DynamoDB server,
948+
// see https://github.com/golang/go/issues/13801
949+
MaxIdleConnsPerHost: 100,
950+
IdleConnTimeout: 90 * time.Second,
951+
TLSHandshakeTimeout: 10 * time.Second,
952+
ExpectContinueTimeout: 1 * time.Second,
953+
},
954+
}
955+
config := dynamodb.Options{HTTPClient: httpClient}
956+
957+
// Use a custom http.Client with the golang defaults but also specifying
958+
// MaxIdleConnsPerHost because of a bug in golang https://github.com/golang/go/issues/13801
959+
// where MaxIdleConnsPerHost does not work as expected.
960+
961+
if awsURL.User != nil {
962+
key, secret := credentialsFromURL(awsURL)
963+
964+
// We request at least the username or password being set to enable the static credentials.
965+
if key != "" || secret != "" {
966+
config.Credentials = credentials.NewStaticCredentialsProvider(key, secret, "")
967+
}
968+
}
969+
970+
if strings.Contains(awsURL.Host, ".") {
971+
region := os.Getenv("AWS_REGION")
972+
if region == "" {
973+
region = InvalidAWSRegion
974+
}
975+
config.Region = region
976+
if awsURL.Scheme == "https" {
977+
config.BaseEndpoint = aws.String(fmt.Sprintf("%s://%s", awsURL.Scheme, awsURL.Host))
978+
} else {
979+
config.BaseEndpoint = aws.String(fmt.Sprintf("http://%s", awsURL.Host))
980+
}
981+
} else {
982+
config.Region = awsURL.Host
983+
}
984+
// Let AWS generate default endpoint based on region passed as a host in URL.
985+
return &config, nil
949986
}

0 commit comments

Comments
 (0)