Skip to content

Commit ad8907d

Browse files
authored
support/datastore: Support reading from public s3 buckets (#5752)
* support/datastore: Support reading from public s3 buckets * Add a log line
1 parent dc4f4c8 commit ad8907d

File tree

2 files changed

+53
-14
lines changed

2 files changed

+53
-14
lines changed

support/datastore/s3_datastore.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,19 @@ import (
66
"errors"
77
"fmt"
88
"io"
9+
"net/http"
910
"os"
1011
"path"
1112
"strings"
1213

1314
"github.com/aws/aws-sdk-go-v2/aws"
15+
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
1416
"github.com/aws/aws-sdk-go-v2/config"
1517
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
1618
"github.com/aws/aws-sdk-go-v2/service/s3"
1719
"github.com/aws/aws-sdk-go-v2/service/s3/types"
1820
"github.com/aws/smithy-go"
21+
1922
"github.com/stellar/go/support/log"
2023
"github.com/stellar/go/support/url"
2124
)
@@ -50,6 +53,15 @@ func NewS3DataStore(ctx context.Context, datastoreConfig DataStoreConfig) (DataS
5053
if endpointUrl != "" {
5154
o.BaseEndpoint = aws.String(endpointUrl)
5255
}
56+
57+
// Check if default credentials were successfully retrieved from the chain.
58+
// If not, fall back to anonymous credentials for public S3 bucket access.
59+
_, err := cfg.Credentials.Retrieve(ctx)
60+
if err != nil {
61+
log.Infof("No default AWS credentials found, configuring S3 client for anonymous access")
62+
o.Credentials = aws.AnonymousCredentials{}
63+
}
64+
5365
o.Region = region
5466
o.UsePathStyle = true
5567
})
@@ -68,10 +80,37 @@ func FromS3Client(ctx context.Context, client *s3.Client, bucketPath string, sch
6880
bucketName := parsed.Host
6981
uploader := manager.NewUploader(client)
7082

71-
input := &s3.HeadBucketInput{Bucket: aws.String(bucketName)}
72-
_, err = client.HeadBucket(ctx, input)
83+
log.Infof("Creating S3 client for bucket: %s, prefix: %s", bucketName, prefix)
84+
85+
listInput := &s3.ListObjectsV2Input{
86+
Bucket: aws.String(bucketName),
87+
Prefix: aws.String(prefix),
88+
MaxKeys: aws.Int32(1),
89+
}
90+
91+
_, err = client.ListObjectsV2(ctx, listInput)
92+
7393
if err != nil {
74-
return nil, fmt.Errorf("failed to head bucket, the bucket may not exist or you may not have access: %w", err)
94+
// check for http redirect specifically as it implies a region issue.
95+
var responseError *awshttp.ResponseError
96+
if errors.As(err, &responseError) {
97+
if responseError.HTTPStatusCode() == http.StatusMovedPermanently {
98+
return nil, fmt.Errorf("bucket '%s' requires a different endpoint (301 PermanentRedirect)."+
99+
" Please ensure the correct region is configured: %w", bucketName, err)
100+
}
101+
}
102+
103+
var apiError smithy.APIError
104+
if errors.As(err, &apiError) {
105+
switch apiError.ErrorCode() {
106+
case "NoSuchBucket":
107+
return nil, fmt.Errorf("bucket '%s' does not exist (NoSuchBucket): %w", bucketName, err)
108+
case "AccessDenied":
109+
return nil, fmt.Errorf("access denied to bucket '%s' (AccessDenied): %w", bucketName, err)
110+
default:
111+
}
112+
}
113+
return nil, fmt.Errorf("failed to list objects in bucket '%s': %w", bucketName, err)
75114
}
76115

77116
return S3DataStore{client: client, uploader: uploader, bucket: bucketName, prefix: prefix, schema: schema}, nil
@@ -107,12 +146,14 @@ func (b S3DataStore) GetFile(ctx context.Context, filePath string) (io.ReadClose
107146

108147
output, err := b.client.GetObject(ctx, input)
109148
if err != nil {
149+
log.Errorf("Error retrieving file '%s': %v", filePath, err)
110150
if isNotFoundError(err) {
111151
return nil, os.ErrNotExist
112152
}
113-
return nil, err
153+
return nil, fmt.Errorf("error retrieving file %s: %w", filePath, err)
114154
}
115155

156+
log.Infof("File retrieved successfully: %s", filePath)
116157
return output.Body, nil
117158
}
118159

support/datastore/s3_datastore_test.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,11 @@ func (s *mockS3Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
4141

4242
switch r.Method {
4343
case http.MethodHead:
44-
// See https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadBucket.html
4544
// See https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html
4645
s.handleHeadRequest(w, pathParts)
4746
case http.MethodGet:
4847
// See https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html
49-
s.handleGetRequest(w, pathParts)
48+
s.handleGetRequest(w, r, pathParts)
5049
case http.MethodPut:
5150
// See https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
5251
s.handlePutRequest(w, r, pathParts)
@@ -56,13 +55,6 @@ func (s *mockS3Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
5655
}
5756

5857
func (s *mockS3Server) handleHeadRequest(w http.ResponseWriter, pathParts []string) {
59-
// Handle HeadBucket: A request with no key part in the path.
60-
// We assume the bucket always exists in the mock.
61-
if len(pathParts) < 2 || pathParts[1] == "" {
62-
w.WriteHeader(http.StatusOK)
63-
return
64-
}
65-
6658
// Handle HeadObject: A request with a key.
6759
key := pathParts[1]
6860
obj, exists := s.objects[key]
@@ -75,7 +67,13 @@ func (s *mockS3Server) handleHeadRequest(w http.ResponseWriter, pathParts []stri
7567
w.WriteHeader(http.StatusOK)
7668
}
7769

78-
func (s *mockS3Server) handleGetRequest(w http.ResponseWriter, pathParts []string) {
70+
func (s *mockS3Server) handleGetRequest(w http.ResponseWriter, r *http.Request, pathParts []string) {
71+
// Check for query param for ListObjectsV2
72+
if r.Method == http.MethodGet && r.URL.Query().Get("list-type") == "2" {
73+
w.WriteHeader(http.StatusOK)
74+
return
75+
}
76+
7977
if len(pathParts) < 2 {
8078
http.Error(w, "Invalid path: Key is required for GET", http.StatusBadRequest)
8179
return

0 commit comments

Comments
 (0)